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:jar
, javax.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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
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
:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
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 ispom
- whose
dependency
elements are incompile
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
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.
One thought on “Maven Specifications and Environments: Part 0”
Comments are closed.