A library for unit-tests to check the dependencies between classes.

Table 1. Resources on GitHub
Repository Description

dessert-core

Library source code

dessert-site

Documentation source code

dessert-tests

Tests and samples

Table 2. Documentation
Release Reference Doc. API Doc.

0.5.0-SNAPSHOT

reference

javadoc

0.4.2

reference

javadoc

1. Getting Started

1.1. Maven Dependency

Add the dessert-core dependency to your project:

<dependency>
    <groupId>de.spricom.dessert</groupId>
    <artifactId>dessert-core</artifactId>
    <version>0.4.2</version>
    <scope>test</scope>
</dependency>

1.2. Snapshot Dependency (optional alternative)

If you rather want o try out the most current snapshot, then use:

<dependency>
    <groupId>de.spricom.dessert</groupId>
    <artifactId>dessert-core</artifactId>
    <version>0.5.0-SNAPSHOT</version>
    <scope>test</scope>
</dependency>

You have to specify the repository to use snapshot releases:

<repositories>
    <repository>
        <id>ossrh</id>
        <name>OSSRH Snapshot Repository</name>
        <url>https://oss.sonatype.org/content/repositories/snapshots</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>

1.3. First Test

Implement your first dependency test:

@Test
void willFail() {
    Classpath cp = new Classpath();
    Clazz me = cp.asClazz(this.getClass());
    Root root = cp.rootOf(Test.class);
    SliceAssertions.dessert(me).usesNot(root);
}

Each dessert test starts with the Classpath. Dessert is all about slicing down the Classpath and checking the dependencies between the slices. The Classpath is the whole cake. A Clazz represents a single .class file, the smalles possible Slice. A Root represents a classes directory, a .jar file or a module. All of them are slices, hence they implement the Slice interface. SliceAssertion provides a static assertThat method to check for unwanted dependencies between slices.

The test above will fail, because it has a dependency to the junit-jupiter-api.jar. Thus, it produces the following output:

java.lang.AssertionError: Illegal Dependencies:
de.spricom.dessert.sample.DessertSampleTest
 -> org.junit.jupiter.api.Test

The following test shows some other methods to get slices from the Classpath:

@Test
void willSucceed() {
    Classpath cp = new Classpath();
    Slice myPackage = cp.packageOf(this.getClass());
    Slice java = cp.slice("java..*");
    Slice libs = cp.packageOf(Test.class).plus(cp.slice("..dessert.assertions|slicing.*"));
    dessert(myPackage).usesOnly(java, libs);
}

The java..* slice represents all classes in the java package or any nested package. The methods plus and minus can be used do create new slices from existing slices. The dessert method is an alias for assertThat to prevent name collisions with other libraries. It stands for dependency assert that. All the assertion methods of SliceAssertions accept more than one slice, like usesOnly. They treat the slices as an union of all the slices passed.

2. Tutorial

This tutorial not only guides you step by step to the dessert features but it also is a guideline for applying dessert. Follow these steps, and you’ll get the most benefit for your project. The best is to apply it to some Java project you are currently working on (any other JVM language, like Kotlin, Scala, Groovy, etc. will do, too). Before you start the tutorial make sure the dessert-core dependency has been added to your project, and you are able to run dessert tests as described in the Getting Started started section.

DessertTutorialTest.java provides some simple solutions for this tutorial. If the solutions are not applicable for your particular problem, then have a look at the tutorial package of the dessert-tests-jdeps project. You can find there some more advanced examples and solutions for typical problems. If you still have problems, then post your question on discussions.

2.1. Detect usage of internal APIs

Internal APIs are subject to change without notice. Using internal APIs may cause trouble when you update a dependency. The signature of the internal API may have changed, it may have disappeared entirely, or it may behave differently in some corner cases. Thus, your code should not rely on any internal API. To ensure this, write a test that detects the usage of JDK’s internal API (packages com.sun.. or sun..) or any other internal API from some external library (any ..internal..* package).

2.2. Detect duplicates

Each JAR has its own directory structure, thus a class with the same fully qualified name may appear in more than one JAR. The ClassLoader always uses the first matching class on the classpath, but the order of the JARs on the classpath may vary on different systems. If there are different implementations for one of the duplicates that is actually used, some systems may fail. Such errors are hard to track down. Thus, write a test that makes sure, there are no duplicates on the classpath. See the Duplicates section on how to do this. If you have duplicates write some code that helps you to track down the problem (i.e list the classes and jars involved).

Many JARs contain a module-info class in their root package. Make sure to ignore this class when checking for duplicates.

