A library for unit-tests to check 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.6-SNAPSHOT

reference

javadoc

0.5.5

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.5.5</version>
    <scope>test</scope>
</dependency>

1.2. Snapshot Dependency (optional alternative)

To try out the most current snapshot use:

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

Snapshot releases require the OSSRH Snapshot Repository>:

<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 junit = cp.rootOf(Test.class);
    SliceAssertions.assertThatSlice(me).doesNotUse(junit);
}

Each dessert test starts with the Classpath. Classpath, Clazz and Root all implement the Slice interface. Imagine the Classpath to be a cake that has to be sliced down to suitable pieces. The Clazz is the smallest possible piece, it represents a single .class file. A Root is a classes directory, a .jar file or a JDK module. SliceAssertion provides a static assertThatSlice 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.start.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.*"));
    assertThatSlice(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 most important methods for slice assertions are doesNotUse and usesOnly. Both accept more than one slice.

To find out more read the Practical Guide.

1.4. First Modules Test

With dessert projects can profit form the Java Platform Module System (JPMS), even if they don’t use modules. Therefore, dessert provides an easy and intuitive module API:

package de.spricom.dessert.intro;

import de.spricom.dessert.assertions.SliceAssertions;
import de.spricom.dessert.modules.ModuleRegistry;
import de.spricom.dessert.modules.core.ModuleSlice;
import de.spricom.dessert.modules.fixed.JavaModules;
import de.spricom.dessert.slicing.Classpath;
import de.spricom.dessert.slicing.Slice;
import org.junit.jupiter.api.Test;

import static de.spricom.dessert.assertions.SliceAssertions.assertThatSlice;

class ModulesSampleTest {
    private final Classpath cp = new Classpath();
    private final ModuleRegistry mr = new ModuleRegistry(cp);
    private final JavaModules java = new JavaModules(mr);
    private final ModuleSlice junit = mr.getModule("org.junit.jupiter.api");
    private final Slice dessert = cp.rootOf(SliceAssertions.class);

    @Test
    void testDessertDependencies() {
        assertThatSlice(dessert)
                .usesOnly(java.base, java.logging);
    }

    @Test
    void testMyDependencies() {
        assertThatSlice(cp.sliceOf(this.getClass()))
                .usesOnly(java.base, junit, dessert);
    }
}

Through the ModuleRegistry modules can be accessed by name. JavaModules provides constants for all java modules as of JDK 17. Both are available even for JDK 8 and earlier. The usesOnly assertion makes sure only exported packages are used from the modules listed.

2. Introduction

The name dessert comes from dependency assert. Hence, dessert is a library for unit-tests to check dependencies between classes. It can be used to clean up the architecture of a software produkt and to ensure it remains clean.

When you describe architecture requirements as unit-tests each violation will turn on a red traffic-light on your CI-pipeline. Thus, they will be fixed immediately, and they will get much more attention within your team. Over time this improves the quality of your architecture.

An architecture describes a complex system by breaking it apart into smaller pieces. Then it specifies the relations to or interactions with the other pieces of the system. In an implementation of the system the relations or interactions show up as dependencies. Hence, checking architecture requirements can be reduced to dependency checking.

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, record or enum and their inner variants.

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 available for an application, these are the classes of the application code, all classes of its dependencies and all JDK classes.

A dessert based test checks dependency assertions. Each dependency assertion requires three parts:

  1. The application code the assertion is about.

  2. The dependencies the assertion is about.

  3. The requirement that has to be fulfilled.

Both the application code and the dependencies are a slice of classes taken from the class-path. Therefore, the Classpath has different methods to get a Slice from it. Just imagine the Classpath to be big cake you have to slice down. Thus the most import concept of dessert is the Slice.

Architecture requirements are expressed with a fluent API starting with the static SliceAssertions method assertThatSlice. The application code to check is the parameter of this method. Next comes the requirement, which is one of the methods doesNotUse or usesOnly. The parameter(s) of these methods are the dependencies the assertion is about.

Hence, a complete dessert test looks like this:

package de.spricom.dessert.intro;

import de.spricom.dessert.slicing.Classpath;
import de.spricom.dessert.slicing.Root;
import de.spricom.dessert.slicing.Slice;
import org.junit.jupiter.api.Test;

import static de.spricom.dessert.assertions.SliceAssertions.assertThatSlice;

class SlicingSampleTest {
    private static final Classpath cp = new Classpath();

    @Test
    void checkDessertLibraryDependencies() {
        // application code:
        Root dessert = cp.rootOf(Slice.class);

        // dependencies:
        Slice allowedDependencies = cp.slice("java.lang|util|io|net..*");

        // requirement:
        assertThatSlice(dessert).usesOnly(allowedDependencies);
    }
}

3. Concepts and Terminology

3.1. Slices

When using dessert you work most of the time with some Slice implementation. Mostly this is one of:

Slice

Dessert is all about slices. A slice is an arbitrary set of .class files.

Clazz

The Clazz is the smallest possible slice. It represents a single .class file.

Classpath

The Classpath is the starting point of every dessert test and the biggest possible slice. Warning: Never try to combine slices from different Classpath instances!

Root

A Root is a special slice that represents a .jar file or a classes directory.

ModuleSlice

A ModuleSlice represents the public interface of a module. For a JPMS module these are all unqualified exports.

The following diagram gives an overview of the 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.2. Modules

Dessert’s JPMS support can be used through the ModuleRegistry. It’s getModule method returns the corresponding module by name. JavaModules and JdkModules provides pre-defined constants for the corresponding JDK 17 modules:

private final Classpath cp = new Classpath();
private final ModuleRegistry mr = new ModuleRegistry(cp);
private final JavaModules java = new JavaModules(mr);
private final JdkModules jdk = new JdkModules(mr);

The following sample is not very useful, but it shows how to use the module API:

ModuleSlice junit = mr.getModule("org.junit.jupiter.api");
assertThatSlice(junit.getImplementation()
        .minus(cp.slice("org.junit.jupiter.api.AssertionsKt*")))
        .usesOnly(
                java.base,
                ((JpmsModule)mr.getModule("org.junit.platform.commons"))
                        .getExportsTo("org.junit.jupiter.api"),
                mr.getModule("org.opentest4j"),
                mr.getModule("org.apiguardian.api")
        );

A ModuleSlice itself contains only the classes exported to anyone. The getImplementation method of a ModuleSlice returns all classes of a module, no matter whether they are exported. The JpmsModule method getExportsTo returns the classes that are exported to some certain module.

The module API is fully supported for JDK 8 and earlier. Therefore, dessert comes FixedModule definitions which are based on JDK 17. They are used as fall-back if the current JDK does not provide modules.

AssertionsKt has been excluded, because it adds dependencies to the Kotlin runtime system, which are not available for Java.

3.3. 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.4. 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.5. 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();
assertThatSlice(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.6. 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 detected:
de.spricom.dessert.concepts.cycle.bar.Bar -> de.spricom.dessert.concepts.cycle.CycleDump
de.spricom.dessert.concepts.cycle.CycleDump -> de.spricom.dessert.concepts.cycle.foo.Foo
de.spricom.dessert.concepts.cycle.foo.Foo -> de.spricom.dessert.concepts.cycle.bar.Bar

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 detected:
de.spricom.dessert.concepts.cycle.foo -> de.spricom.dessert.concepts.cycle.bar:
	Foo -> Bar
de.spricom.dessert.concepts.cycle.bar -> de.spricom.dessert.concepts.cycle:
	Bar -> CycleDump
de.spricom.dessert.concepts.cycle -> de.spricom.dessert.concepts.cycle.foo:
	CycleDump -> Foo

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.7. 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).minus(ClazzPredicates.DEPRECATED).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.8. 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.

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

4. Practical Guide

4.1. Motivation

Keeping an architecture clean pays out manifold over time, but it has no immediate business value. For developers architecture requirements are an additional burden. If you point out an architecture problem, you will get answers like: "I’ll do that later, first it must be working to show it up to the customer." Actually it’s the same with tests, but tests can be written afterwards. If you point out the same architecture issue after it’s working you get answers like: "Well, it’s working and the customer tested it. Never change a running system." That’s why any architecture degrades over time. To get around this, you have to establish an architecture awareness across your team.

As I pointed out, communicating architecture requirements won’t help. Especially inexperienced developers will focus on other things first. Tools like SonarQube won’t help much, either. Typically, the sonar issues are the last task during a development phase. This is adding javadoc comments, making some variables final or something like that. But in that phase no developer will dare to resolve a package cycle, because such a fundamental change may break the system and needs thorough re-testing.

The dessert library has proved to solve this problem. It’s simple and intuitive syntax can be read by any developer. Thus, it’s an easy way to communicate an architecture requirement. Any violation of such a requirement will break the build on your CI system, because the corresponding dessert test will fail. Hence, the nose of the responsible developer will directly be pointed to the corresponding requirement. Now, the only thing you need, is a plausible comment that explains why it is important to adhere to that requirement. Over time this will build up architecture awareness across your team.

4.2. Sample Code

The code fragments below are excerpts of executable unit tests. See the dessert-site project for the full source code.

4.3. Detecting unwanted dependencies

Developers tend to use everything, simply because it’s there, and it looks promising to accomplish some task. Thus, anything which happens to be available on the class-path can be used for any purpose in any place. If the software is working and the customers are happy, so what is the problem? Well, some reasons for more restrictions might be:

  • Internal APIs are subject to change without notice. Using internal APIs may cause trouble when a dependency is updated.

  • If you know that some library or API will be replaced by something else, or you want to get rid of some dependency then you surely don’t want that new code uses that library.

  • If you have a big monolith and want to break it down into smaller modules, then you must reduce the dependencies that hinder you and prevent developers to introduce additional obstacles.

  • You might want to enforce a clean architecture where certain packages or classes following some naming convention have certain responsibilities. For example your JPA persistence layer should not use JDBC directly or your DTOs should not access the file system.

  • You want to make sure, some critical code does not use anything that may cause security vulnerabilities.

Detecting unwanted dependencies using dessert is as simple as:

assertThatSlice(something).doesNotUse(unwanted1, unwanted2);

Every developer should be able to read and understand this. Because it’s within a unit-test it is checked during each CI build, and it can’t be ignored.

The something and the unwanted each are a Slice. In dessert almost everything is a Slice and you have many ways to tell what’s in a certain slice.

The starting-point for each Slice is the Classpath:

private static final Classpath cp = new Classpath();
The Classpath is a Slice, too.

The something is a part of your handwritten code:

// All classes of a .jar file or of the classes directory containing ClazzResolver.class
Root something1 = cp.rootOf(ClazzResolver.class);

// A single class
Clazz something2 = cp.asClazz(ClazzResolver.class);

// All classes within a certain package
Slice something3 = cp.packageOf(ClazzResolver.class);

// All classes within a certain package or any sub-package
Slice something4 = cp.packageTreeOf(ClazzResolver.class);

// The package-tree limited to the classes from the something1 Root
Slice something5 = something1.packageTreeOf(ClazzResolver.class);

The unwanted might be something like:

// All classes withing the package name 'com.sun' or any sub-package
Slice unwanted1 = cp.slice("com.sun..*");

// An arbitrary list of classes
Slice unwanted2 = cp.sliceOf(ClazzResolver.class, ClassFile.class);

// A combination of packages
Slice unwanted3 = cp.slice("java.lang.reflect|runtime..*");

// Classes from internal packages
Slice unwanted4 = cp.slice("..internal..*");

// Classes following some naming pattern
Slice unwanted5 = cp.slice("..springframework..*Impl");

// Everything form a framework but a certain class
Slice unwanted6 = cp.slice("..springframework..*").minus(cp.sliceOf(Environment.class));

// The deprecated classes from the JUnit-Jupiter API .jar
Slice unwanted7 = cp.rootOf(Test.class).slice(ClazzPredicates.DEPRECATED);

// All classes annotated with @Configuration
Slice unwanted8 = cp.slice(ClazzPredicates.matchesAnnotation(AnnotationPattern.of(Configuration.class)));

// The union of two slices
Slice unwanted9 = unwanted1.plus(unwanted4);

The something and the unwanted examples all specify a 'Slice'. Each of them can be use in the assertThatSlice or in the doesNotUse part of the assertion.

4.4. Enforcing architecture requirements

When defining an architecture you don’t specify which dependencies your building blocks must not use, you rather define the dependencies they do have. With dessert you express this with the usesOnly assertion:

assertThatSlice(block).usesOnly(dep1, dep2);

Of course, block, dep1 and dep2 are slices. Within the usesOnly all dependencies that block has, must be listed. Usually there are many common dependencies like Java SE classes, logging API’s or standard libraries used everywhere in your application. For this purpose you can define an instance variable that you can use all over your architecture test:

private final Slice common = Slices.of(
    cp.slice("java.lang|util|io|net..*"), // java packages
    cp.sliceOf(Logger.class, LogManager.class), // logging
    cp.rootOf(StringUtils.class) // apache commons-lang
);

It’s good practice to have instances variables for all of your main building blocks and dependencies. Then you can have a test method for each build block, that lists all the dependencies it is allowed to have. Some of these dependencies are other building blocks, of course.

private static final Classpath cp = new Classpath();
private static final Root dessert = cp.rootOf(Slice.class);
private static final Slice classfile = dessert.packageTreeOf(ClassFile.class);
private static final Slice slicing = dessert.packageTreeOf(Slice.class);
private static final Slice java = cp.slice("java.lang|util|io|net..*");

@Test
void testClassfileDependencies() {
    assertThatSlice(classfile).usesOnly(java);
}
Slice instances are immutable.

If you have several classes for architecture tests you may want to define your main building blocks in a separate class:

public final class BuildingBlocks {
    public static final Classpath cp = new Classpath();
    public static final Root dessert = cp.rootOf(Slice.class);
    public static final Slice classfile = dessert.packageTreeOf(ClassFile.class);
    public static final Slice slicing = dessert.packageTreeOf(Slice.class);
    public static final Slice java = cp.slice("java.lang|util|io|net..*");

    private BuildingBlocks() {
    }
}
All slices used for an assertion must stem from the same Classpath instance.

4.5. Utilize JPMS information

Libraries implemented with the Java Platform Module System (JPMS) explicitly list the exported packages. These packages form the public API of a library. Every thing else is internal and subject to change without notice. Hence, you want to ensure your building blocks use only the public API. One way to achieve this, is using the JPMS for your project. But the JPMS is a bit cumbersome especially when it comes to testing. Therefore, it’s not that wide-spread.

With dessert you can profit from the JPMS even if you are not using it for your project. You still can make sure that your building blocks use only exported packages. To access module information you need a ModuleRegistry:

private static final Classpath cp = new Classpath();
private static final ModuleRegistry mr = new ModuleRegistry(cp);
private static final JavaModules java = new JavaModules(mr);

The JavaModules and JdkModules define constants for the modules of the Java Platform, Standard Edition (Java SE) and the Java Development Kit (JDK) respectively. To ensure some building block uses only the exported packages of certain Java SE modules, you simply write:

assertThatSlice(dessert)
        .usesOnly(java.base, java.logging);

Any other module can be accessed by name from the ModuleRegistry:

private final ModuleSlice junit = mr.getModule("org.junit.jupiter.api");

If you want to make sure the internal API of a module is not used, then use an assertion like:

assertThatSlice(cp.sliceOf(this.getClass()))
        .doesNotUse(junit.getInternals());
Dessert’s module features are available for older Java versions, too. This may be useful to prepare for an update to a later Java version.

4.6. More details about defining slices

The slice operations can be used to create new slices from existing ones:

Operation Description
Slice union = slice1.plus(slice2);

The resulting slice contains all classes from slice1 and all classes from sice2. An alternative to get the union of slices is the Slices.of method.

Slice intersection = slice1.slice(slice2);

The resulting slice contains only the classes found in both slices.

Slice difference = slice1.minus(slice2);

The resulting slice contains the classes of slice1 that don’t belong to slice2.

The slice operations don’t modify the original slices.

The slice and the minus methods have variants that accept patterns or predicates:

Patterns
// All classes within org.springframework
Slice spring = cp.slice("org.springframework..*");

// All classes with name ending to 'Impl'
Slice impl = spring.slice("..*Impl");

// All classes that contain 'Service' within their name
Slice service = spring.slice("..*Service*");

// All classes in any internal package
Slice internal = spring.slice("..internal..*");

// All classes with the 3rd package named 'core', in this case
// these are all classes within org.springframework.core
Slice core = spring.slice("*.*.core..*");

// A sample for a more complex pattern
Slice complex = cp.slice("..*frame*|hiber*..schema|codec..*Impl");
Patterns are case-sensitive.
Predicates
// All interfaces
Slice interfaces = spring.slice(ClazzPredicates.INTERFACE);

// All deprecated classes
Slice deprecated = spring.slice(ClazzPredicates.DEPRECATED);

// All final public classes
Slice finalpublic = spring.slice(Predicates.and(ClazzPredicates.FINAL, ClazzPredicates.PUBLIC));

// All classes that implement InitializingBean directly
Slice implementsDirectly = spring.slice(ClazzPredicates.implementsInterface(InitializingBean.class.getName()));

// All classes that implement Slice or another interface that extends Slice
// or that extend such a super-class
Slice implementsRecursive = dessert.slice(ClazzPredicates.matches(Slice.class::isAssignableFrom));

// All classes that's simple name matches a regex-pattern
Slice regex = spring.slice(ClazzPredicates.matchesSimpleName(".*[Ss]ervice.*"));

// All classes located in a META-INF/versions/9 directory
Slice version = cp.slice(clazz -> Integer.valueOf(9).equals(clazz.getVersion()));

// All classes throwing a NoClassDefFoundError when trying to load
Slice nodef = spring.slice(this::causesNoClassDefFoundError);

// All classes that have a SourceFileAttribute
Predicate<ClassFile> hasSourceFile = cf ->
        !Attributes.filter(cf.getAttributes(), SourceFileAttribute.class).isEmpty();
Slice source = spring.slice(ClazzPredicates.matchesClassFile(hasSourceFile));

// Some complex predicate using Predicates' combinator logic
Predicate<Clazz> complexPredicate = Predicates.or(
        Predicates.and(ClazzPredicates.ABSTRACT, Predicates.not(ClazzPredicates.INNER_TYPE)),
        ClazzPredicates.ENUM
);
Slice complex = spring.slice(complexPredicate);
Dessert has been optimized for patterns. Evaluation predicates can be slow. Thus, predicates should be always the last thing to filter with.

This is the implementation used above to find classes that could not be loaded:

private boolean causesNoClassDefFoundError(Clazz clazz) {
    try {
        clazz.getClassImpl();
        return false;
    } catch (NoClassDefFoundError er) {
        return true;
    } catch (Throwable th) {
        log.info("{} caused: {}", clazz.getName(), th);
        return false;
    }
}

To debug predicates it is very useful to see, what is in a slice. Hence, a method like this is very useful:

private void dump(Slice slice) {
    slice.getClazzes().stream()
            .map(Clazz::getName)
            .sorted()
            .forEach(System.out::println);
}

Some libraries use annotations to mark internal or experimental code. For example JUnit 5 uses @API Guardian for that purpose. Therefore, dessert provides the AnnotationPattern:

// classes annotated with @Configuration
Slice config = spring.slice(ClazzPredicates.matchesAnnotation(
        AnnotationPattern.of(Configuration.class)));

// classes that have methods annotated with @Bean
Slice beans = spring.slice(ClazzPredicates.matchesAnnotation(
        AnnotationPattern.of(Bean.class)));

// classes that have methods or fields annotated with @Autowired
Slice autowired = spring.slice(ClazzPredicates.matchesAnnotation(
        AnnotationPattern.of(Autowired.class)));

// classes that have methods or fields annotated with @Autowired(required = false)
Slice autowiredOptional = spring.slice(ClazzPredicates.matchesAnnotation(
        AnnotationPattern.of(Autowired.class,
                AnnotationPattern.member("required", false))));

// classes annotated with @ConditionalOnMissingBean({JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class})
Slice conditional = spring.slice(ClazzPredicates.matchesAnnotation(
        AnnotationPattern.of(ConditionalOnMissingBean.class,
                member("value", JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class))));

// a complex annotation pattern which matches to:
//    @ComponentScan(
//            excludeFilters = {@ComponentScan.Filter(
//                    type = FilterType.CUSTOM,
//                    classes = {TypeExcludeFilter.class}
//            ), @ComponentScan.Filter(
//                    type = FilterType.CUSTOM,
//                    classes = {AutoConfigurationExcludeFilter.class}
//            )}
//    )
Slice scan = spring.slice(ClazzPredicates.matchesAnnotation(
        AnnotationPattern.of(ComponentScan.class,
                member("excludeFilters",
                        AnnotationPattern.of(ComponentScan.Filter.class,
                                member("type", FilterType.CUSTOM),
                                member("classes", TypeExcludeFilter.class)
                        ),
                        AnnotationPattern.of(ComponentScan.Filter.class)
                ))
));

// the experimental junit classes
Slice experimental = junit.slice(ClazzPredicates.matchesAnnotation(
        AnnotationPattern.of(API.class, member("status", API.Status.EXPERIMENTAL))
));
Dessert recognizes annotations for retention policy RUNTIME and CLASS.

4.7. Detecting 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 concern 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 detect cycles between any set of slices (remember: a Clazz is a Slice, too). Sometimes it’s hard and unnecessary to prevent class cycles, but package-cycles are a clear indicator for design flaws. There is always a clean design without package-cycles. Use the following code to assert that some building block block has no package-cycle:

assertThatSlice(block.partitionByPackage()).isCycleFree();

Typically, it’s necessary to move classes to resolve a cycle. To be backwards compatible a deprecated place-holder must be kept in the old place. Such place-holders must be ignored, when checking for cycles. A good example for this is JUnit 5:

assertThatSlice(cp.slice("org.junit..*")
        .minus(ClazzPredicates.DEPRECATED)
        .partitionByPackage()).isCycleFree();

Any collection of slices can be used for cycle detection:

List<Slice> slices = Arrays.asList(slice1, slice2, slice3);
assertThatSlices(slices).areCycleFree();

This can be written even shorter:

assertThatSlices(slice1, slice2, slice3).areCycleFree();

Because each Clazz is a Slice, detecting class-cycles is as simple as:

assertThatSlice(slice1.getClazzes()).isCycleFree();

Even a Map can be used to pass the slices to check:

Map<String, Slice> slicesByName = Map.of(
        "slice1", slice1,
        "slice2", slice2,
        "slice3", slice3
);
assertThatSlices(slicesByName).areCycleFree();

The partitionByPackage() method for a Slice returns a Map of package-names to their corresponding package slices. Hence, all non-empty packages of a slice can be listed like that:

spring.partitionByPackage().keySet().forEach(System.out::println);

To show the number of classes in each package use:

spring.partitionByPackage()
        .forEach((k, s) -> System.out.printf("%s[%d]%n", k, s.getClazzes().size()));

Any function that maps a Clazz to a string can be used to partition the classes of a slice:

SortedMap<String, PartitionSlice> topLevelPackages = spring.partitionBy(this::topLevelPackageName);
assertThatSlices(topLevelPackages).areCycleFree();

The implementation below return the top-level package name for spring-framework classes:

private String topLevelPackageName(Clazz clazz) {
    Pattern pattern = Pattern.compile("org\\.springframework\\.([^.]+)\\.");
    Matcher matcher = pattern.matcher(clazz.getName());
    if (matcher.lookingAt()) {
        return matcher.group(1);
    }
    return clazz.getPackageName(); // fall-back, should not happen
}

Dessert comes with some pre-defined slice-partitioners. One of them is SlicePartitioners.HOST. The host is the class that hosts all nested classes. For a host or a class without nested classes the host is the class itself. Thus, this partitioner produces slices where each slice contains a class together with all its inner-classes:

assertThatSlice(block.partitionBy(SlicePartitioners.HOST)).isCycleFree();

4.8. Keeping vertical slices apart

Big applications, sometimes called monolith, are used by different stakeholders for very different use-cases. Image some stamp-shop where a customer can buy stamps. The shop-owner must be able to add new offerings and update prices. An office clerk handles the orders and sends the stamps to customer. Once a month the shop-owner needs to get some statistics. Hence, an application consists of verify different and hopefully independent parts (typically there is no 1:1 mapping between use-case and part, that’s why I name it part). Normally the parts share components, the business model, services and other things, and they are tied together within a common shell that provides authentication and access control. Thus, the architecture of the application looks something like this:

vertical slices

In a clean design each part has its own module with explicit dependencies to prevent any unintended cross-connections. But, this brings some overhead, especially if there are many small parts. An alternative approach is to use dessert:

Root stampShop = cp.rootOf(ShopApplication.class);
Slice parts = stampShop.slice("..stampshop.parts..*"); // the parent package of all parts
Map<String, ? extends Slice> partsByName = parts.partitionBy(clazz ->
        clazz.getPackageName().split("\\.", 6)[5]); // partition by sub-package name
partsByName.forEach((nameA, partA) -> partsByName.forEach((nameB, partB) -> {
    if (!(nameA.equals(nameB))) {
        assertThatSlice(partA).doesNotUse(partB);
    }
}));

The doubly nested loop can be replaced by using dessert’s CombinationUtils:

CombinationUtils.combinations(new ArrayList<>(partsByName.values()))
        .forEach(pair -> assertThatSlice(pair.getLeft()).doesNotUse(pair.getRight()));

4.9. Checking a layered architecture

The stamp-shop above is also an example for a layered architecture with 3 layers:

layers

Dessert provides convenience methods to check layers:

Root stampShop = cp.rootOf(ShopApplication.class);
Slice application = stampShop.slice("..stampshop.application..*");
Slice parts = stampShop.slice("..stampshop.parts..*");
Slice commons = stampShop.slice("..stampshop.commons..*");

assertThatSlices(application, parts, commons).areLayeredStrict();

The test above will fail if application uses commons. To relax this use:

assertThatSlices(application, parts, common).areLayeredRelaxed();

4.10. Detecting 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. Hence, your application may use the implementation of class A from jar X on one system and the implementation from jar Y on other systems. If these two implementations behave differently, then you have a problem.

To prevent duplicates in your application write a test as simple as:

assertThat(cp.duplicates().minus("module-info").getClazzes()).isEmpty();
Many JARs contain a module-info class in their root package. Make sure to ignore this class when checking for duplicates.

Sometimes you cannot prevent all duplicates (i.e. if you’re using Java 8), but at least you should have a test that informs you if there are additional duplicates.

@Test
@DisplayName("Make sure there are no additional duplicates")
void ensureNoAdditonalDuplicates() {
    Slice duplicates = cp.duplicates().minus("module-info");

    List<File> duplicateJars = duplicates.getClazzes().stream()
            .map(this::getRootFile).distinct()
            .sorted(Comparator.comparing(File::getName))
            .collect(Collectors.toList());

    Map<String, Set<File>> duplicateJarsByClass = duplicates.getClazzes().stream()
            .collect(Collectors.groupingBy(Clazz::getName,
                    TreeMap::new,
                    Collectors.mapping(this::getRootFile, Collectors.toSet())));

    System.out.printf("There are %d duplicate classes spread over %d jars:%n",
            duplicateJarsByClass.size(), duplicateJars.size());
    System.out.println("\nDuplicate classes:");
    duplicateJarsByClass.forEach((name, files) -> System.out.printf("%s (%s)%n", name,
            files.stream().map(File::getName).sorted().collect(Collectors.joining(", "))));
    System.out.println("\nJARs containing duplicates:");
    duplicateJars.forEach(jar -> System.out.printf("%s%n", jar.getAbsolutePath()));

    // make sure there are no additional jars involved
    assertThat(duplicateJars.stream().map(File::getName))
            .areAtLeast(3, matching(startsWith("jakarta.")))
            .hasSize(5);

    // make sure there are no additonal classes involved
    assertThat(duplicates
            .minus("javax.activation|annotation|transaction|xml..*")
            .minus("com.sun.activation..*")
            .getClazzes()).isEmpty();
}

private File getRootFile(Clazz clazz) {
    return clazz.getRoot().getRootFile();
}

The sample above prints all duplicate classes and the corresponding jars.

You may want to do some further investigations. To list all classes for which there are binary differences in the .class file, you can use:

@Test
@DisplayName("Dump all duplicates for which the .class files are different")
void dumpBinaryDifferences() {
    Slice duplicates = cp.duplicates().minus("module-info");

    Map<String, List<Clazz>> duplicatesByName = duplicates.getClazzes().stream()
            .collect(Collectors.groupingBy(Clazz::getName));

    for (List<Clazz> list : duplicatesByName.values()) {
        list.subList(1, list.size()).forEach(c -> checkBinaryContent(list.get(0), c));
    }
}

private void checkBinaryContent(Clazz c1, Clazz c2) {
    if (!isSameBinaryContent(c1, c2)) {
        System.out.printf("Binaries of %s in %s and %s are different.%n",
                c1.getName(), getRootFile(c1).getPath(), getRootFile(c2).getPath());
    }
}

private boolean isSameBinaryContent(Clazz c1, Clazz c2) {
    try {
        byte[] bin1 = IOUtils.toByteArray(c1.getURI().toURL().openStream());
        byte[] bin2 = IOUtils.toByteArray(c2.getURI().toURL().openStream());
        return Arrays.equals(bin1, bin2);
    } catch (IOException ex) {
        throw new IllegalStateException("Cannot compare duplicates of " + c1.getName());
    }
}

To list all classes for which there are API differences, you can use:

@Test
@DisplayName("Dump all duplicates for which the API differs")
void dumpApiDifferences() {
    Slice duplicates = cp.duplicates().minus("module-info");

    Map<String, List<Clazz>> duplicatesByName = duplicates.getClazzes().stream()
            .collect(Collectors.groupingBy(Clazz::getName));

    for (List<Clazz> list : duplicatesByName.values()) {
        list.subList(1, list.size()).forEach(c -> checkAPI(list.get(0), c));
    }
}

private void checkAPI(Clazz c1, Clazz c2) {
    if (!isSameAPI(c1, c2)) {
        System.out.printf("API of %s in %s and %s is different.%n",
                c1.getName(), getRootFile(c1).getPath(), getRootFile(c2).getPath());
    }
}

private boolean isSameAPI(Clazz c1, Clazz c2) {
    ClassFile cf1 = c1.getClassFile();
    ClassFile cf2 = c2.getClassFile();
    return cf1.getAccessFlags() == cf2.getAccessFlags()
            && cf1.getThisClass().equals(cf2.getThisClass())
            && cf1.getSuperClass().equals(cf2.getSuperClass())
            && Arrays.equals(cf1.getInterfaces(), cf2.getInterfaces())
            && isEqual(cf1.getFields(), cf2.getFields(), this::isEqual)
            && isEqual(cf1.getMethods(), cf2.getMethods(), this::isEqual);
}

private <T> boolean isEqual(T[] t1, T[] t2, BiPredicate<T, T> predicate) {
    if (t1 == null && t2 == null) {
        return true;
    }
    if (t1 == null || t2 == null) {
        return false;
    }
    if (t1.length != t2.length) {
        return false;
    }
    for (int i = 0; i < t1.length; i++) {
        if (!predicate.test(t1[i], t2[i])) {
            return false;
        }
    }
    return true;
}

private boolean isEqual(MethodInfo m1, MethodInfo m2) {
    return m1.getAccessFlags() == m2.getAccessFlags()
            && m1.getDeclaration().equals(m2.getDeclaration());
}

private boolean isEqual(FieldInfo f1, FieldInfo f2) {
    return f1.getAccessFlags() == f2.getAccessFlags()
            && f1.getDeclaration().equals(f2.getDeclaration());
}

4.11. Simulating refactorings

Sometimes a package cycle can be resolved by moving a class from one package to another. Doing that may require many changes and — as a side effect — new cycles may be introduced. Wouldn’t it be nice if one could predict the effects of moving a class?

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:

Root stampshop = cp.rootOf(ShopApplication.class);

// Make sure original packages are cycle-free.
SortedMap<String, Slice> packages = new TreeMap<>(stampshop.partitionByPackage()); (1)
assertThatSlices(packages).areCycleFree();

// Simulate moving class SomeUtil to package of the ShopApplication class.
Clazz classToMove = cp.asClazz(SomeUtil.class);
packages.compute(classToMove.getPackageName(),
        (packageName, slice) -> slice.minus(classToMove).named(packageName)); (2)
packages.compute(ShopApplication.class.getPackageName(),
        (packageName, slice) -> slice.plus(classToMove).named(packageName));

// Check for package cycles after moving the class.
assertThatSlices(packages).areCycleFree(); (3)

The sample shows: If one moved SomeUtil from ..stampshop.parts.part3 to ..stampshop.application that would introduce a package cycle.

1 Create new Map<String, Slice> from Map<String, PackageSlice> to be able to replace a PackageSlice by a Slice.
2 Assign the package-name to the newly create Slice so that it looks like a PackageSlice in the AssertionError message.
3 This assertion will fail, because a package-cycle would be introduced by moving SomeUtil.

4.12. Defining a custom classpath

By default, the Classpath is based on the path defined by the java.class.path system property. For most use-cases that’s what you want, but there might be circumstances where this is not suitable. Classpath uses a ClassResolver to determine the locations it looks for classes. ClassResolver has static factory methods for most common use cases, for example:

Classpath customClasspath = new Classpath(ClassResolver.ofClassPathWithoutJars());

The code above defines a Classpath that contains only the classes directories of the current class-path.

Never use different Classpath instances in a dessert test. The result of Slice operations and assertions is undefined if the slices originate from different Classpath instances and the behaviour may change over time.

A Classpath, that does not contain all classes used for an application, has some restrictions. For example, assertions using name patterns do work:

assertThatSlice(customClasspath).doesNotUse(customClasspath.slice("org.junit.jupiter..*"));
Classpath implements the Slice interfaces, thus assertThatSlice can be called with customClasspath.

But assertions, that need access to the .class file, will fail:

assertThatSlice(customClasspath).doesNotUse(customClasspath.rootOf(Test.class));

The code above will throw:

java.lang.IllegalArgumentException: org.junit.jupiter.api.Test not found within this classpath.

To see all locations, where classes are searched for, you might use:

customClasspath.getClazzes().stream()
        .map(Clazz::getRoot)
        .map(Root::getURI)
        .distinct()
        .forEach(System.out::println);

The code above is very slow, because it iterates over all classes, rather use ClassResolver.getPath():

private void dumpRoots(ClassResolver resolver) {
    resolver.getPath().stream()
            .map(ClassRoot::getURI)
            .forEach(System.out::println);
}

To see all locations used by default you can use:

dumpRoots(ClassResolver.ofClassPathAndJavaRuntime());

The code above lists all locations where a Classpath instance, created with the default-constructor, searches for classes.

You may even use the default Classpath to determine the location of .jar files, so that you can build a custom Classpath:

Classpath cp = new Classpath();
File jupiterApiJar = cp.asClazz(Test.class).getRoot().getRootFile();
File jupiterEngine = cp.asClazz(JupiterTestEngine.class).getRoot().getRootFile();
File junitPlatformEngine = cp.asClazz(TestEngine.class).getRoot().getRootFile();

ClassResolver resolver = new ClassResolver();
resolver.add(junitPlatformEngine);
resolver.add(jupiterEngine);
resolver.add(jupiterApiJar);
Classpath customClasspath = new Classpath(resolver);

5. 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 (no 3rd party libraries required)

  • Support a wide range of java versions and 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 18

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

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

7. Frequently asked Questsions

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

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

8. Plans for dessert-core 0.6.x

  1. Removal of all deprecated classes and methods

  2. Virtual Clazzes (a view on specific methods or fields of a Clazz)

  3. Double-scan of Jar-Files to reduce open file-handles, memory-consumption and startup-time

  4. Performance improvements by optimized pattern-matching and more lazy evaluation

  5. Considering related classes when processing predicates to be able to recognize:

    • Any ancestor of the super classes or an implemented interface

    • Inherited annotations

    • Meta-annotations

9. Release Notes

9.1. dessert-core-0.5.5

Bugfix-release:

  • Does not recurse into subpackages of xx.yy when resolving name patterns like '..xx.yy.*'.

  • Performance for slice.slice(pattern).slice(pattern) has been improved.

9.2. dessert-core-0.5.4

Performance improvement:

  • Does not resolve all classes of a deferred slice to check whether one class belongs to the slice. This brings a huge performance boost for library slices given by name-patterns that comprise many classes.

9.3. dessert-core-0.5.3

Bugfix-release:

  • Return same URI as ClassLoader for JDK classes by removing modules prefix.

9.4. dessert-core-0.5.2

Minor additions and bugfixes:

  • Assertions method aliases added for using plural in assertions.

  • Alias assertThatSlice added for dessert.

  • Bugfix: isLayeredStrict does not skip first slice anymore.

  • Bugfix: Doesn’t log warning for versioned duplicates in multi-release jars.

  • Bugfix: Encoded URL’s in within Manifest Class-Path entries will be resolved correctly.

  • Javadocs added and typos fixed.

  • Documentation: Tutorial replaced by practical guide.

9.5. dessert-core-0.5.1

Bugfixes and minor enhancements:

  • JPMS detection fixed for Java 8

  • Adds ClazzPredicates.DEPRECATED

  • Static constructor methods os ClassResolver throw ResolveException instead of an IOException

  • Javadocs added and typos fixed

9.6. dessert-core-0.5.0

This feature release primarily adds support for the JPMS, even for JDK 8 and older:

  • Utilize information within module-info classes, to make sure only exported classes are used.

  • Ready-to-use module definitions for the JDK that resemble the Java17 modules, to be used for older java versions

  • Supports .class files up to Java 18 (inkl. sealed classes and records)

  • Support multi-release jars

  • Predicates for filtering by Annotations (for retention types class and runtime)

  • API for nested classes

  • Some utilities for combinations and dependency-closure

  • Deprecated Classpath method sliceOf(String…​) has been removed

9.7. dessert-core-0.4.3

Preparation for 0.5.0:

  • Issue #4: Adds entries from Class-Path header of Manifest files

  • Improved DefaultCycleRenderer lists classes involved in cycle

  • SliceAssert alias method doesNotUse for usesNot added

  • Classpath method sliceOf(String…​) deprecated (to be removed in 0.5.0)

9.8. dessert-core-0.4.2

Bugfix-release:

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

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

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

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