🕵️ Deptective is a plug-in for the Java compiler (javac) that validates the dependencies amongst a project's packages against a description of allowed dependences and fails the compilation when detecting any unintentional dependencies.
🕵 In order to implement comprehensible and maintainable software systems, a well-defined structure between their components is needed. While module systems (multi-module builds, the Java Module System, OSGi etc.) are proven means of structuring large code bases, software structure should also be defined and enforced at a lower level, i.e. within individual modules and by defining which APIs should be accessed across module boundaries.
Deptective helps with this task by allowing you to define a software system's structure in terms of intended package relationships (e.g. com.example.service
may read com.example.persistence
, but not the other way around)
and enforcing these relationships at compile time.
Implemented as a plug-in for the javac compiler, Deptective will abort the compilation when detecting unwanted package dependencies, allowing you to fix the issue at the earliest time possible.
Compared to traditional architecture monitoring tools (that for instance run once nightly on a CI server and whose reports are easy to ignore), hooking right into the compiler itself allows for very fast feedback cycles.
The overhead is very low, e.g. less than 1 sec for a medium-sized code base such as Hibernate Validator (740 classes).
The following shows an example when using Deptective via Maven:
Optionally, you also can visualize the package relationships via GraphViz, highlighting any unwanted relationships.
The following shows an example from the Spring PetClinic sample application, which has been modified to have an undesired reference from the model
to the visit
package:
🕵 JDK 8 or later is needed to run Deptective.
The plug-in is specific to javac, i.e. the compiler coming with the JDK, it does not work with other compilers such as the Eclipse Batch Compiler (ecj). Support for ecj may be added later on.
Deptective can be used with any Java build system such as Maven, Gradle etc.
🕵 Deptective is configured through a file deptective.json which describes the allowed dependencies amongst the project's packages.
🕵 The deptective.json file is structured like this:
{
"packages" : [
{
"name" : "com.example.foo",
"reads" : [
"com.example.bar",
"com.example.baz"
]
},
{
"name" : "com.example.bar",
"reads" : [
"com.example.baz"
]
}
],
"whitelisted" : [
"java.util*",
"java.swing*"
]
}
packages
is a list of Package
objects.
The Package
object has a name
property (fully-qualified name of the described package)
and a reads
property (list of strings representing the fully-qualified names of other packages read by the given package).
whitelisted
is a list of strings representing whitelisted packages,
i.e. packages that always can be read by any other package.
The *
character can be used as a wildcard, so e.g. java.util*
will whitelist the packages java.util
, java.util.concurrent
etc.
Note: access to the package java.lang
is always allowed.
Place the configuration file in the root of your source directory (e.g. src/main/java for Maven projects)
or on the classpath at META-INF/deptective.json (e.g. META-INF/src/main/resources/deptective.json for Maven projects).
Alternatively you can specify the location of the config file using the -Adeptective.configfile
option (see below).
🕵 In order to use Deptective, add deptective-javac-plugin-1.0-SNAPSHOT.jar to your project's annotation processor path and specify the option -Xplugin:Deptective
when invoking javac.
🕵 When using Maven, add the following configuration to your pom.xml:
...
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<showWarnings>true</showWarnings>
<compilerArgs>
<arg>-Xplugin:Deptective</arg>
<!-- specify options like so -->
<!-- <arg>-Adeptective.reportingpolicy=WARN</arg> -->
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.moditect.deptective</groupId>
<artifactId>deptective-javac-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
...
See integration-test/pom.xml for a complete example.
🕵 When using Gradle, add the following configuration to your build.gradle:
...
dependencies {
annotationProcessor 'org.moditect.deptective:deptective-javac-plugin:1.0-SNAPSHOT'
}
tasks.withType(JavaCompile) {
options.compilerArgs = [
'-Xplugin:Deptective',
'-Adeptective.mode=VALIDATE',
'-Adeptective.reporting_policy=ERROR',
'-Adeptective.visualize=true',
"-Adeptective.config_file=${projectDir}/src/main/resources/META-INF/deptective.json"
]
}
...
See integration-test/build.gradle for a complete example.
🕵 The following options can be provided when running the plug-in:
-Adeptective.config_file=path/to/deptective.json
: Path of the configuration file in the file system-Adeptective.reporting_policy=(ERROR|WARN)
: Whether to fail the build or just raise a warning when spotting any illegal package dependencies (defaults toERROR
; make sure to set<showWarnings>
totrue
when using the plug-in via Maven)-Adeptective.unconfigured_package_reporting_policy=(ERROR|WARN)
: Whether to fail the build or just raise a warning when detecting a package that's not configured in the config file (defaults toWARN
)-Adeptective.mode=(ANALYZE|VALIDATE)
: Whether the plug-in should validate the packages of the compiled package against the deptective.json file (VALIDATE
) or whether it should generate a template for that file based on the current actual package relationships (ANALYZE
). The latter can be useful when introducing Deptective into an existing code base where writing the configuration from scratch might be too tedious. Generating the configuration from the current "is" state and iteratively refining it into an intended target state can be a useful approach in that case. The generated JSON/DOT files are created in the compiler's class output path, e.g. target/classes in case of Maven. Defaults toVALIDATE
-Adeptective.whitelisted=...
: A comma-separated list of whitelist package patterns which will be applied inANALYZE
mode. Any reference to a whitelisted package will then not be added to thereads
section of the referencing package in the generated descriptor template. The special value*ALL_EXTERNAL*
can be used to automatically whitelist all packages which are not part of the current compilation (i.e. packages from dependencies). This can be useful if you're only interested in managing the relationships amongst the current project's packages themselves but not the relationships to external packages.-Adeptective.visualize=(true|false)
: Whether to create a GraphVize (DOT) file representing generated configuration template (inANALYZE
mode) or the dependency configuration and (if present) any illegal package dependencies (inVALIDATE
mode). Defaults tofalse
🕵 Deptective is not yet available in Maven Central. For the time being, you can obtain the latest snapshot JARs via Jitpack. Add the following repository to your project's pom.xml or your Maven settings.xml file:
<repositories>
<repository>
<id>jitpack</id>
<name>Jitpack</name>
<url>https://jitpack.io</url>
</repository>
</repositories>
Then reference the Deptective JAR using the GAV coordinates com.github.moditect.deptective:deptective-javac-plugin:master-SNAPSHOT
.
See jitpack-example/pom.xml for a complete example.
For Gradle, add the repo like this:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
🕵 In order to build Deptective, OpenJDK 11 or later and Apache Maven 3.x must be installed. Then obtain the source code from GitHub and build it like so:
git clone https://github.com/moditect/deptective.git
cd deptective
mvn clean install
Your contributions to Deptective in form of pull requests are very welcomed. Before working on larger changes, it's recommended to get in touch first to make sure there's agreement on the feature and design.
🕵 To work on the code base in Eclipse, please follow this steps:
- Run Eclipse with (at least) Version 2018-12/4.10.0 and make sure it runs with OpenJDK 11
- In Eclipse, register an OpenJDK 11 instance ("Preferences" -> "Java" -> "Installed JREs") if not already there
- Then run "File" -> "Import..." -> "Maven" -> "Existing Maven Projects" and select the root folder of this repository.
- After importing the project, make sure that Java 11 is on the build path of the javac-plugin module (right-click on that module, then "Properties" -> "Java Build Path" -> "Libraries").
🕵 Different projects exist that analyze Java package dependencies, validate and/or produce metrics on them. I'm not aware of any tool though that provides instantaneous feedback about any unwanted dependencies right during compilation. Some related tools are:
- ArchUnit aims at enforcing architectures described in a Java DSL. In contrast to Deptective it is not executed during compilation but via (JUnit) tests.
- JDepend analyzes Java packages and produces metrics on them.
- The Eclipse Java compiler allows to put access restrictions in place but they can only be used to limit access to types/packages in other JARs on the classpath, not to packages of the current compilation unit itself
- code-assert allows to enforce dependency rules described in a Java DSL. Similarly to ArchUnit, these rules are executed as unit tests.
- Macker enforces architectural rules at build time via dedicated build tool plug-ins. (not actively maintained)
🕵 Deptective is licensed under the Apache License version 2.0.