Often you cannot prevent all duplicates, but at least you should have a test that informs you if there are additional duplicates.

2.3. Detect cycles

The problem with a cycle is, it does not have a beginning nor does it have an end. Thus, if you pick out any class involved in a dependency cycle you cannot use it without all other classes involved in that cycle. This is not a problem for small cycles of closely related classes, but it’s a nightmare if you have to change a software with big intertwined cycles.

Dessert can detected cycles between any set of slices (remember: a Clazz is a Slice, too). To start with, make sure, your software does not have any package-cycles. See the Cycle detection section on how to do this.

2.4. Investigate your project

If you have any cycles in your software, you might want to find out, which classes cause that cycle. Or you may have other questions on your software, for which the search facilities of your IDE are not sufficient. The slice method in combination with Predicates lets you filter your classes by almost any condition.

If you have a package cycle, write some code that tells you exactly which classes from two packages involved in the cycle cause that cycle. Alternatively find out, which classes of your project use java.io.

2.5. Simulate refactorings

Often a package cycle can simply be resolved by moving a class from on package to another, but actually moving a class may require many changes and introduce new cycles. Thus, it would be very useful if one could find out the effects of moving a classes without actually doing it. With dessert you can use the Slice methods minus and plus to simulate the removal of a Clazz from one slice and the addition to another.

Simulate the introduction of a new package-cycle by creating new slices from existing package slices with one or more classes moved from one package to another. After you have created your simulated cycle, make sure dessert detects it.

2.6. Check your layers

Each nontrivial software product is composed of layers. In a classical software product you have persistence, business logic a presentation layers. Modern designs are base on the hexagonal architecture, also called ports and adapters architecture. But these are still layered architectures with the business logic at the bottom, now.

The architecture may be strict where one layer can only access the layer below. This is typically the case in network protocol implementations similar to the OSI model. In most applications the architecture is relaxed, hence one layer can access all layers below.

A common property of the classes within one layer are there external dependencies. Thus, classes in the presentation layer should not have dependencies to persistence libraries like hibernate and classes in the persistence layer should not have dependencies to presentation libraries like JavaFX or Vaadin.

Now it’s time to define the layers of your project. Define a slice for each layer. You may want to use the Slice method named to assign it a name. Then use Architecture verification to ensure your code complies with your architecture. Additionally, make sure none of your layers has any external dependencies it should not.

2.7. Modularize your project

Layers are the first coarse subdivision of your project. Typically, your software is made up form smaller parts by cutting down the layers to vertical slices. Each vertical slice is tied to one domain and has certain dependencies to other parts of the software or to external libraries. It’s good practice to explicitly name the dependencies of each such part.

Start from the layers defined in the previous exercise and use the slice method to cut them down into vertical slices. Then make sure, each such slice uses only the dependencies it is allowed to, by using the usesOnly assertion. You might want to group your external dependencies into corresponding slices, to do this.

The slices defined in this step are the building blocks of your project. We might call some modules, but that term is already occupied in the java world, thus I’ll stay with building block. Often you don’t want to expose anything from one building block to others. You can accomplish this by defining one or more interface slices for each build block and make sure the depending building blocks only use the corresponding interface slice.

2.8. Define your custom classpath

By default, the Classpath is based on the path defined by the java.class.path system property. This fits most cases, but there might be circumstances where this is not suitable. See Customize class resolving on how you can define your own custom classpath. Then define a custom Classpath that contains all elements of the java.class.path system property in reverse order. Check if your tests produce the same results.

3. User Documentation

3.1. Background

The goal of dependency checking is finding unwanted dependencies. Dessert does this be analyzing .class files. The java compiler generates a .class file for each class, interface, annotation, (anonymous) inner-class or -interface or enum class.

A java source file can define more than one class.

In dessert a .class file is represented by Clazz. The Clazz is the smallest unit of granularity dessert can work with. The biggest unit is the Classpath. The Classpath contains all classes within your application.

To specify dependency assertions you have to tear the Classpath down into smaller pieces. Imagine the Classpath is a big cake you have to slice down. Thus the most import concept of dessert is the Slice. The smallest Slice is a Clazz and the biggest Slice is the Classpath.

The name dessert comes from dependency assert.

3.2. Design Goals and Features

If you’re considering to use dessert you probably have problems with dependencies. Hence the most important design goal was to not introduce any additional dependency that might cause you a headache.

  • No other dependencies but pure java

  • Support a wide range of java versions an execution environments

  • Easy and seamless integration with other testing or assertion frameworks

  • Simple and intuitive API (motivated by AssertJ)

  • Assertions should be robust against refactorings (no strings for class- or package names required)

  • Compatibility to the jdeps utility.

  • Focus on dependency assertions and nothing else

  • Support for projects of any scale

  • Speed

