A library for unit-tests to check the dependencies between classes.
Repository | Description |
---|---|
Library source code |
|
Documentation source code |
|
Tests and samples |
Release | Reference Doc. | API Doc. |
---|---|---|
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.1</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.2-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. User Documentation
2.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.
2.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.
2.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:
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.
2.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.
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 |
2.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
.
2.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.
2.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 ).
|
2.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 detected: de.spricom.dessert.cycle.foo.Foo -> de.spricom.dessert.cycle.bar.Bar de.spricom.dessert.cycle.bar.Bar -> de.spricom.dessert.cycle.CycleDump de.spricom.dessert.cycle.CycleDump -> 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 detected: de.spricom.dessert.cycle.foo -> de.spricom.dessert.cycle.bar: Foo -> Bar de.spricom.dessert.cycle.bar -> de.spricom.dessert.cycle: Bar -> CycleDump de.spricom.dessert.cycle -> de.spricom.dessert.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
.
2.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).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();
2.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.
3. 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.
3.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).
3.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.
3.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.
3.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.
3.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.
3.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.
3.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.
3.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.
4. 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.
5. Frequently asked Questsions
5.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.
5.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. |
6. Release Notes
6.1. 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
6.2. 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
methodsliceOf(String…)
has been removed
6.3. 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 methoddoesNotUse
forusesNot
added -
Classpath
methodsliceOf(String…)
deprecated (to be removed in 0.5.0)
6.4. dessert-core-0.4.2
Bugfix-release:
-
The cycle detection algorithm ignores dependencies within the same slice, now.
6.5. dessert-core-0.4.1
Some minor changes:
-
Duplicate .class files in JAR files won’t cause an AssertionError.
-
A
Clazz
created byClasspath.asClazz(java.lang.Class<?>)
immediately contains all alternatives on theClasspath
. -
ClassPackage
internally usesTreeMap
instead ofList
to lookup classes. This improves the performance if a package has many classes. -
Many Javadoc additions.
6.6. 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
6.7. Older Releases
See GitHub releases.
7. Copyright and License
Code and documentation copyright 2017–2021 Hans Jörg Heßmann. Code released under the Apache License 2.0.