Host Europe Blog

Analyse und Verifizierung von Software-Architektur-Regeln mittels jQAssistant

Titelmotiv - Analyse und Verifizierung von Software-Architektur-Regeln mittels jQAssistant - Expertenbeitrag von Stefan Fenn im Host Europe Blog

In vielen Software-Systemen und Software-Projekten besteht die Herausforderung, die Software einheitlich und gemäß der entschiedenen Architektur-Regeln zu entwickeln.

Auch neue Entwickler sollen die Architektur-Regeln einhalten oder sogar nach alten Architektur-Regeln arbeiten, falls das Projekt nur noch gewartet wird. Eine Dokumentation der Architektur-Regeln untersteht genauso wie Code-Kommentare der Software-Erosion und führt in der Regel zu mehr Verwirrung, als dass es eine Unterstützung ist. Mein Lieblings-Code-Kommentar ist folgender:

Deshalb habe ich nach Möglichkeiten gesucht, Architektur-Regeln in den Build-Prozess von Projekten zu integrieren. Eine der wichtigsten Regeln ist die Einhaltung der Architektur-Schichten, so dass z.B. keine Klasse der Rest-Controller-Schicht direkt mit der DB-Schicht interagiert, sondern die Service-Schicht verwenden muss. Ich habe mir folgende Lösungsansätze dafür genauer angesehen:

Mit Java-Modules können nur Architektur-Schichten festgelegt werden, und bei AspectJ ist es sehr komplex, Regeln zu definieren. Checkstyle und Annotation-Prozessoren können den Java-Code (Byte-Code, Source-Code) analysieren, aber es ist ein relativ aufwändiges Unterfangen und kann schwer parametrisiert werden.

Analyse mit jQAssistant

Schlussendlich bin ich zu dem Maven-Plugin jQAssistant gekommen, welches für meine Wünsche die passendste Lösung ist. jQAssistant wird im Maven-Build-Prozess gestartet und analysiert unter anderen folgende Projekt-Artefakte:

Die Informationen werden in einer lokalen Neo4j Graphdatenbank gespeichert.

Um zu zeigen, was in die Datenbank aufgenommen wird, analysieren wir das Projekt Apache Commons Lang:

Apache Commons Lang für jQAssistant vorbereiten

Nach dem Import des Projekts reicht es, ein Maven-Plugin in den POM-File aufzunehmen.

Nach dem nächsten Build sind alle Informationen in der Datenbank aufgenommen. Mit dem Maven-Plugin kann auch ein Web-Server für Neo4j gestartet werden, der dann unter http://localhost:7474/ verfügbar ist.

Projekt-Übersicht mit jQAssistant

Unter den Klassen kann man z.B. nun nach allen “*Utils” Klassen suchen.

Die Suche “MATCH (n:Class) WHERE n.name ENDS WITH 'Utils' RETURN n” ist die spezielle DB-Abfragesprache Cypher, welche das Gegenstück zu SQL für Graphdatenbanken ist. Hier wird nach Knoten des Typs “Class” gesucht, deren Property “name” mit “Utils” endet. Die runden Klammern sollen den Knoten symbolisieren.

Hier kommt ein weiterer Vorteil von jQAssistant zum Vorschein. Es ist möglich, eine Code-Basis explorativ zu analysieren und daraufhin die Vermutungen per Cypher-Abfrage zu belegen.

In einem unbekannten Projekt interessiere ich mich als erstes für die komplexen, meist langen und unstrukturierten Methoden. Das kann näherungsweise mit dem zyklomatischen Index beschrieben werden.

Hier sucht man nach Knoten-Verbindungen, die sich in ASCII-Art mit (…)-[…]->(…) also Knoten-Kante-Knoten beschreiben lässt.

Offenbar enthält die Klasse “NumberUtils” zwei relativ komplexe Methoden “boolean isCreatable(java.lang.String)” und “java.lang.Number createNumber(java.lang.String)”. Auch der “effectiveLineCount” wird berechnet und beide Methoden sind größer als 60 Zeilen.

Bei meinen weiteren Analysen habe ich bemerkt, dass sich relativ häufig toString-Methoden hintereinander aufrufen, und dies könnte zu Performance-Problemen führen. Hier eine erweiterte Abfrage.

Interessanterweise wurde damit ein rekursiver Aufruf gefunden, den man sonst nicht sofort sehen würde.

java.lang.String classToString(java.lang.Class)” ruft hier “java.lang.String toString(java.lang.reflect.Type)” auf, welche rekursiv wieder andere Klassen mit classToString aufrufen kann. Eine StringBuffer oder StringBuilder Variante könnte die Performance für diese Methoden erhöhen.

