Enforce software design with Checkstyle and QDox

Developers care about the code they write. They build tools that enforce spaces instead of tabs, forbid 1-letter identifiers and ensure that every class and method has Javadoc comments. One example of such a tool is Checkstyle.

But usually, it’s not code style violations that makes code hard to read and maintain. More often, it is higher level code organization (software design) – all the decisions made about classes, their responsibilities, connections between them, etc.

Tools like Checkstyle surprisingly won’t help you to avoid making your view layer go directly to the database. In this post I want to show how to implement this kind of design constraint by building a custom Checkstyle check.

Problem definition

Let’s consider a naive example – a Spring web application where you have controllers, services and repositories. Let’s pretend that, as the architect, you believe that controllers should only talk to services, and services should talk to repositories. You consider controllers talking directly to repositories a bad design and want to entirely forbid this:

HelloController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package me.loki2302.spring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
@Autowired
private PersonRepository personRepository;

@GetMapping
public String hello() {
return String.format(
"There are %d persons in the repository",
personRepository.count());
}
}

When you run the build, you want it to fail with a clear message saying that having a reference to PersonRepository in HelloController is a bad idea.

Solution

Checkstyle’s code analysis capabilities are quite low-level. When you build your own check, Checkstyle gives you an AST and it’s up to you to understand what it describes. While AST is one of the central concepts in compiler design, it only describes the words code consists of, not the code’s semantics.

We are interested in semantics. We don’t want to operate on AST level and instead of looking at the code, we want to look at what this code describes. QDox is a great library that reads Java code and builds a model of all packages, classes, methods and links between them. It doesn’t analyze method bodies, though (if it’s a showstopper, take a look at Spoon – a more powerful alternative to QDox).

To solve the problem, we’ll make Checkstyle and QDox work together:

  • We’ll create a SpringAppDesignCheck – a custom Checkstyle check that will connect our code analysis with Checkstyle. Its only goal is going to be CodeModel initialization and querying.
  • We’ll create a CodeModel – a service that uses QDox models to understand the code structure. Its only goal is going to be to provide a collection of DesignViolation objects for every file we validate.
SpringAppDesignCheck – a custom Checkstyle check

Let’s first take a look at SpringAppDesignCheck and its 2 methods: beginProcessing() and processFiltered().

The beginProcessing() method gets called once per build. This method is a good place to perform initialization. In our case, we’ll construct the JavaProjectBuilder and the CodeModel:

SpringAppDesignCheck.java
1
2
3
4
5
6
7
public class SpringAppDesignCheck extends AbstractFileSetCheck {
@Override
public void beginProcessing(String charset) {
JavaProjectBuilder jpb = new JavaProjectBuilder();
jpb.addSourceTree(sourceRoot);
codeModel = new CodeModel(jpb);
}

What we do is, we just load the entire codebase from the very beginning.

The second method, processFiltered(), gets called once per source file and is supposed to emit errors (if any) for this specific file. In our case, we ask codeModel object to give us all the error descriptors and then just format and log them (logging is how Checkstyle expects you to report the errors):

SpringAppDesignCheck.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  @Override
protected void processFiltered(File file, List<String> lines) {
List<DesignViolation> designViolations =
codeModel.getDesignViolations(file);
for(DesignViolation designViolation : designViolations) {
String message = String.format(
"%s is a controller and references %s via field %s",
designViolation.controllerClassName,
designViolation.repositoryClassName,
designViolation.controllerFieldName);
log(designViolation.lineNumber, message);
}
}
}
CodeModel – a code analyzer built around QDox

Now, let’s take a look at CodeModel. Its only responsibility is to provide a collection of DesignViolation objects for a given source code file. Every DesignViolation is a descriptor of which controller class has a reference to which repository class in what field.

CodeModel.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class CodeModel {
private final JavaProjectBuilder javaProjectBuilder;

public CodeModel(JavaProjectBuilder javaProjectBuilder) {
this.javaProjectBuilder = javaProjectBuilder;
}

public List<DesignViolation> getDesignViolations(File file) {
List<JavaClass> classes = getControllerClasses(file);

List<DesignViolation> designViolations = new ArrayList<>();
for(JavaClass controllerClass : classes) {
List<JavaField> fieldsOfTypeJpaRepository =
getJpaRepositoryFields(controllerClass);

List<DesignViolation> thisControllerViolations =
fieldsOfTypeJpaRepository.stream()
.map(field -> new DesignViolation(
field.getLineNumber() + 1,
controllerClass.getName(),
field.getName(),
field.getType().getName()))
.collect(Collectors.toList());
designViolations.addAll(thisControllerViolations);
}

return designViolations;
}

The algorithm is quite straightforward:

  • For every controller class
  • Get all fields of type JpaRepository
  • And for every such field record the error

How do we find all controller classes? We just look for all classes annotated with @RestController:

CodeModel.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private List<JavaClass> getControllerClasses(File file) {
return javaProjectBuilder.getClasses().stream()
.filter(c -> file.equals(getJavaClassSource(c)))
.filter(CodeModel::isRestController)
.collect(Collectors.toList());
}

private static boolean isRestController(JavaClass javaClass) {
final String REST_CONTROLLER_CLASS_NAME =
"org.springframework.web.bind.annotation.RestController";
return javaClass.getAnnotations().stream()
.map(a -> a.getType())
.anyMatch(c -> c.isA(REST_CONTROLLER_CLASS_NAME));
}

How do we find all class fields of type JpaRepository? We just look for all fields of type JpaRepository:

CodeModel.java
1
2
3
4
5
6
7
private List<JavaField> getJpaRepositoryFields(JavaClass javaClass) {
final String JPA_REPOSITORY_CLASS_NAME =
"org.springframework.data.jpa.repository.JpaRepository";
return javaClass.getFields().stream()
.filter(f -> f.getType().isA(JPA_REPOSITORY_CLASS_NAME))
.collect(Collectors.toList());
}

Thanks to QDox’s intuitive API, the most hardest part of our solution, code analysis, looks very straightforward.

Demo

Now, if we enable our check in checkstyle.xml:

checkstyle.xml
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.2//EN"
"http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
<module name="Checker">
<module name="me.loki2302.SpringAppDesignCheck">
<property name="fileExtensions" value="java" />
<property name="sourceRoot" value="src/main/java" />
<property name="severity" value="error" />
</module>
</module>

The build will fail with this error:

1
2
3
4
[ant:checkstyle] [ERROR] /home/loki2302/checkstyle-experiment/
library/src/main/java/me/loki2302/spring/HelloController.java:10:
HelloController is a controller and references me.loki2302.spring.
PersonRepository via field personRepository [SpringAppDesign]

If you jump to the top of the page, you’ll see that HelloController.java line 10 is exactly where the violation is: private PersonRepository personRepository;

Conclusion

Software exists in time, so maintainability is one of the defining factors of software quality. Clear and logical design is a key to good maintainability. While it’s a responsibility of every developer to keep the system balanced as it evolves, some of the design validations are easy to automate and minimize the risk of human factor.

There’s a self-sufficient demo project in this GitHub repository – make sure to take a look.