The design goals lead to these features:

  • Supports any JDK from Java 6 to Java 15

  • Has only dependencies to classes within the java.base module

  • Annalyzes more than 10000 classes per second on a typical developer machine [1]

  • Detects any dependency jdeps detects. [2] (This is not true the other way round, see the FAQ why this is so.)

  • Performs the dependency analysis as late as possible to prevent any unnecessary analysis. Thus its safe to use on big projects with lots of dependencies.

3.3. The Slice

When using dessert you work most of the time with some Slice implementation. The following diagram shows all such implementations provided by dessert:

slice overview

The most important Slice methods are plus, minus, and slice to create new slices from existing ones. The AbstractRootSlice provides some convenience methods to work with packages. packageOf returns a slice of all classes within one package (without sub-packages). packageTreeOf' returns a slice of all classes within a package, or a nested sub-package. Called on a `Root these methods return only classes within that root. A Root is a classes directory or a .jar file that belongs to the Classpath. To get a Root you can use the Classpath method rootOf.

The Classpath methods asClazz and sliceOf can create slices for classes that are not on the Classpath such as classes located in the java or javax packages or dependencies of some classes. In such a case the Classpath uses the current ClassLoader to get the corresponding .class file. If this does not work either, it creates a placeholder Clazz that contains only the classname.

The slice methods of Classpath do not necessarily return a slice that can be resolved to a concrete set of classes. A slice may also be defined by a name-pattern or some predicate. For such a slice one can use the contains method to check whether a Clazz belongs to it, thus the class fulfills all predicates. But calling getClazzes will throw a ResolveException. This happens if the Classpath contains none of the classes belonging to the slice. Internally dessert works with such predicate based slices as long as getClazzes has not been called, for performance reasons.

Dessert has been optimized to resolve name-pattern very fast. Hence, it’s a good practice to first slice be name (or packageOf/packageTreeOf) then used predicates to slice-down the slices further.

getDependencies returns the dependencies of a slice and uses checks whether some other slice contains one of these dependencies.

To add features to existing slices always extend AbstractDelegateSlice. The named method returns one such extension that makes a slices' toString method return the name passed.

3.4. Name-Patterns

Name-patterns are the most important means to define slices. A name-pattern identifies a set of classes by their full qualified classname.