Zyklische Abhängigkeiten

Für das Auffinden von zyklischen Package-Strukturen ist die Cypher-Abfrage schon erheblich länger, aber dennoch relativ gut lesbar.

Dieser Select gibt nun keinen Graphen als Ergebnis zurück, sondern eine Tabelle.

Um Architektur-Regeln zu definieren, können diese nun mit Cypher beschrieben werden.

Ideen für weitere Software-Architektur-Regeln

Folgende Regeln sind zum Beispiel bei uns verwendet worden:

Einbinung in JUnit

Um aus den Cypher-Abfragen Build-Constraints zu bekommen, wird im Projekt unter “jqassistant” für jedes Constraint eine XML-Datei abgelegt.

Mit jQAssistant kann man die Constraints auch gut parametrisieren. Das hilft, projektspezifische Anpassungen einfach zu definieren. Ein weiterer Vorteil ist, dass man Knoten per Cypher auch mit neuen Knoten-Typen annotieren kann. Damit werden Abfragen auf die neuen Knoten-Typen möglich und erleichtern die Lesbarkeit. Das folgende Beispiel zeigt beide Mechanismen.

Cypher im Detail

Um auch alle Architektur-Constraints definieren zu können, ist ein fundiertes Wissen über Cypher unausweichlich. Deshalb werden im Folgenden die wichtigsten Cypher-Details erklärt.

Oft ist es hilfreich, den Neo4j-Server ohne jQAssistant zum Testen von Abfragen zu starten. Damit kann man gezielte Szenarien und interessante Konstellationen für die Abfrage einfach erstellen.

Mit Docker ist der Neo4j-Server schnell erstellt.

Ohne das “volume”-Attribut werden alle Daten nach dem Stop des Containers gelöscht.

Knoten, Kanten und Attribute können einfach mit Cypher erstellt werden.

Die allgemeine Abfrage-Syntax lautet wie folgt:

“MATCH” ist die Selektion des Graphen, die nach “MATCH” als Graph-Template definiert wird. Es können auch mehrere Graph-Templates komma getrennt aufgeführt werden. “OPTIONAL MATCH” sind Graph-Templates, die vorkommen können, aber nicht müssen.

Der “WITH” Absatz ist ähnlich zu “RETURN”: es wird definiert, was für die darunterliegende Absätze verwendet wird. In der Regel werden die Ausdrücke mit “… AS <name>” für die Verwendung zu einer Variablen gebunden. Alle Variablen, die darunterliegend verwendet werden sollen, müssen in WITH angegeben werden.

“SKIP” und “LIMIT” sind die aus SQL bekannten Paging-Mechanismen.

MATCH

Im Graph-Template gibt es folgende Möglichkeiten:

RETURN / WITH

Hauptsächlich in RETURN und WITH werden die Aggregations-Funktionen verwendet:

WHERE

Folgende Operatoren werden unterstützt (die Liste ist nicht vollständig):

Des Weiteren gibt es auch Listen-Operatoren:

Auch sind Allquantoren mittels Listen-Prädikate möglich:

Abschließendes Beispiel: Selektiere alle Klassen, die in einen bestimmten Package liegen und zu der es keine entsprechende “Data” Klasse gibt.

  1. Selektiere alle Knoten mit Klassen-Label.
  2. Filter Klassen, die nicht mit “com.wogra” beginnen, heraus.
  3. Aggregiere alle Knoten zu einer Liste.
  4. Starte mit einer neuen Suche und selektiere alle Knoten mit Klassen-Label. Binde die Knoten an die Referenz c1.
  5. Behalte c1, falls es keine Klasse c2 gibt, die den gleichen Namen wie c1 und dazu “Data” hat und…
  6. Mit “com.wogra” startet, mit “Data” endet und kein “$” enthält (keine anonyme oder innere Klasse).
  7. Gebe c1 zurück.

Fazit

jQAssistant bietet eine gute Möglichkeit, Software-Architektur-Regeln flexibel zu definieren. Es stehen viele Plugins zur Verfügung. Auch die Anbindung von neuen Projekt-Artefakten für die Graphen-Datenbank ist mit geringem Aufwand möglich. Lediglich die Syntax und Semantik von Cypher muss selbst für einfache Abfragen beherrscht werden.

jQAssistant kann aber auch für Neo4j-Interessenten nützlich sein — hat man doch damit sehr schnell einen komplexen fachgetriebenen Graphen aufgebaut, womit man seine Abfrage-Künste ausprobieren kann.

Die mobile Version verlassen