Dienstag, 30. August 2011

Groovy vereinfacht SAP-Zugriffe mit dem Java Connector

Es ist keine Propaganda zur Verbreitung einer (nicht mehr so ganz) neuen Programmierprache, sondern es stimmt wirklich: Wer seine Aufgaben in Groovy statt in Java löst, produziert wesentlich weniger Code, der obendrein noch besser lesbar ist. Das kann man mit allen möglichen Aufgaben zeigen – zum Beispiel auch mit einem RFC-Aufruf in ein SAP-System und der Auswertung der Rückgabewerte, wobei für die Verbindung der Java Connector zum Einsatz kommt.

Die Besonderheit von Groovy ist, dass es - als dynamische Programmiersprache - zur Laufzeit versucht, das vom Programmierer Beabsichtigte herauszubekommen und damit meistens erstaunlich richtig liegt. Wenn es sich doch einmal anders als gewünscht entscheidet, kann man ihm diese Unarten ohne viel Aufwand abgewöhnen. So entfällt im Code viel "syntaktisches Rauschen" (syntactic noise), wie man es von statischen Programmiersprachen kennt. Übrig bleibt der näher beim Problem liegende Code. Durch diese Beschränkung auf das Wesentliche wird der verbleibende Code automatisch lesbarer. Es gibt in Groovy die Möglichkeit, bei wichtigen grundlegenden Zeitpunkten zur Laufzeit einzugreifen, etwa beim Methoden-Lookup und beim Setzen und Abfragen eines Attributs. Dadurch hat man die Möglichkeit, die Syntax des Codes zu gestalten. Diese Freiheit kann genutzt werden, um die Klarheit, Lesbarkeit und Kompaktheit des Codes noch weiter zu steigern.

Was ist der Preis? Man könnte argwöhnen, dass die Performance durch die vielen dynamischen Features von Groovy leidet. Zunächst wird man überrascht sein zu sehen, dass die Performance von Groovy-Programmn entgegen solchen Erwartungen sehr gut ist. Dennoch mag der Einwand für Programmteile stimmen, in denen hochperformant gearbeitet werden muss. Da Groovy mit Java kompatibel ist, hat man aber immer die Möglichkeit, mit Plain Old Java Objects (POJOs) zu arbeiten oder im Extremfall – wie in Java – native Bibliotheken aufzurufen. Je näher man aber in den Bereich der Benutzerschnittstelle kommt – wenn Dialoge involviert sind oder auf irgendeine Art die unendlich langsame menschliche Reaktionszeit ins Spiel kommt – desto unerheblicher wird die Frage, ob man mit einer statischen oder dynamischen Sprache arbeitet. Für Dinge wie automatische Tests - in denen Benutzerschnittstellen durchlaufen werden - ist das Laufzeitverhalten einer dynamischen Sprache wie Groovy mehr als ausreichend.

Wie gut man eine Sprache wie Groovy zur Formulierung von automatischen Tests verwenden kann, habe ich in meinem letzten Blog über Selenium und Groovy gezeigt.

Hier will ich beschreiben, wie sich die Zugriffe auf komplexe ABAP-Datenstrukturen mit Groovy vereinfachen. Für den Zugriff selbst verwende ich dabei den SAP Java Connector (JCo). Der JCo ist ein Tool zur Ausführung entfernter Funktionsbausteinaufrufe (RFC) in einem SAP-System. Mittels eines Metadaten-API kann man den Aufruf vor dem Baustein mit Daten versorgen und hinterher aus den Rückgabewerten Daten auslesen, wobei viele Daten bereits passend konvertiert werden: Ein ABAP-Feld vom Typ D (Datum) ergibt z.B. in Java einen Feldwert vom Typ java.util.Date.

Nun muss man zum Auslesen komplexer Daten auch in Java viel hinschreiben. Für jede einzelne Auswertung bedarf es eines eigenen Methodenaufrufs, und Zwischenergebniss müssen häufig in lokalen Variablen gehalten werden. Eine komplexe Struktur order müsste man mit dem JCo z.B. wie folgt auslesen, um den Preis der 5. Position zu ermitteln:
double getPrice( JCO.Record order, int pos) {
// ITEMS sei Komponente der ABAP-Struktur BELEG
JCO.Table items = order.getValue( "ITEMS" );
// Workarea der JCO.Table auf Position pos setzen
items.setRow( pos );
// Nun kann der Preis ausgelesen werden
return items.getValue( "PRICE" );
}

Diese aufwendigen Folge von im Grunde uninteressanten Methodenaufrufen stört das Auge und erschwert den Blick auf das Wesentliche. Wäre es nicht schöner, einfach folgendes schreiben zu können?
order.ITEMS[pos].PRICE

Groovy bietet die Möglichkeit, die Wertabfrage komplexer Daten genau so zu notieren. Man könnte den Ausdruck sogar auf der linken Seite stehen haben,
order.ITEMS[pos].PRICE = 10.00

und es würde eine Zuweisung an die Komponente PRICE der Zeile pos in der tabellenförmigen Komponente ITEMS der Struktur order ausgeführt.

Wie könnte man so etwas erreichen? Zunächst braucht man einen Wrapper (Adapter) - eine Klasse, die intern Referenzen auf die JCO-Objekte verwaltet, deren Methoden aufruft und die Möglichkeit der Rekursion bietet – denn Rekursion ist für Werteabfragen komplexer Datenobjekte das passende Verfahren:
class Data {

private JCO.Record record

Data( JCO.Record record) {
this.record = record;
}
...
}

