Maven Specifications and Environments: Part 1

In the previous post, we took a look at specifications: Maven pom.xml files that represent pom-typed artifacts that house dependencies in compile scope, but are themselves depended upon in provided scope.

Specifications give you the artifacts you need to compile your code. But (if built properly) they don’t come with implementations. This lets your code be coupled to, say, the CDI specification without being coupled to, say, Weld (instead of, say, OpenWebBeans).

I’ve settled on the term environment to describe another Maven pattern I use to back a particular specification with the jars needed to implement it. The challenge here is that a given environment is—like the specification it implements—comprised of many jars. To reduce boilerplate, we want to find a good way to declare our runtime dependence on various jars that implement the classes and interfaces defined by the API jars our code relies upon during compilation.

Another challenge is that strictly speaking an environment often correctly and legally brings in its own implementations of API jars. For example, Weld does not depend on javax.interceptor:javax.interceptor-api:jar (which houses the javax.interceptor packages that reify the interceptors specification), but on org.jboss.spec.javax.interceptor:jboss-interceptors-api_1.2_spec:jar. This jar file also houses the javax.interceptor packages that reify the interceptors specification. This plurality of API jars reifying the same (English) specification is entirely legal, but you should only have one API jar at runtime, and the runtime (Weld, OpenWebBeans) should pick it (org.jboss.spec.javax.interceptor:jboss-interceptors-api_1.2_spec:jar, javax.interceptor:javax.interceptor-api:jar). That is, at runtime you want the runtime environment to dictate which API jars are actually on the classpath, not your own Maven pom.xml file.

The idea behind an environment as represented by a pom.xml file is basically the same as that of a specification as represented by a pom.xml file. You define your pom.xml to have a packaging type of pom, and you list your elements, but this time you put them in runtime scope. Then you depend on this new environment by depending on it in runtime scope. The net effect is that its runtime-scoped dependencies become your project’s transitive dependencies in runtime scope.

Note that an environment defined like this doesn’t (necessarily) depend on a specification as we defined it in the previous post. An environment always supplies what it needs, and code designed to run in any environment of a particular kind is compiled against a specification as defined in the prior post.

Environments can be composed, and can be abstract or concrete. Here’s an example of one of the environments I use in my microBean projects. It is abstract, in the sense that it sketches out a modular runtime but lacks a CDI implementation. It does, however, specify a javax.validation implementation (Hibernate) and a Java Expression Language implementation (Glassfish):

<groupId>org.microbean</groupId>
<artifactId>microbean-abstract-environment</artifactId>
<version>0.5.3-SNAPSHOT</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<!– Normal dependencies. –>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.fasterxml</groupId>
<artifactId>classmate</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<type>jar</type>
<version>3.0.1-b10</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-cdi</artifactId>
<version>6.0.13.Final</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.3.2.Final</version>
</dependency>
<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-configuration</artifactId>
<version>0.4.4</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-configuration-api</artifactId>
<version>0.4.4</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-configuration-cdi</artifactId>
<version>0.4.5</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-main</artifactId>
<version>7</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.8.0-beta2</version>
<type>jar</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!– Runtime-scoped dependencies. –>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<type>jar</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-cdi</artifactId>
<type>jar</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-configuration</artifactId>
<type>jar</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-configuration-api</artifactId>
<type>jar</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-configuration-cdi</artifactId>
<type>jar</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-main</artifactId>
<type>jar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
view raw 03.pom.xml hosted with ❤ by GitHub

Here is another example of an environment that I use in my microBean projects that uses the abstract environment above:

<groupId>org.microbean</groupId>
<artifactId>microbean-weld-se-environment</artifactId>
<version>0.5.4-SNAPSHOT</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<!– Imports. –>
<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-abstract-environment</artifactId>
<version>0.5.3-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-core-bom</artifactId>
<version>3.0.5.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!– Normal dependencies. –>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.jboss</groupId>
<artifactId>jandex</artifactId>
<version>2.0.5.Final</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.8.0-beta2</version>
<type>jar</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!– Runtime-scoped dependencies. –>
<dependency>
<groupId>org.jboss</groupId>
<artifactId>jandex</artifactId>
<type>jar</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
<type>jar</type>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.el</groupId>
<artifactId>jboss-el-api_3.0_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-abstract-environment</artifactId>
<type>pom</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<type>jar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
view raw 04.pom.xml hosted with ❤ by GitHub

Line 85 is where we pull in the abstract environment. Note that it is in runtime scope. That means we pull in everything that environment defines, as well as whatever else is listed here. The end result is the contents of that abstract environment, plus the Jandex runtime, a SLF4J logging binding and Weld itself. At line 76, you can see that because the abstract environment has already pulled in a runtime implementation of the Java Expression Language, it by definition has supplied its own EL API jar, so we want to make sure that that is the API jar in use for that specification, so we exclude JBoss’ EL API jar here.

Once this environment has been defined, then we just have to use it. Let’s say we want to build a program that will run in this environment. In our project’s pom.xml, we would simply do this:

<dependency>
<groupId>org.microbean</groupId>
<artifactId>microbean-weld-se-environment</artifactId>
<version>0.5.4-SNAPSHOT</version>
<type>pom</type>
<scope>runtime</scope> <!– or test –>
</dependency>
view raw 05.pom.xml hosted with ❤ by GitHub

The last remaining hurdle is testing. Even when compiling code against a specification, you often want to test it in one of possibly many environments. When you do this, you want to make sure you’re testing against the environment and its dependencies, and not the provided-scoped specification that you’re compiling against. (Often the differences are merely academic, but not always.) We’ll look at this in the next post.

Author: Laird Nelson

Devoted husband and father; working on Helidon at the intersection of Java, Jakarta EE, architecture, Kubernetes and microservices at Oracle; open source guy; Hammond B3 player and Bainbridge Islander.

One thought on “Maven Specifications and Environments: Part 1”

Comments are closed.