The syntax has been motivated by the https://www.eclipse.org/aspectj/doc/released/progguide/quick-typePatterns.htmlAspectJ TypeNamePattern] with slight modifications:

  • The pattern can either be a plain type name, the wildcard *, or an identifier with embedded * or .. wildcards or the | separator.

  • An * matches any sequence of characters, but does not match the package separator ".".

  • An | separates alternatives that do not contain a package separator ".".

  • An .. matches any sequence of characters that starts and ends with the package separator ".".

  • The identifier to match with is always the name returned by {@link Class#getName()}. Thus, $ is the only inner-type separator supported.

  • The * does match $, too.

  • A leading .. additionally matches the root package.

Table 3. Examples:
Pattern Description

sample.Foo

Matches only sample.Foo

sample.Foo*

Matches all types in sample starting with "Foo" and all inner-types of Foo

sample.bar|baz.*

Matches all types in sample.bar and sample.baz

sample.Foo$*

Matches only inner-types of Foo

..Foo

Matches all Foo in any package (incl. root package)

...Foo

Matches all Foo nested in a sub-package

*

Matches all types in the root package

..*

Matches all types

3.5. Predicates

Predicates are another way to define slices. Typically, they are used to selecting classes from an existing Slice that meets certain properties. Predicate is a functional interface. Predicates provides and, or and not to combine predicates. ClazzPredicates contains some pre-defined predicates for classes.

The following code fragment demonstrates their usage:

Classpath cp = new Classpath();
Root dessert = cp.rootOf(Slice.class);
Slice assertions = dessert.packageOf(SliceAssertions.class);
Slice slicing = dessert.packageOf(Slice.class);
Slice slicingInterfaces = slicing.slice(
        Predicates.and(ClazzPredicates.PUBLIC,
                Predicates.or(
                        ClazzPredicates.INTERFACE,
                        ClazzPredicates.ANNOTATION,
                        ClazzPredicates.ENUM
                )
        )
);
SliceAssertions.dessert(assertions).usesNot(slicing.minus(slicingInterfaces));

slicingInterfaces contains all public interfaces, enums or annotations of the slicing package within the dessert-core library. The assertion checks that the assertions packages uses only these types by asserting that it uses nothing from the complement. That check will fail, because assertions uses i.e. Clazz.

3.6. Duplicates

The Classpath has a method duplicates that returns a special Slice of all .class files that appear at least twice on the Classpath. Other as the ClassLoader dessert does not stop at the first class that matches a certain name. It always considers all matches. Duplicates are a common cause of problems, because the implementation is chosen more ore less randomly.

The following code fragment demonstrates this:

Classpath cp = new Classpath();
ConcreteSlice duplicates = cp.duplicates();
duplicates.getClazzes().forEach(clazz -> System.out.println(clazz.getURI()));
Assertions.assertThat(duplicates.getClazzes()).isNotEmpty();

Slice slice = duplicates.minus(cp.asClazz("module-info").getAlternatives());
Assertions.assertThat(slice.getClazzes()).isEmpty();

Slice slice2 = duplicates.minus(cp.slice("module-info"));
Assertions.assertThat(slice2.getClazzes()).isEmpty();

Slice slice3 = duplicates.minus("module-info");
Assertions.assertThat(slice3.getClazzes()).isEmpty();

The sample uses JUnit 5 which has a module-info.class in each of its jars, thus duplicates is not empty. The Classpath method asClazz returns a single Clazz object which represents one if these module-info classes. A Clazz object always represents one single .class file. getAlternatives() returns all classes with the name on the Classpath. After subtracting the module-info classes there are no more duplicates left. An alternative way to get a slice of all module-info classes is slice("module-info") or simple by using the short-cut minus("module-info"), because it filters by name.

3.7. Assertions

All dessert assertions start with one of the static SliceAssertions methods assertThat or its alias dessert. These methods return a SliceAssert object with its most important methods usesNot and usesOnly. Both methods return a SliceAssert again, so that assertions can be queued:

Classpath cp = new Classpath();
dessert(cp.asClazz(this.getClass()))
        .usesNot(cp.slice("java.io|net..*"))
        .usesNot(cp.slice("org.junit.jupiter.api.Assertions"))
        .usesOnly(cp.slice("..junit.jupiter.api.*"),
                cp.slice("..dessert..*"),
                cp.slice("java.lang..*"));

When an assertion fails it throws an AssertionError. The message shows details about the cause of the failure. This message is produced by the DefaultIllegalDependenciesRenderer. That renderer can be replaced with the SliceAssert method renderWith.

To have any effect renderWith must be invoked before the assertions (i.e. usesNot).

3.8. Cycle detection

SliceAssert provides the method isCycleFree to check whether a set of slices has any cyclic dependencies. Because each Clazz is a Slice one check the classes of slice for a cycle like this:

Classpath cp = new Classpath();
dessert(cp.packageTreeOf(CycleDump.class).getClazzes()).isCycleFree();

The sample contains a cycle, hence it produces the following output:

java.lang.AssertionError: Cycle:
clazz de.spricom.dessert.cycle.foo.Foo,
clazz de.spricom.dessert.cycle.bar.Bar,
clazz de.spricom.dessert.cycle.CycleDump,
clazz de.spricom.dessert.cycle.foo.Foo

Class-cycles are quite common and should not be a problem as long as the cycle is within a package. Package-cycles on the other hand are an indicator for serious architecture problems. To detect these you can use:

Classpath cp = new Classpath();
Slice slice = cp.packageTreeOf(CycleDump.class);
dessert(slice.partitionByPackage()).isCycleFree();

This produces:

java.lang.AssertionError: Cycle:
slice partition de.spricom.dessert.cycle.bar,
slice partition de.spricom.dessert.cycle,
slice partition de.spricom.dessert.cycle.foo,
slice partition de.spricom.dessert.cycle.bar

The AssertionError message is produced by the DefaultCycleRenderer. Another CycleRenderer can be given with the SliceAssert method renderCycleWith.

The partitionByPackage is a specialized Slice method that partitions the classes of a Slice by package-name. Thus, it produces a Map for which the package-name is the key, and the value is a slice containing all classes that belong to the package. In this case the value is a specialized slice that gives access to the package-name and the parent-package.

The more general partitionBy uses a SlicePartitioner that maps each class to some key. The result is a map of PartitionSlice. A PartitionSlice is a ConcreteSlice (set of classes), with the key assigned. See SlicePartitioners for examples of pre-defined slice partitioners. There is another partitionBy method with a second PartitionSliceFactory parameter. This can be used to create specialized PartitionSlice objects like the PackageSlice.

3.9. Architecture verification

SliceAssert has two additional convenience methods to verify a layered architecture. Therefore, you have to pass a list of layers to the dessert method. isLayeredStrict check whether each layer depends only on classes within itself or classes within its immediate successor. isLayeredRelaxed relaxes this from an immediate successor to _any successor. The following example shows how to use this:

Classpath cp = new Classpath();
List<Slice> layers = Arrays.asList(
        cp.packageTreeOf(SliceAssertions.class).named("assertions"),
        cp.packageTreeOf(Slice.class).named("slicing"),
        cp.packageTreeOf(ClassResolver.class).named("resolve"),
        cp.packageTreeOf(ClassFile.class).named("classfile"),
        cp.slice("..dessert.matching|util..*").named("util")
);
dessert(layers).isLayeredRelaxed();

3.10. Customize class resolving

The Classpath needs a ClassResolver to find the classes it operates on. By default, the Classpath uses a resolver that operates on the path defined by the java.class.path system property. You can define your own ClassResolver and add the classes directories and jar files you want. You can even define your own ClassRoot with some custom strategy to find classes. Then pass that ClassResolver to the Classpath constructor. This will freeze the ClassResolver. Thus, after a ClassResolver is used by a Classpath it, it must not be changed.

4. Plans for dessert-core 0.5.x

  • Utilize information within module-info classes

  • Support multi-release jars

  • API to define modules for dessert-tests

  • Ready-to-use module definitions for the JDK that resemble the Java9+ modules, usable for older java versions

  • Architecture visualization API

  • Improved support for investigating existing projects

5. Getting Involved

If you’re missing some feature or find a bug then please open an issue on GibHub.

If you have questions the best way to get in contact is discussions.

If you just want to send (positive) feedback, then send an e-mail to dessert@spricom.de, but don’t expect to get an answer, because I’m doing all this in my spare time.

6. Frequently asked Questsions

6.1. When will be there a 1.0 version?

As along as I don’t have any feedback of someone who is using this library, there is no reason to keep the API backwards compatible. Within the 0.x.y versions the API is subject to change without notice. If you are using dessert and if you’re fine with the API then send an e-mail to dessert@spricom.de. As soon as there are enough e-mails I’ll release a 1.0.0 and try to keep the API backwards compatible from that moment on.

6.2. Why does dessert find more dependencies than jdeps?

Well, jdeps shows all runtime dependencies whereas dessert shows all compile-time dependencies. Hence, if you use a class for which a runtime dependency is missing you’ll get a NoClassDefFoundError. There are dependencies within generics or within annotations that were required during compilation but not while using the compiled class. For more information see JDK-8134625. If you want to split a project into modules, then all the compile-time dependencies are relevant. Thus, that’s what dessert operates on.

The compiler may have removed some source dependency that cannot be detected in the .class file anymore.

7. Release Notes

7.1. dessert-core-0.4.2

Bugfix-release:

  • The cycle detection algorithm ignores dependencies within the same slice, now.

7.2. dessert-core-0.4.1

Some minor changes:

  • Duplicate .class files in JAR files won’t cause an AssertionError.

  • A Clazz created by Classpath.asClazz(java.lang.Class<?>) immediately contains all alternatives on the Classpath.

  • ClassPackage internally uses TreeMap instead of List to lookup classes. This improves the performance if a package has many classes.

  • Many Javadoc additions.

7.3. dessert-core-0.4.0

Starting with this release dessert will be available on Maven Central. Therefore, the maven coordinates have been changed. The project has been renamed to dessert-core and everything that does not belong to the core functionality (i.e. DuplicateFinder) has been deleted.

The most prominent changes are:

  • New maven coordinates: de.spricom.dessert:dessert-core

  • Removal of DuplicateFinder and corresponding traversal API

  • Support for any Classfile-Format up to Java 15

  • Multi-Release JARs don’t cause an error (but version specific classes are ignored)

  • API much simpler and more intuitive: SliceEntry renamed to Clazz, SliceContext renamed to Classpath and both implement Slice

  • The Grouping-API has been replaced by simple maps and methods for partitioning

  • Performant pattern-matching for class-names

  • Many bugfixes, simplifications and preformance-improvements

7.4. Older Releases

Code and documentation copyright 2017–2021 Hans Jörg Heßmann. Code released under the Apache License 2.0.


1. See the dessert-tests project for a corresponding performanc test.
2. This has been verified for more that 40000 classes. See dessert-tests for details. Please open an issue if you encounter any class for which this is not true.