Kontinuierliche Architekturvalidierung mit jQAssistant

Keine Kommentare

Alles getestet?

In Softwareprojekten wird viel Wert auf Qualität gelegt. Die Qualität der geschriebenen Software wird dabei durch verschiedene Testarten wie Unittest, Integrationstests und Akzeptanztest sichergestellt. Gemäß der Testpyramide gibt es dabei viele, einfache und schnell ausführbare Unittests. Komplexere Tests wie Akzeptanztests mit Selenium gibt es wenige, weil diese meist aufwändiger zu pflegen sind und längere Durchlaufzeiten haben. Als zusätzliche Maßnahmen zur Sicherstellung der Softwarequalität werden oft noch weitere Tools wie Findbugs, PMD, Checkstyle oder serverbasierte Systeme wie SonarQube und Teamscale eingesetzt. Weitere Testarten wie Lasttests mit Gatling lassen Rückschlüsse auf das Laufzeitverhalten des Systems unter Last zu und können somit nochmals die Qualität erhöhen. Haben wir jetzt wirklich alles getestet? Wo sind die Tests für unsere Softwarearchitektur? Wie kann ich in einer größeren Codebasis mit einer langen Lebenszeit sicherstellen, dass Konzepte von heute auch später noch berücksichtigt werden?

Unser Beispiel

Als einfaches Beispiel soll folgendes Szenario dienen: Das Team einigt sich, dass sämtliche Methodenaufrufe auf die Klasse ‚TargetClass‘ nur über Interface ‚ProxyInterface‘, bzw. eine Implementierung dessen, erfolgen sollen.

Die Entscheidung wird getroffen und bestenfalls im Code dokumentiert. Nach einiger Zeit und personellen Wechseln im Team erinnert nur noch ein Kommentar an diese Entscheidung. Für ein neues Feature wird das erste Mal über Klasse ‚EvilCallerClass‘ direkt auf eine Methode aus Klasse ‚TargetClass‘ zugegegriffen. Die vormals getroffene Architekturentscheidung ist verletzt.

Je nach Grund für die Architekturentscheidung können die Auswirkungen ausfallen. Werden beispielsweise Aufrufe des Interfaces geproxied und zusätzliche Sicherheitschecks ausgeführt, würde diese bei direktem Aufruf nicht mehr erfolgen. Im Prinzip gibt es zwei Möglichkeiten:

  • Der direkte Aufruf ist gewollt und die Einschränkung ist nicht relevant oder
  • der Aufruf sollte nach dem bisherigen Muster über das Interface erfolgen.

In jedem Fall sollten die Entwickler über die Architekturverletzung informiert werden, um diese dann zu analysieren. Idealerweise erfolgt dieser Hinweis durch einen automatisierten Test, der Build wird abgebrochen und die Entwickler erhalten vom Buildserver eine Nachricht. Aber wie lässt sich diese Entscheidung automatisiert in Form eines Tests verifizieren?

Ein Fall für jQAssistant

Dieses Problem lässt sich gut mit dem Tool jQAssistant lösen. jQAssistant scannt dabei den Code, extrahiert Informationen und speichert diese in einer Neo4j-Graphdatenbank ab. Als Knoten werden dabei unter anderem Dateien, Klassen, Interfaces, Packages, Felder, Methoden und Annotationen angelegt. Kanten werden durch Schlüsselwörter wie CONTAINS, DEPENDS_ON, INVOKES, DECLARES, IMPLEMENTS und RETURNS abgebildet. Durch weitere Plugins kann der Graph nochmals erweitert werden, zum Beispiel um Code-Coverage, Informationen aus der Git-Historie, JUnit-Testergebnisse, Spring-spezifische Informationen, XML- und JSON-Strukturen und vieles mehr. Die Abfrage des Graphen erfolgt dann mit mithilfe der einfachen, aber mächtigen Abfragesprache Cypher. Eine Beispielabfrage für „Zeige alle Klassen“ sieht wie folgt aus:

MATCH
	(c:Class)
RETURN
	c

Match ist der Selektor, :Class gibt das Label an, das diesem Knoten zugeordnet ist. Ein Knoten kann dabei mehreren Labels zugeordnet sein. Klassen haben beispielsweise auch die Labels für File und Java. Als Rückgabewert erhält man alle Klassen des analysierten Projekts mit den entsprechenden Properties, wie den vollqualifizierten Namen und den Dateinamen. Da der analysierte Code als gerichteter Graph abgebildet ist, lässt sich dieser traversieren. So lassen sich aufrufende Klassen einer Methode leicht herausfinden, wie alle aufrufenden Methoden auf unsere ‚TargetClass‘.

MATCH
	(target:Class) -[:DECLARES]-> (targetMethod:Method) <-[:INVOKES]- (callerMethod:Method) <-[:DECLARES]- (caller:Class)
WHERE
	target.fqn = 'jqademo.directaccess.target.TargetClass'
RETURN
	DISTINCT caller.fqn

