Control your 3pp:s with maven

By | September 20, 2013

In my assignment I recently got the task to look over the maven build system in a large application that consist of more then 10 projects.

All this projects have dependencies to each other (no circular one) and to various 3pp libraries like spring.

Maven dependency management

To get control over the system, the architects wants all compile time libraries to be fully visible in every project. To try to manage this all 3pp:s have been declared as optional and every 3pp have exclusions in its definition to prevent all transitive dependencies to be inherited in the next project.

The problem with this is that it works fine when building and deploying on the server, it is harder for the developer that tries to run unit-test locally. It also makes it harder to know exactly what 3pp are being used in the application when it needs to be explicitly defined when building the full application for deployment. So way have this emerge in the first place? Lets look at the maven site introduction to dependency mechanism.

Maven states that:

(*) Note: it is intended that this should be runtime scope instead, so that all compile dependencies must be explicitly listed – however, there is the case where the library you depend on extends a class from another library, forcing you to have available at compile time. For this reason, compile time dependencies remain as compile scope even when they are transitive.

Also according to limiting a transitive dependency to runtime scope in maven it’s not possible to get transitive dependencies to run-time scope.

This means that any transitive dependency on a compile time dependency will be available to dependent modules in compile time. This may seen fine, but it cases some mayor flow when trying to get control over all 3pp:s in the system and where they are used.

So to begin getting control over the dependencies we need to get to work.
I like the thought of having nice plain import where no 3pp can add new dependencies. So we will still have this managed in the dependency settings of a base definition pom. This can simply be done in a BaseDependency pom that defines all compile and run-time dependencies.

<project>
  ...
  <properties>
    <version.spring>3.2.3-RELEASE</version.dpring>
    ...
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${version.spring}<version>
        <exclusions>
          <!-- Exclude all implicit dependencies for here and make them explicit -->
          ...
        </exclusions>
      </dependency>
      ...
    </dependencies>
  </dependencyManagement>
</project>

In the first version all dependencies are inside this pom also pure test dependencies.

Pure test dependencies

Pure test dependencies can be managed in its own base pom to separate things and make it easier to access. So lets begin by defining a new module and naming it TestDependencies. Add all 3pp:s here and lets define them by setting the version as a properties in the beginning of the pom.

<project>
  ...
  <properties>
    <version.junit>4.11</version.junit>
    ...
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${version.junit}<version>
      </dependency>
      ...
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </dependency>
  </dependencies>
</project>

Add all pure test dependencies to the compile scope, this is important otherwise it will fail during compilation. We are going to add this project as a dependency in the following way to all other projects:

<project>
  ...
  <dependencies>
    ...
    <dependency>
      <groupId>se.aidium.test</groupId>
      <artifactId>TestDependencies</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
      <type>pom</type>
    </dependency>
  </dependencies>
</project>

This makes the dependencies be used in the test compile and run phase, but they will not be inherited as a transitive dependency to other projects. This gives us a perfect opportunity to remove all pure test dependencies from the project structure. So now we can concentrate on the real dependencies that the application needs to run in production.

Transitive 3pps

Now the problem is that we want compile time dependencies to be transitive run-time dependencies when its used. Also if we enables transitive dependencies we must also fix so that the war:s don’t includes all dependencies in its own library. This can by done by making them skinny. Skinny wars can be achieved in two ways. Either by the war plugin or by the ear plugin. The ear plugin would be the best choice, but the m2e plugin does not support that according to issue MECLIPSEWTP-222. This makes it impractical to use when the development environment must work as close as production as possible. So we must use the war plugin to making them skinny.

That is achieved by declaring what should be included in the war by using the includes or exclude statement. The exclude is cleaner when all dependent jars should be removed from the war. Also the manifest file class-path must be updated with the 3pp:s that the war needs and that is included in the war. This can be achieved by using the following configuration for the war plugin:

<project>
  ...
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-war-plugin</artifactId>
        <configuration>
          <!-- Use on of the following depending on if we need some specific jars or not -->
          <packagingExcludes>WEB-INF/lib/*.jar</packagingExcludes>
          <packagingIncludes>WEB-INF/lib/my-tag-library.jar,**/*.xml,**/*.properties,**/*.class,**/*.png,**/*.css,**/*.js,**/*.jsp</packagingIncludes>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
              <!-- This assumes that all dependent jars are placed in the EAR lib directory-->
              <classpathPrefix>lib/</classpathPrefix>
            </manifest>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>
  ...
</project>

So now we can begin to remove all optional dependency declarations. Making the dependencies transitive. Still we have one problem left. That is that the transitive dependencies are in the compile scope.

Now I have some solutions to this. The first is to change mavens source to include an option to change this. There is actual a jira task for this MNG-2589. I can think of multiple way of achieving this. A property -DcompileTransitiveScope=runtime, a tag <compileTransitiveScope>runtime</compileTransitiveScope> in the dependency plugin configuration or a tag <transitiveScope>runtime</transitiveScope> for each artifact.

The second is to declare all compile time dependency optional. This make them not transitive. So in the next level when they are used we can include them in the run-time scope and let them be transitive from this point on.

So we are choosing between a rock and a hard place here. We could either contribute by submitting a patch to the maven project or bite the bullet and declaring every thing twice (it’s still better then always need to declare it everywhere). To patch maven seems unlikely if we don’t do it in our spare time, because our customer will likely not paying for it. So we have to go with option two. It going to be a bit of work but in the end it will be worth it.

One thing that we also can do is to use the dependency:analyze and dependency:analyze-dep-mgt goals for maven in our continuous integration environment. This can check for unused and missing dependencies and mark the build as a failure if we find such things. This is a good complement to the build system and helps us to manage the dependencies a little bit more.

References:
Introduction to dependency mechanism
Maven dependency plugin
Limiting a transitive dependency to runtime scope in maven
Introduction to m2eclipse
Maven dependency best practices
Apache Maven 3 Cookbook

Jira:
MECLIPSEWTP-222
MNG-2589

One thought on “Control your 3pp:s with maven

  1. Pingback: Revisiting dependency management with Maven | Aidium

Leave a Reply

Your email address will not be published.