Nun kommt etwas Groovy-Spezifisches: Mit den Methoden getProperty(name) und setProperty(name,value) kann in Groovy der Member-Operator "." überschrieben werden. Wenn die Groovy-Laufzeit auf den "." trifft, so versucht sie, bevor der Zugriff auf ein statisches Attribut gemacht wird, zuerst die Methode getProperty (oder setProperty auf der linken Seite einer Zuweisung) auszuführen, wenn eine Methode dieses Namens vorhanden ist. Ist dies der Fall, so wird die Operation auf die Methoden umgelenkt. Das heisst, an Stelle des Zugriffs a.b wird intern a.getProperty( "b" ) evaluiert, und an Stelle der Zuweisung a.b = c wird intern die Methode a.setProperty( "b", c ) ausgeführt.

Obwohl also ein Data-Objekt für die Zeilenstruktur der USERLIST kein Attribut USERNAME besitzt, kann der Zugriff so ausgeführt werden, als wäre es vorhanden. Hinter den Kulissen werden dabei die Methoden getProperty bzw. setProperty aufgerufen. Die Rekursion lösen wir, indem wir das Ergebnis automatisch in eine neue Data-Instanz einpacken, falls es wieder ein komplexes Datenobjekt ist. Die Methoden sehen daher im wesentlichen so aus:
class Data {
...
def getProperty(String name) {
// Spezielle Attribute des Datenobjekts müssen hier gesondert behandelt werden
if (name == "length") return record.getNumRows()
if (name == "record") return record
// Allgemeiner Fall: Mit JCo in der Struktur record suchen
def f = record.getValue( name )
if (record.isTable( name ) || record.isStructure( name ))
return new Data( f )
else {
return f
}
}

void setProperty(String name, Object value) {
record.getField( name ).setValue( value );
}
...
}

Auch den Subscript-Operator [] können wir für Data-Objekte nach eigenem Gusto definieren.
  def getAt(i) {
record.setRow( i );
// Danach erfolgt ein Komponetenzugriff
this
}

Hier wird nur die Anfrage nach der i-ten Tabellenzeile vom Data-Objekt an das JCo-Repository-Objekt record delegiert. Die Rückgabe von this dient nur dem method chaining: Nachdem man in eine JCo.Table mittels setRow eine Zeile eingestellt hat, funktioniert sie so wie eine JCo.Structure – man kann auf einzelne Komponenten der Zeile mittels getValue zugreifen. Das wäre aber genau das, was Data beim Komponentenzugriff sowieso macht.

Als angenehme Zugabe habe ich noch eine Methode set vorgesehen, die es erlaubt, mehrere Komponenten einer Struktur auf einmal zu setzen. So kann ich mehrere Name/Wert-Paare auf einmal übergeben. Hier ein Beispielaufruf, um zwei Importparameter eines Funktionsbausteins auf einmal zu setzen:
    userList.importParameter.set( 
MAX_ROWS : 50,
WITH_USERNAME : "X"
)

Die Lesbarkeit ist deutlich besser - während die Implementierung der benötigten set()-Methode denkbar einfach ist:
  def set = { pairs ->
for (p in pairs) {
setProperty p.key, p.value
}
}

Schliesslich können wir noch das Interface Iterator sinnvoll für interne Tabellen implementieren:
class Data implements Iterator { 
...
// Iterator interface
void remove() {}

boolean hasNext() {
return !record.isLastRow()
}

def next() {
record.nextRow()
this
}

Was ist der Gewinn? Wir können in Groovy (und auch in Java) nun mit dem for ( .. in .. ) wie in einer ABAP-Loop die Zeilen der Tabelle durchlaufen. Mit der folgenden kleinen display-Methode bilden wir z.B. aus den Spalten USERNAME und FULLNAME einer internen Tabelle einen zweidimensionalen Array, wie ihn die JTable als Datenoobjekt entgegennimmt:
// Show a JTable with the result    
void display( userlist ) {
def data = []
for (record in userlist) {
data.add( [ record.USERNAME, record.FULLNAME ] )
}
new GUI().showTable( data, ["Benutzername","Bürgerlicher Name"] )
}

Der record ist hier, was in ABAP eine Workarea wäre. Die Schleife ist in dieser angenehm lesbaren Form verwendbar, weil userlist das Interface Iterator implementiert.

Die folgende Testmethode ruft den Funktionsbaustein BAPI_USER_GETLIST per RFC auf und stellt die Ergebnisse in einer Tabelle dar. Was sich für andere Aufgaben wiederverwenden lässt, ist in Hilfsklassen ausgelagert.
  @Test void test() {      

// Provide handle to a remotely callable function
def userList = repo.getFunction "BAPI_USER_GETLIST"

// Parametrize it
userList.importParameter.set(
MAX_ROWS : 50,
WITH_USERNAME : "X"
)

// Do the call
userList.execute( )

// Get and display result
display userList.tableParameter.USERLIST

}


Ich habe nicht geschummelt. Das vollständige Codebeispiel zum Auslesen und Anzeigen der Tabelle von Benutzern eines SAP-Systems ist nicht wesentlich grösser als die hier gezeigten Ausschnitte und kann auf Pastebin unter folgendem Link eingesehen werden:

Accessing ABAP data efficiently

[1] Ich verwende die Groovy-Version 1.8.0, in das gemäss Release Notes einiger Aufwand in Performance-Optimierungen gesetzt wurde. Im übrigen gilt für Groovy wie für alles andere: "Don't blame the tools". 90% aller Performanceprobleme kommen nicht der verwendeten Plattform, sondern dem Applikationscode zuschulden.

Keine Kommentare :