Codementor Events

Analysis and verification of software architecture rules using jQAssistant

Published May 13, 2021Last updated Nov 08, 2021
Analysis and verification of software architecture rules using jQAssistant

In many software systems and software projects there is the challenge to develop the software uniformly and according to the decided architecture rules. Also new developers have to to keep the architecture rules or even work after old architecture rules, if the project is only maintained. Documentation of architecture rules is subject to software erosion just like code comments and usually leads to more confusion than it is a support. My favorite code comment is the following:

/**
 * Always returns true
 */
public boolean aMethod() {
  return false;
}

Therefore, I have been looking for ways to integrate architecture rules into the build process of projects. One of the most important rules is to respect the architecture layers, so that, for example, no class of the rest-controller layer interacts directly with the DB layer, but has to use the service layer. I took a closer look at the following approaches to solve this:

  • Java modules or various Java projects to separate layers.
  • AspectJ and code weaving for method interceptors
  • Own checkstyle extension
  • Own annotation processor
  • jQAssistant with Neo4j

With Java-Modules only architecture layers can be defined, and with AspectJ it is very complex to define rules. Checkstyle and annotation processors can parse the Java code (byte code, source code), but it is a relatively costly endeavor and can be difficult to parameterize.

Analysis with jQAssistant

Finally I came to the Maven plugin jQAssistant, which is the most suitable solution for my needs. jQAssistant is started within the Maven build process and analyzes among others the following project artifacts:

  • Java byte code with dependencies and meta information
  • Properties files, pom.xml
  • Via plugin even more information can be collected, such as tables from Excel files.

The information is stored in a local Neo4j graph database.

To show what is included in the database, we analyze the Apache Commons Lang project.

Prepare Apache Commons Lang for jQAssistant

First the project is loaded

git clone https://github.com/apache/commons-lang.git

After the project is imported, all that remains is to add a Maven plugin to the POM file.

<plugin>
 <groupId>com.buschmais.jqassistant</groupId>
 <artifactId>jqassistant-maven-plugin</artifactId>
 <executions>
   <execution>
     <goals>
       <goal>scan</goal>
       <goal>analyze</goal>
     </goals>
     <configuration>
       <warnOnSeverity>MINOR</warnOnSeverity>
       <failOnSeverity>MAJOR</failOnSeverity>
     </configuration>
   </execution>
 </executions>
</plugin>

After the next build, all information is included in the graph database. The Maven plugin can also be used to start with jqassistant:server a web server for Neo4j, which is then available at http://localhost:7474/.

Architecture overview with jQAssistant

Under classes, for example, you can now search for all "*Utils" classes.

screenshot.png

The query MATCH (n:Class) WHERE n.name ENDS WITH 'Utils' RETURN n is the special DB query language Cypher, which is the counterpart of SQL for graph databases. The query searches for nodes of type "Class" whose property "name" ends with "Utils". The round brackets are meant to symbolize the node.

This is another advantage of jQAssistant. It is possible to analyze a code base exploratively and then prove or disprove the architectural assumptions by a Cypher query.

Simple architecture rules

For an unknown project, I am first interested in the complex, mostly long and unstructured methods. This can be described approximately with the cyclomatic index.

MATCH 
  p=(c:Class)-[r:DECLARES]->(m:Method) 
WHERE 
  m.cyclomaticComplexity > 50 
RETURN p

Here we search for node connections that can be described in ASCII format with (...)-[...]->(...), i.e. With the syntax node-edge-node.

screenshot.png

Apparently, the "NumberUtils" class contains two relatively complex methods boolean isCreatable(java.lang.String) and java.lang.Number createNumber(java.lang.String). Also the "effectiveLineCount" is calculated and both methods are greater than 60 lines.

In my further analysis, I noticed that toString methods call each other relatively frequently, one after the other, which can lead to performance problems. Here is an extended query to find these cases.

MATCH 
  p=(m1)-[:INVOKES]->(m2)-[:INVOKES]->(m3) 
WHERE 
  m1.name = 'toString' AND m3.name = 'toString' 
  AND m1.cyclomaticComplexity>1 AND m2.cyclomaticComplexity>1 
  AND m3.cyclomaticComplexity>1 
