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.

Advertisement

Maven Specifications and Environments: Part 0

I’d like to blog about two concepts that I’ve finally figured out how to express in Maven.

The first is that of a specification.  By specification, I mean loosely a collection of versioned artifacts that you code to, but without relying on any particular underlying implementation of those versioned artifacts.

Consider the CDI specification—the online document.  It is expressed in code form in terms of Java packages like javax.enterprise.inject, javax.enterprise.event, and so on.  These packages, in turn, and their classes, are reified in so-called API jars such as javax.enterprise:cdi-api:2.0:jar.  While an API jar is not itself the specification (different vendors may supply their own definitionally functionally equivalent reifications of the specification document) nor an implementation of it, it is one of possibly several reifications of the APIs described by the specification, and so when we’re talking about specifications in terms of Java code, it’s useful to just treat the API jars (from any given vendor) as the specification itself.

Note as well that the CDI specification (specifically) is actually a directed acyclic graph of API jars.  For example, the javax.enterprise:cdi-api:2.0:jar artifact depends on javax.el:el-api:3.0.0:jar, javax.inject:javax.inject:1:jar and javax.interceptors:interceptor-api:1.2:jar.  (It also has to depend on a version of the javax.annotation:javax.annotation-api:jar artifact, but omits this requirement for some reason, as does the specification document.)

So broadly speaking these jars comprise one possible reification of the CDI specification.  (They are not its implementation.)

These jars are also, of course, on their own useful only to prevent compilation errors.  Without a backing implementation, such as Weld, they’re otherwise useless.

Now, if you are a Maven user and you depend on javax.enterprise:cdi-api:2.0:jar in compile scope, then you will also depend on its dependencies in compile scope.  That also means perhaps less obviously that you’ll drag these jars with you into any runtime environment you might find your code in, even if that runtime environment happens to already have those jars.  In fact, in many cases, these jars—or functionally definitionally equivalent ones supplied by another vendor—will already be present, because the runtime that implements CDI 2.0 will include them.  So (as you probably know) you want to depend on javax.enterprise:cdi-api:2.0:jar in provided scope.  This is a means of instructing Maven that javax.enterprise:cdi-api:2.0:jar is provided by some implementation of the specification it represents.

But a dependency in provided scope does not also drag in its transitive dependencies!  So if you do this, you’ll miss the rest of the jars that comprise the CDI specification.  So you’ll also want to add explicit dependencies on javax.annotation:javax.annotation-api:1.3:jarjavax.el:el-api:3.0.0:jar, javax.inject:javax.inject:1:jar and javax.interceptors:interceptor-api:1.2:jar.

So this might look like this:


<dependencies>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.1-b04</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>2.0</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
</dependencies>

view raw

00.pom.xml

hosted with ❤ by GitHub

That is a lot of boilerplate to remember each time you want, in practical terms, to depend on the CDI specification without coupling yourself to, say, Weld (one of several possible CDI-compliant runtimes).  Can we reduce this boilerplate?

We can.  A neat little trick of Maven is that your pom.xml can depend on another pom.xml in whatever scope you like.  Let’s see how this helps us out.

First, though, a slight digression.  Many of you may be familiar with import scope.  That’s not what I’m talking about here.  Maven defines a weird scope called import that (a) isn’t really a scope, (b) can be used only from within a  stanza, and (c) is applicable only to artifacts of type pom.  Briefly, if you add a  element in your pom.xml‘s  stanza that references an artifact of type pom with a scope of import, then that pom.xml‘s  stanza’s contents are effectively copied by value into your pom.xml‘s  stanza in place of the import-scoped  itself.  So what you’re really “importing” is the  stanza of the target pom.xml and nothing else.  You can read more about import scope in the official Maven documentation.  But remember the shorthand takeaway: it’s basically a textual templating mechanism, not an actual scope.

Whether or not you use import scope in your  stanza, you can depend on artifacts not just of type jar or war or zip and so on, but also on artifacts of type pom.  When you do this, the artifact of type pom in question becomes a direct dependency of your project, and any dependencies it declares become transitive dependencies of your project.  In terms of dependency graphs, this is no different from what happens when you depend on, say, JUnit (a jar artifact) and it drags in Hamcrest (a jar artifact): Hamcrest becomes a transitive dependency of your project.

This is a powerful tool for constructing specifications.  If we create a pom.xml whose stanza contains API jars and whose element is of type pom, then if we depend on this newly created artifact of type pom, we’ll get all those API jars as transitive dependencies. Here’s an example, showing only the important bits of such a pom.xml:


<groupId>com.foobar</groupId>
<artifactId>cdi-specification-pom</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<dependencies>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.1-b04</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>2.0</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
</dependencies>

view raw

01.pom.xml

hosted with ❤ by GitHub

One thing you’ll notice is that each is declared to be in compile scope. I could have omitted this line (in which case compile is assumed to be the default) but I wanted to make it explicit. If I had used provided scope, then if you depended on this pom-type artifact its dependencies—the very jar files you’re interested in—would not appear in your project, since provided-scoped dependencies are not transitive!

Instead, we declare them as compile-scoped dependencies, and then you can depend on this pom-type artifact in provided scope! The net result is that all of its dependencies—the jars you’re interested in—will show up in your project as transitive provided-scoped dependencies: exactly what we want! We replace twenty-odd lines of boilerplate with about four.

So let’s refine our definition of what a specification is (for the purposes of this discussion): it’s a pom.xml:

  • whose packaging type is pom
  • whose dependency elements are in compile scope
  • whose dependencies are “API jars” as sketchily defined above and their dependencies

Here’s a partial example showing what I mean. Suppose the com.foobar:cdi-specification-pom:pom artifact is essentially the pom.xml listed above. Then if you wanted to use it as a specification, you could simply do:


<dependencies>
<dependency>
<groupId>com.foobar</groupId>
<artifactId>cdi-specification-pom</artifactId>
<version>0.0.1-SNAPSHOT</version>
<type>pom</type>
<scope>provided</scope>
</dependency>
</dependencies>

view raw

02.pom.xml

hosted with ❤ by GitHub

Now you’ll get javax.el:el-api:3.0.0:jar, javax.inject:javax.inject:1:jar, javax.interceptors:interceptor-api:1.2:jar and javax.annotation:javax.annotation-api:jar as transitive provided-scoped dependencies.

In the next post, I’ll cover the idea of environments—runtime implementations of these specifications also expressed as pom.xml files.