JavaOne – Apache Maven and its Impact on Java 9

“Apache Maven and its Impact on Java 9”

Speaker: Robert Scholte

For more blog posts from JavaOne, see the table of contents


Up until Java 8, Maven goes fine on top of Java. In Java 9, there is overlap.

Review of Maven

  • Dependency management
  • All about pom.xml
  • All about classpaths
  • Generate project information

Module path mismatch

  • Classpath accepts – output dirs, jar files and zip files (Maven doesn’t support zip files)
  • Module path accepts output directories, module output directories and directories with jars [and jars; keep reading]
  • Options included pointing to diretory containing the jar (but contains other things to), symlins (windows unhappy), create lib directory and copy files (a lot of IO)
  • Java 9 team added specifying direct jar files per Maven’s request

Module names

  • Like problem with group id/artifact id
  • Need to be unique
  • Learned over 30K artifact ids end with a number. This was a problem for automatic module names because many woul have same name creating collision. ex: commons-lang2 and commons-lang3
  • Also, many artifact ids are naming conflicts if ignore group id. Which is problematic for automatic module names when use two jars that collide
  • Made difference between library builders and app builders. Standard that library builders should never refer to automatic modules. If no exports, can assume it is an application. And apps have control of what gets used.
  • Maven gives warning if library refers to automatic module usage. (use Maven 3.+ so warning shows up in color

Dependencies not yet modular

  • If you are dependent on open source library and it is dependent on one that isn’t modular (or the depenency changed the module name), the direct dependency can’t migrate to modules yet
  • Solution: use Automatic-Module-Name to manifest of libraries so not using algorithm. A good bridge

Tips

  • Point of no return – each time you refer to a dependency that is a module, you can’t go back to a previous version that does not
  • Don’t have split packages if can (Maven itself has a split package issue for old compatibility classes)
  • Don’t have classs in the root package
  • Provide your future module name in the manifest
  • Use a unique module name
  • Don’t change folder structure to add module name
  • plexus-java can help plugins build path
  • Classpath isn’t going away. The older the project, the harder it will likely be to migrate to modules

release tag
In addition to source/target 1.9, you can specify <release> to indicate compatiblity
Can use animal-sniffer to determine if code compiled on a newer version of Java in an older JDK mode will ctually run.

Module file
Maven doesn’t generate. Requires human input. Not all modules are dependencies. jdeps can do it because has access to binary files. But need before compile at build time so not compiling twice

My take: Interesting problems and solutions. I feel like he takeaway is “don’t upgrade to Java 9 yet”. The font was small on the screen. Even when there aren’t a lot of words on the screen

upgrading to junit 5 parameterized tests for the first time

I’ve been playing with JUnit 5 for a while. Migrating existing project is something else so I decided to try with my small project cert objectives checker. This project does run on Jenkins so is a good test because it isn’t just on my machine. First I set up Eclipse and Maven to work with JUnit 5.

Switch to Jupiter APIs

Since I only had 5 classes, I updated by hand. For each class:

  1. Changed imports to
    import static org.junit.jupiter.api.Assertions.*;
    import static org.hamcrest.MatcherAssert.assertThat;
    import org.junit.jupiter.api.*;

    (The hamcrest one is so I can keep using assertThat)

  2. Changed assertEquals/assertTrue/etc calls to have String message as last parameter instead of first.
  3. Changed @Before to @BeforeEach
  4. Got distracted and updated tests to Java 8 syntax (nothing to do with JUnit)
  5. Changed to use new ParameterizedTest syntax. More on this…

Parameterized tests

Interestingly Eclipse knows about the new Parameterized Tests dependency natively. But if you use Maven, you need to add a dependency:

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-params</artifactId>
  <version>5.0.0-RC2</version>
</dependency>

Now let’s compare the before and after. I’ve omitted parts that are the same in both (aka the detailed logic of my test). Not that it is secret of anything. (After all the project is public on github.) But it makes it easier to compare the differences with less code.

JUnit 4 version

import static org.junit.Assert.*;

import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;

import javax.xml.parsers.*;

import org.junit.*;
import org.junit.runner.*;
import org.junit.runners.*;
import org.junit.runners.Parameterized.Parameters;
import org.w3c.dom.*;

@RunWith(Parameterized.class)
public class CheckForChangesIT {

  @Parameters
  public static List<CertsToCheckEnum[]> suite() {
    List<CertsToCheckEnum[]> result = new ArrayList<>();
    for (CertsToCheckEnum element : CertsToCheckEnum.values()) {
       result.add(new CertsToCheckEnum[] { element });
    }
    return result;
  }
  // ----------------------------------------------------
  private CertsToCheckEnum certToCheck;
  private InputStream stream;
  private Document doc;

  public CheckForChangesIT(CertsToCheckEnum certToCheck) {
    this.certToCheck = certToCheck;
  }
  // ----------------------------------------------------
  @Before
  public void connect() throws Exception {
    String url = XML_URL + certToCheck.getExamNumber();
    stream = new URL(url).openStream();
  }
  @After
  public void close() {
    if (stream != null) {
      try {
        stream.close();
      } catch (Exception e) {
        // ignore
      }
    }
  }
  // ----------------------------------------------------
  @Test
  public void upToDate() throws Exception {
    parseDocument();
    String currentData = convertToString();
    assertSameAsExisting(currentData);
  }

JUnit 5 version

import static org.junit.jupiter.api.Assertions.*;

import java.io.*;
import java.net.*;
import java.nio.file.*;

import javax.xml.parsers.*;

import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import org.w3c.dom.*;

public class CheckForChangesIT {

  private CertsToCheckEnum certToCheck;
  private Document doc;
  // ----------------------------------------------------
  @ParameterizedTest
  @EnumSource(CertsToCheckEnum.class)
  public void upToDate(CertsToCheckEnum c) throws Exception {
    certToCheck = c;
    String url = XML_URL + certToCheck.getExamNumber();
    try (InputStream stream = new URL(url).openStream()) {
      parseDocument(stream);
      String currentData = convertToString();
      assertSameAsExisting(currentData);
    }
  }

Key differences between JUnit 4 and 5 Parameterized Test

  • The JUnit 5 annotation has a different name and is placed on the test rather than the class. This is good because it lets you have some parameterized tests in a class and some regular tests. With JUnit 4, I had to arbitrarily create another class for the non-parameterized parts.
  • JUnit 5 has a number of annotations for building the parameter list without having to write custom code.
  • JUnit 5 injects the parameter as a method parameter rather than an instance variable.
    • In many ways this is nice. For example, I am now using a try-with-resources to close the stream rather than creating in @BeforeEach and having ugly logic to clean it up later. I could have done this in JUnit 4 but it seemed less natural.
    • In the case of my “legacy” test, I use the previously injected instance variable all over the class.  Hence my hack of storing the local variable in the original instance variable as the first line of my test method.
  • The JUnit 5 version is a good bit shorter

One problem

I encountered a minor Eclipse bug with parameterized tests. So I reported it.

preparing to run junit 5 with eclipse and maven

I’ve been playing with JUnit 5 for a while. Since I’m going to be speaking about it at JavaOne, I decided to write all my tests using JUnit 5 between now and then. Plus we are getting close to JUnit 5’s official release; it’s pretty stable now.

Since it is more than just playing, I wanted my IDE to support it. And my Maven POM needed to know about both JUnit 4 and 5. It wasn’t hard.

Note if you are using IntellIJ, it works out of the box so you just have to set up Maven.

Update Eclipse

I also decided to update my Eclipse so I could run JUnit 5 tests in the workspace.  I had to let Eclipse update 3 other plugins to be compatible with the latest version. It was easy to do though and I didn’t need to re-install my custom plugins. In September (or so), Eclipse will release with JUnit 5 support in it.

Update Maven POM

Then I updated my pom to use the dependencies in the JUnit 5 Maven sample. I also added failsafe support and junit-jupiter-params dependency (for parameterized tests which I use a lot)

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>GROUP_ID_GOES_HERE</groupId>
  <artifactId>ARTIFACT_ID_GOES_HERE</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <properties>
    <java.version>1.8</java.version>
    <surefire.version>2.19.1</surefire.version>
    <junit.version>4.12</junit.version>
    <junit.jupiter.version>5.0.0-RC2</junit.jupiter.version>
    <junit.vintage.version>${junit.version}.0-RC2</junit.vintage.version>
    <junit.platform.version>1.0.0-RC2</junit.platform.version>
  </properties>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>${surefire.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>integration-test</goal>
              <goal>verify</goal>
            </goals>
          </execution>
        </executions>
        <dependencies>
          <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-surefire-provider</artifactId>
            <version>${junit.platform.version}</version>
          </dependency>
        </dependencies>
      </plugin>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.0</version>
        <configuration>
          <source>${java.version}</source>
          <target>${java.version}</target>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>${surefire.version}</version>
        <configuration>
          <includes>
            <include>**/Test*.java</include>
            <include>**/*Test.java</include>
            <include>**/*Tests.java</include>
            <include>**/*TestCase.java</include>
          </includes>
          <properties>
            <!-- <includeTags>fast</includeTags> -->
            <excludeTags>slow</excludeTags>
          </properties>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-surefire-provider</artifactId>
            <version>${junit.platform.version}</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>
  <dependencies>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-api</artifactId>
       <version>${junit.jupiter.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-params</artifactId>
       <version>${junit.jupiter.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <version>${junit.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.junit.platform</groupId>
       <artifactId>junit-platform-launcher</artifactId>
       <version>${junit.platform.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
       <version>${junit.jupiter.version}</version>
       <!-- docs don't have this as test scope; trying anyway to see what happens -->
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.junit.vintage</groupId>
       <artifactId>junit-vintage-engine</artifactId>
       <version>${junit.vintage.version}</version>
       <!-- docs don't have this as test scope; trying anyway to see what happens -->
       <scope>test</scope>
     </dependency>
     <!-- other dependencies my project needs like selenium go here -->
  </dependencies>
</project>

Problems I encountered (with solutions)

  1. Maven doesn’t run failsafe tests – Originally I didn’t have failsafe version 2.19.1 because I had copied the pom from the junit sample. And the JUnit sample didn’t have any integration tests so no reason to include failsafe.
  2. Eclipse doesn’t recognize migrated JUnit 5 tests – If you’ve ever run JUnit 4 tests against that project/test, Eclipse has a stored configure with the test runner set to JUnit 4. You can go to “run configurations” and either delete the existing configuration or change the runner to JUnit 5 for that configuration. I choose the later:

Remember that with this configuration, you are merely prepared to have JUnit 5 tests in your project. Since JUnit 5 is (mostly) backward compatible, the tests still work as is.