RETURN p LIMIT 200

Interestingly, this found a recursive call that you would not otherwise immediately see.

screenshot.png

java.lang.String classToString(java.lang.Class) here calls java.lang.String toString(java.lang.reflect.Type), which can recursively call other classes with classToString again. A StringBuffer or StringBuilder variant could increase the performance for these methods.

Cyclic dependencies

To find cyclic package structures, the Cypher query is much longer, but still relatively easy to read.

MATCH
  (a:Artifact),
  (a)-[:CONTAINS]->(p1:Package),
  (a)-[:CONTAINS]->(p2:Package),
  (p1)-[:DEPENDS_ON]->(p2),
  path=shortestPath((p2)-[:DEPENDS_ON*]->(p1))
WHERE 
  p1.fqn <> 'org.apache.commons.lang3' 
  AND p2.fqn <> 'org.apache.commons.lang3'
RETURN
  a.fileName, p1.fqn AS package, 
  extract(p in nodes(path) | p.fqn) AS Cycle
ORDER BY
  package

screenshot.png

This query now does not return a graph as a result, but a table.

To define architecture rules, these can now be described with Cypher.

Ideas for more architecture rules

For example, the following rules have been used in my projects:

  • For each <X>Entity class in a given package, there is a corresponding <X> class and an <X>Mapping class.
  • Classes from the "controller" package may not use the classes from the "entity" package directly.
  • Utility classes may not have a public constructor and member variables.
  • Methods in enums and DTO classes must have a low cyclomatic index.
  • We distinguish between services that "compute a lot" and services that "compute nothing and just delegate further". ComputationServices must not use any other services. DelegatingServices must have a low cyclomatic index in all methods.
  • The DelegatingPasswordEncoder must be used in SpringBoot.
  • The number of parameters in methods must not become too large.
  • JPA repository interfaces shall not contain default methods.
  • Reflection must only be used in the utility package.
  • Controller methods must start with the Http verb.

Integration in JUnit

To get build constraints from Cypher queries, an XML file is stored in the project under "jqassistant" for each constraint.

<jqa:jqassistant-rules xmlns:jqa="http://www.buschmais.com/jqassistant/core/analysis/rules/schema/v1.0">

    <constraint id="my-rules:TestClassName">
        <requiresConcept refId="junit4:TestClass" />
        <description>All JUnit test classes must have a name with suffix "Test".</description>
        <cypher><![CDATA[
            MATCH
                (t:Junit4:Test:Class)
            WHERE NOT
                t.name =~ ".*Test"
            RETURN
                t AS InvalidTestClass
        ]]></cypher>
    </constraint>

    <group id="default">
        <includeConstraint refId="my-rules:TestClassName" />
    </group>
</jqa:jqassistant-rules>

With jQAssistant, constraints can also be parameterized well. This helps to easily define project specific customizations. Another advantage is that with Cypher nodes can be annotated with new node types. This allows queries to also refer to the new node type and thus facilitates readability. The following example shows both mechanisms.

<jqa:jqassistant-rules xmlns:jqa="http://www.buschmais.com/jqassistant/core/rule/schema/v1.3">

    <concept id="my-rules:ApplicationRootPackage">
        <requiresParameter name="rootPackage" type="String" defaultValue="com.buschmais"/> 
        <description>Labels the root package of the application with "Root".</description>
        <cypher><![CDATA[
           MATCH
             (root:Package)
           WHERE
             root.name = {rootPackage} 
           SET
             root:Root
           RETURN
             root
        ]]></cypher>
    </concept>

</jqa:jqassistant-rules>

Conclusion

jQAssistant offers a good way to flexibly define architecture rules. Many plugins are available. The connection of new project artifacts for the graph database is also possible with little effort. Only the syntax and semantics of Cypher must be mastered even for simple queries.

However, jQAssistant can also be useful for people interested in Neo4j - after all, you can very quickly build a complex domain-driven graph with which you can try out your query skills.

Discover and read more posts from Stefan Fenn
get started