Code Quality Test
Code Quality Test (or CQT) is an in-process static state analyzer. It uses a combination of application state and bytecode analysis to find potential problems, such as concurrency issues or mutable singleton objects.
It differs from the usual FindBugs/SpotBugs and SonarQube in that CQT is executed in the running application, thus utilizing the knowledge of instantiated objects and their relationship.
It has minimum dependencies (only common-text) and is framework-agnostic, even though it has built-in support for Spring-, Vaadin- or Servlet-based applications.
Note: In order to limit number of dependencies, internally jdk.internal.asm is used for ASM. While this works in Java 8 - Java 14, there might be some edgecases when it'll fail.
Usage
- Add the following lines to your
pom.xml:
<dependency>
<groupId>com.github.fluorumlabs</groupId>
<artifactId>code-quality-test</artifactId>
<version>1.0.0</version>
</dependency>
- And the following to your
main()method (or any other method that is invoked during application startup):
import com.github.fluorumlabs.cqt.CodeQualityTestServer;
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
public static void main(String[] args) {
new CodeQualityTestServer()
.includePackages("my.application.package")
.start();
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
}
}
-
Start your application and navigate to
http://localhost:8777once CQT server starts. -
Perform some actions in your application.
-
Hit
Rescanbutton in CQT UI and see if there are any new reports.
Configuration
CQT can be configured before server is started as follows:
new CodeQualityTestServer()
/* Which port CQT server will listen to. Defaults to 8777 */
.listenOn(8777)
/* Where to create .cqtignore file for suppressed reports. Defaults to current directory */
.cqtIgnoreRoot(Paths.get("target"))
/* Control fields of which classes would be reported */
.include(Servlet.class::isAssignableFrom, ...)
.includePackages("my.application.example", ...)
.exclude(clazz -> clazz.isAnnotationPresent(Entity.class), ...)
.excludePackages("my.application.example.dto", ...)
/* Specify scope for specific classes */
.withScopeHint(MyService.class, "singleton")
/* Run only selected inspection suites. If no specified, all built-in CQT suites are used */
.withSuites(ResourceInspections::new, ...)
/* Use specific object as an entry point for scanner. Class loaders are used if not specified */
.scanTargets(SomeClass::new, ...)
.start();
.cqtignore
It is possible to suppress specific reports using CQT UI. The suppressed reports will be written to a .cqtignore file, which can be committed to your project repository if needed.
Inspections
Collection inspections
-
Advice: Store
ConcurrentHashMapin fields of typeConcurrentHashMapConcurrentHashMap(unlike the genericConcurrentMapinterface) guarantees that the lambdas passed intocompute()-like methods are performed atomically per key, and the thread safety of the class may depend on that guarantee. If used in conjunction with a static analysis rule that prohibits callingcompute()-like methods onConcurrentMap-typed objects that are notConcurrentHashMapit could prevent some bugs: e. g. callingcompute()on aConcurrentSkipListMapmight be a race condition and it’s easy to overlook that for somebody who is used to rely on the strong semantics ofcompute()inConcurrentHashMap.See: https://github.com/code-review-checklists/java-concurrency#chm-type
-
Advice: Store
ConcurrentSkipListMapin fields of typeConcurrentMaporConcurrentSkipListMapBy explicitly specifying type of the fields, it would be easier to spot problematic patterns like following:
ConcurrentMap<String, Entity> entities = getEntities(); if (!entities.containsKey(key)) { entities.put(key, entity); } else { ... }
It should be pretty obvious that there might be a race condition because an entity may be put into the map by a concurrent thread between the calls to
containsKey()andput()(see https://github.com/code-review-checklists/java-concurrency#chm-race about this type of race conditions). While if the type of the entities variable was justMap<String, Entity>it would be less obvious and readers might think this is only slightly suboptimal code and pass by.See: https://github.com/code-review-checklists/java-concurrency#concurrent-map-type
-
Advice: Use non-blocking collection instead of blocking
- Use non-blocking
ConcurrentHashMapinstead ofHashtable - Use non-blocking
ConcurrentHashMapinstead ofCollections.synchronizedMap(HashMap) - Use non-blocking
ConcurrentHashMap.newKeySet()instead ofCollections.synchronizedSet(HashSet) - Use non-blocking
ConcurrentLinkedDequeinstead ofLinkedBlockingDeque - Use non-blocking
ConcurrentLinkedQueueinstead ofLinkedBlockingQueue - Use non-blocking
CopyOnWriteArrayListinstead ofCollections.synchronizedList(ArrayList) - Use non-blocking
CopyOnWriteArrayListinstead ofVector - Use non-blocking
ConcurrentSkipListMapinstead ofCollections.synchronizedMap(TreeMap) - Use non-blocking
ConcurrentSkipListSetinstead ofCollections.synchronizedMap(TreeSet)
Note:
ConcurrentSkipListMapis not the state of the art concurrent sorted dictionary implementation. - Use non-blocking
-
Advice: Use
EnumMapandEnumSetinstead ofMap<Enum, ...>andSet<Enum> -
Suggestion: Consider using
ClassValueinstead ofMap<Class, ...>Note, however, that unlike
ConcurrentHashMapwith itscomputeIfAbsent()methodClassValuedoesn’t guarantee that per-class value is computed only once, i. e.ClassValue.computeValue()might be executed by multiple concurrent threads. So if the computation insidecomputeValue()is not thread-safe, it should be synchronized separately. On the other hand,ClassValuedoes guarantee that the same value is always returned fromClassValue.get()(unlessremove()is called).See: https://github.com/code-review-checklists/java-concurrency#use-class-value
-
Warning: Mutable collection exposed via non-private field or non-private getter
Exposing mutable collection allows the state of the shared object to be mutated externally.
-
Probable Error: Non-thread-safe mutable collection is used
Using non-thread-safe mutable collections in a shared environment can cause hard to track race conditions and spontaneous
ConcurrentModificationExceptionexceptions
Field inspections
-
Warning: Non-final field can be modified: stateful shared object
Non-final fields make service stateful and allow the state to be mutated externally.
Care must be taken to properly initialize fields of static, singleton- and session-scoped objects. Consider using double-checked locking following SafeLocalDCL (http://hg.openjdk.java.net/code-tools/jcstress/file/9270b927e00f/tests-custom/src/main/java/org/openjdk/jcstress/tests/singletons/SafeLocalDCL.java#l71), or UnsageLocalDCL (http://hg.openjdk.java.net/code-tools/jcstress/file/9270b927e00f/tests-custom/src/main/java/org/openjdk/jcstress/tests/singletons/UnsafeLocalDCL.java#l71).
See: https://github.com/code-review-checklists/java-concurrency#safe-local-dcl
-
Warning: Actual value of non-transient field in serializable object is not
SerializableAll non-static fields of object implementing
Serializablemust be either serializable or transient. Failure to do so will lead toNotSerializableExceptionwhen an attempt to serialize will be made. -
Probable Error: Not thread-safe class
Non-thread-safe classes must not be used in a shared environment.
-
Probable Error: Transient field is not initialized after deserialization
Transient field is not initialized after deserialization. When
Serializableobject is deserialized, it's constructor is not called and all transient fields have no value. If transient field is final or is initialized only from constructor, this will leave object in incomplete state.
Resource management inspections
-
Probable Error:
AutoClosableresource stored in a fieldAutoCloseableresource stored in a field makes it impossible to use try-with-resources pattern and might result in resource leak. -
Warning:
ClassLoaderstored in a static fieldIn a dynamic system like an application server or OSGI, you should take good care not to prevent
ClassLoaderfrom garbage collection. As you undeploy and redeploy individual applications in an application server you create new class loaders for them. The old ones are unused and should be collected. Java isn't going to let that happen if there is a single dangling reference from container code into your application code. -
Warning:
Classstored in static fieldIn a dynamic system like an application server or OSGI, you should take good care not to prevent
ClassLoaderfrom garbage collection. As you undeploy and redeploy individual applications in an application server you create new class loaders for them. The old ones are unused and should be collected. Java isn't going to let that happen if there is a single dangling reference from container code into your application code.Note that
Classstores reference toClassLoaderinternally. -
Probable Error:
ExecutorServiceresource stored in a fieldExecutorServiceis a resource and must be closed explicitly viatry-with-resourcesortry-finallystatement. Failure to shutdown anExecutorServicemight lead to a thread leak even if anExecutorServiceobject is no longer accessible, because some implementations (such asThreadPoolExecutor) shutdown themselves in a finalizer, whilefinalize()is not guaranteed to ever be called by the JVM. To make explicit shutdown possible, first,ExecutorServiceobjects must not be assinged into variables and fields ofExecutortype.See: https://github.com/code-review-checklists/java-concurrency#explicit-shutdown
-
Probable Error:
ThreadLocalvalue is not removed after work is doneIf one of the application classes stores a value in
ThreadLocalvariable and doesn’t remove it after the task at hand is completed, a copy of that Object will remain with theThread(from the application server thread pool). Since lifespan of the pooledThreadsurpasses that of the application, it will prevent the object and thus aClassLoaderbeing responsible for loading the application from being garbage collected. And we have created a leak, which has a chance to surface in a good oldjava.lang.OutOfMemoryError: PermGen space form.See: https://plumbr.io/blog/locked-threads/how-to-shoot-yourself-in-foot-with-threadlocals
-
Warning:
Threadmust not be managed by short-lived objectsIt possible to reuse executors by creating them one level up the stack and passing shared executors to constructors of the short-lived objects, or a shared
ExecutorServicestored in a static field.See: https://github.com/code-review-checklists/java-concurrency#reuse-threads
-
Probable Error: Spawning thread from static initializers
Timerspawns a new thread in its constructor. The newThreadwill inherit some properties from its parent: context classloader, inheritable ThreadLocals, and some security properties (access rights). It is therefore rarely desireable to have those property set in an uncontrolled way. This may for instance prevent GC of a class loader. The static initializer is executed by the thread that first loads the class (in any givenClassLoader), which may be a totally random thread from a thread pool of a webserver for example. If you want to control these thread properties you will have to start threads in a static method, and take control of who is calling that method. -
Advice: Use a
ForkJoinPoolinstead of aThreadPoolExecutorwith N threadsForkJoinPoolis more scalable because internally it maintains one queue per each worker thread, whereasThreadPoolExecutorhas a single, blocking task queue shared among all threads.ForkJoinPoolimplementsExecutorServiceas well asThreadPoolExecutor, so could often be a drop in replacement.See: https://github.com/code-review-checklists/java-concurrency#fjp-instead-tpe
Lambda inspections
-
Warning: Narrow-scoped object captured in effectively session-, singleton- or static- lambda
Capturing short-lived objects in a closure of session-scoped lambdas will extend their lifetime and, potentially may result in memory leaks.