Maven Specifications and Environments: Part 2

In the previous post, we looked at the concept of environments—implementations of specifications (a concept discussed two posts ago).

When you have a Maven project that you want to be runtime-independent, but coded to a particular JCP specification, things get a little hairy. Your code must compile against an API jar (or API jars, or, if you’ve read this series of posts, a specification), but you want it to run at test time against a particular environment, and you want that environment to dictate which API jars should be used.

This sounds like a contradiction until we realize that Maven respects the order in which <dependency> elements are listed.

The TL;DR here is: when you are putting together your <dependency> elements, list them in your pom.xml in typically reverse order: list test-scoped dependencies first, then runtime-scoped dependencies, then provided-scoped dependencies and finally compile-scoped dependencies.

Why?

When you compile, none of the test-scoped dependencies will be “in scope”, so they will simply be ignored. Next on the classpath will be your provided-scoped dependencies followed by your compile-scoped dependencies. At compile time, none of this really matters: where your provided-scoped dependencies appear on your classpath is typically irrelevant.

But when you test, the order is suddenly quite important. If your test-scoped dependencies come first, then any transitive API jars that they pull in (for example) will be the ones that are actually used on your classpath—they’ll “come before” any provided-scoped API jars you have in order to support runtime environment-independent compilation. Those provided-scoped dependencies will still be on your classpath at test time, but they’ll never be referenced: the runtime environment you’re testing against should (must!) dictate its own API jars to be used, so those are the ones you’ll end up using.

To make this slightly more concrete, let’s say you have a CDI project and you want to be able to run it on either Weld or OpenWebBeans. Let’s say further that you’re going to run unit tests using Weld. Now recall that Weld uses the JBoss-authored version of javax.interceptor packages reified in the org.jboss.spec.javax.interceptor:jboss-interceptors-api_1.2_spec:jar artifact, not the javax.interceptor:javax.interceptor-api:jar artifact. To preserve runtime environment independence, you want to compile (let’s say) against the javax.interceptor:javax.interceptor-api:jar artifact, but when you test with Weld you want Weld to use whatever API jars it wants, not the runtime-environment-independent ones you happen to have selected and compiled against.

To do this, you’ll add Weld first in test scope, then javax.interceptor:javax.interceptor-api:jar in provided scope. When you compile, you’ll use javax.interceptor:javax.interceptor-api:jar; when you test you’ll end up transitively using javax.interceptor:javax.interceptor-api:jar.

When you expand this strategy to include environments and specifications as defined earlier in this series of posts, component-oriented development becomes much simpler.