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 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.