Der Aufruf selektiert Klassen, die Methoden definieren. Diese Methoden werden durch andere Methoden aufgerufen, die wiederum in einer aufrufenden Klasse definiert sind. Über die WHERE-clause wird gefiltert, welche Klassen zurückgegeben werden sollen. In unserem Fall handelt es sich dabei um die Zielklasse. Als Ergebnis werden nicht Knoten, sondern Properties des Knotens zurückgegeben, wobei fqn den vollqualifizierten Namen der Aufruferklasse zurückgibt. In unserem Beispiel werden durch das DISTINCT zwei Ergebnisse zurückgegeben.

  • Die Klasse, deren Aufruf über das Proxy-Interface erfolgt (ProxyImpl), und
  • die Klasse, die die Zielklasse direkt aufruft (EvilCallerClass).

Mit einer weiteren WHERE-clause erhalten wir das gewünschte Ergebnis, nämlich die Klasse, die kein Interface implementiert.

MATCH
	(target:Class) -[:DECLARES]-> (tm:Method) <-[:INVOKES]- (cm:Method) <-[:DECLARES]- (caller:Class)
WHERE
	target.fqn = 'jqademo.directaccess.target.TargetClass'
	AND NOT (caller) -[:IMPLEMENTS]-> (:Interface)
RETURN
	DISTINCT caller.fqn

Dieses einfache Beispiel kann beliebig komplexer gestaltet und mit weiteren Kriterien erweitert werden. Es kann beispielsweise das konkrete Interface angegeben werden, eine Ausnahme über eine Annotation markiert werden etc. Außerdem können weitere Pfade geprüft werden, Methoden aus Cypher angewendet werden und vieles mehr. Diese Flexibiblität ist der große Vorteil von jQAssistant durch die Verwendung der unterliegenden Neo4j-Datenbank.

Wie läuft das jetzt bei mir?

Jetzt, da unsere Architekturvalidierung in Cypher formuliert ist, soll diese automatisiert im Buildprozess integriert werden. Die Integration in ein Java-Projekt erfolgt entweder über ein Maven-Plugin oder über die Commandline. Für die Benutzung mit Gradle kann die Kommandozeile per JavaExec ausgeführt werden. Folgende Tasks sind für die Verwendung von jQAssistant sinnvoll:

  • jqaclean
  • jqascan
  • jqavalidate
task jqaclean(type: Delete) {
    delete 'jqassistant/store'
    delete 'jqassistant/report'
}

task(jqascan, dependsOn: 'jqaclean', type: JavaExec) {
    main = 'com.buschmais.jqassistant.commandline.Main'
    classpath = configurations.jqaRuntime
    args 'scan'
    args '-f'
    args 'java:classpath::build/classes/main'
}

Der Scan wird benötigt, um den vorhandenen Code und Dateien zu scannen. Die Pfade werden dabei als Argument mit übergeben. Per default werden Dateien im Ordner ‚jqassistant‘ abgelegt. Der Unterordner ’store‘ beinhaltet die Neo4j-Dateien, ‚report‘ die generierten Reports. Diese werden über den Task ‚jqaclean‘ gelöscht. Regeln werden im Ordner ‚jqassistant/rules‘ abgelegt – entweder in einem XML-Dokument oder einer AsciiDoc-Datei. Besonders die AsciiDoc-Variante hat den Vorteil, dass Code und Architektur so synchron bleiben, weil Teile der Dokumentation ausgeführt und verifiziert werden. Für das Beispiel wird unsere Regel in einem AsciiDoc definiert.

== Architecture

=== Use ProxyInterface

The TargetClass must be called through the ProxyInterface. Without the proxy TargetClass methods won't by monitored.

[[findAccessThroughProxy]]
.TargetClass must be accessed through ProxyInterface.
[source,cypher,role=constraint]
----
MATCH
	(target:Class) -[:DECLARES]-> (tm:Method) <-[:INVOKES]- (cm:Method) <-[:DECLARES]- (caller:Class)
WHERE
	target.fqn = 'jqademo.directaccess.target.TargetClass'
	AND NOT (caller) -[:IMPLEMENTS]-> (:Interface)
RETURN
	DISTINCT caller.fqn
----

This is a team decision from 27th Sep 2017. The build must break when the constraint is broken.

=== Severities

[[from-asciidoc]]
.Group description.
[role=group,severity=critical,includesConstraints="findAccessThroughProxy"]

The default severity for rules in this file is critical.

Die Regel hat die id ‚findAccessThroughProxy‘ und ist der Gruppe ‚from-asciidoc‘ zugeordnet. Die Dokumentation kann entsprechend ausführlich geschrieben werden. Es empfiehlt sich für jede Regel idealerweise anzugeben:

  • warum diese Regel benötigt wird,
  • von wem diese Entscheidung ausging und
  • wann diese Regel eingeführt wurde.

Durch diese Art der Dokumentation fällt es dem Entwickler leichter, eine Architekturverletzung besser bewerten zu können und die richtigen Maßnahmen zu ergreifen. Für die Integration in den Buildprozess fehlt noch der Gradle-Task zur Validierung der Regeln:

task(jqavalidate, dependsOn: 'jqascan', type: JavaExec) {
    main = 'com.buschmais.jqassistant.commandline.Main'
    classpath = configurations.jqaRuntime
    args 'analyze'
    args '-r'
    args 'jqassistant/rules'
    args '-warnOnSeverity'
    args 'INFO'
    args '-failOnSeverity'
    args 'MAJOR'
    args '-groups'
    args 'from-asciidoc'
}

Für die Validierung können die Levels von Warnungen und Fehlern konfiguriert werden. Warnungen werden als WARN geloggt, Fehler als ERROR. Im Fehlerfall wird zudem der Build gebrochen. Als Gruppe wird die in der AsciiDoc-Datei definierte ‚from-asciidoc‘ referenziert. Im Buildprozess muss für die kontinuerliche Validierung lediglich der Aufruf

./gradlew clean build jqavalidate

integriert werden, um die Architekturvalidierung im Build mitlaufen zu lassen.

Aber bitte mit Neo4j Version 3

Der Vollständigkeit halber werden noch die für jQAssistant relevanten Abhängigkeiten in der build.gradle dargestellt. Standardmäßig verwendet jQAssistant Neo4j in der Version 2, in der aktuellen Version 1.3.0 ist die Version 3 vorbereitet, aber noch nicht per default aktiviert. Durch die unten gezeigten Anpassungen wird die Version 3.1.3 von Neo4j verwendet. Damit können Features wie der Bolt-Support grundsätzlich verwendet werden.

project.ext["jqaversion"] = "1.3.0"
project.ext["jqapluginversion"] = "1.3"
project.ext["neo4jv3"] = "3.1.3" // we want Neo4j version 3

configurations {
    jqaRuntime
}

dependencies {
    jqaRuntime("com.buschmais.jqassistant:jqassistant-commandline:${project.jqaversion}") {
        exclude group: 'com.buschmais.jqassistant', module: 'neo4jserver.neo4jv2'  // exclude version 2
    }
    jqaRuntime("com.buschmais.jqassistant:neo4jserver.neo4jv3:${project.jqaversion}")
    jqaRuntime("org.neo4j:neo4j:${project.neo4jv3}")
    jqaRuntime("org.neo4j.app:neo4j-server:${project.neo4jv3}")

    jqaRuntime("com.buschmais.jqassistant.plugin:java:${project.jqapluginversion}")
}

Es können noch weitere Plugins zur jqaRuntime hinzugefügt werden. Diese werden dann für Scans verwendet. Das Plugin für den JUnit Support kann beispielsweise über

    jqaRuntime("com.buschmais.jqassistant.plugin:junit:${project.jqapluginversion}")

der jqaRuntime einfach hinzugefügt werden.

Eigene Regeln

Für eigene Regeln empfiehlt sich ein weiterer Gradle-Task ‚jqaserver‘.

task(jqaserver, type: JavaExec) {
    main = 'com.buschmais.jqassistant.commandline.Main'
    classpath = configurations.jqaRuntime
    args 'server'
    standardInput = System.in
}

Damit kann nach einem Scan der Neo4j Server mit der Graphstruktur unseres Projekts gestartet werden, um eigene Regeln zu schreiben. Dabei hilft die hervorragende UI von Neo4j, die im Browser unter http://localhost:7474/browser/ verfügbar ist. Über den Browser können Queries abgesetzt und visualisiert werden, die Ergebnisse in grafisch und tabellarisch angezeigt werden, sowie Daten exportiert und Bookmarks für Queries gesetzt werden.

neo4j_jqassistant

Das Bild zeigt das Ergebnis für die Methodenaufrufe auf die ‚TargetClass‘.

Zusammenfassung

Das einfache Beispiel zeigt die Möglichkeit einer kontinuierlichen Architekturvaliderung durch den Einsatz von jQAssistant. Durch die mächtige Abfragesprache Cypher lassen sich für jedes Projekt gezielte Qualitätsaspekte der zu betrachtenden Software beleuchten. Auch Architekturverletzungen können damit aufgedeckt werden. Die Erweiterbarkeit durch weitere Plugins macht das Tool jQAssistant sehr flexibel einsetzbar. Eine für unser Beispiel mögliche Ergänzung wäre beispielsweise die Integration von PlantUML zur Definition von Architekturteilen. In Verbindung mit der Regeldefinition in AsciiDoc erhält man durch jQAssistant nützliche Werkzeuge zur Qualitätssicherung.

Der Code ist in abgewandelter Form auf Github zu finden: https://github.com/jvmtomte/jqassistant-demo

Timo Hillerns

Timo ist seit 12 Jahren als Entwickler und Architekt in Projekten unterschiedlicher Branchen tätig. Als agiler Teamplayer hilft er Kundenwünsche in Binärform zu bringen.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.