3

The POM contains (as described in https://stackoverflow.com/a/22398998/766786):

<profile>
  <id>compileWithJava5</id>
  <!--
    NOTE
    Make sure to set the environment variable JAVA5_HOME
    to your JDK 1.5 HOME when using this profile.
  -->
  <properties>
    <java.5.home>${env.JAVA5_HOME}</java.5.home>
    <java.5.libs>${java.5.home}/jre/lib</java.5.libs>
    <java.5.bootclasspath>${java.5.libs}/rt.jar${path.separator}${java.5.libs}/jce.jar</java.5.bootclasspath>
  </properties>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
          <compilerArguments>
            <bootclasspath>${java.5.bootclasspath}</bootclasspath>
          </compilerArguments>
        </configuration>
      </plugin>
    </plugins>
  </build>
</profile>

$JAVA5_HOME is set:

• echo $JAVA5_HOME
/usr/lib/jvm/jdk1.5.0_22

As far as I understand the magic that is Java+Maven, this should be a valid incantation of the maven-compiler-plugin to instruct JDK 1.8 to pretend to be JDK 1.5 and use the Java 5 boot classpath.


According to Why is javac failing on @Override annotation, JDK 1.5 will not allow @Override on implemented methods of an interface, only on overridden methods present in a super class.

In this commit the @Override annotation is used on the implemented method of an interface, so this is invalid Java 5 code:

private static class DummyEvent implements PdfPTableEvent {

    @Override
    public void tableLayout(PdfPTable table, float[][] widths, float[] heights, int headerRows, int rowStart, PdfContentByte[] canvases) {
    }
}

When I run

mvn clean compile test-compile -P compileWithJava5

I don't get a compilation error on the class that contains the @Override annotation. What am I missing here?

(Already tried: Animal Sniffer Maven Plugin, but that plugin doesn't look at compilation flags, only at the byte code.)


EDIT: This is what I currently have in my POM.

<profile>
  <id>compileWithLegacyJDK</id>
  <!--
    NOTE
    Make sure to set the environment variable JAVA5_HOME
    to your JDK 1.5 HOME when using this profile.
  -->
  <properties>
    <java.version>1.5</java.version>
    <java.home>${env.JAVA5_HOME}</java.home>
    <java.libs>${java.home}/jre/lib</java.libs>
    <java.bootclasspath>${java.libs}/rt.jar${path.separator}${java.libs}/jce.jar</java.bootclasspath>
  </properties>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.3</version>
        <configuration>
          <source>${java.version}</source>
          <target>${java.version}</target>
          <compilerArguments>
            <bootclasspath>${java.bootclasspath}</bootclasspath>
          </compilerArguments>
          <compilerVersion>${java.version}</compilerVersion>
          <fork>true</fork>
          <executable>${java.home}/bin/javac</executable>
        </configuration>
      </plugin>
    </plugins>
  </build>
</profile>

Run with

export JAVA5_HOME=/var/lib/jenkins/tools/hudson.model.JDK/1.5
mvn compile test-compile -P compileWithLegacyJDK

See accepted answer below for more details.

Community
  • 1
  • 1
Amedee Van Gasse
  • 7,280
  • 5
  • 55
  • 101
  • Show the ralated code please – Jens Oct 14 '16 at 12:14
  • 1
    You're misunderstanding `source` and `target`. You need to set a bootclasspath to detect issues like this: http://stackoverflow.com/a/25273329/1743880 – Tunaki Oct 14 '16 at 12:22
  • @Jens I have added a link to the related code, and the part of the POM that tells Java to behave like Java 5. I hope that the question is more clear now. – Amedee Van Gasse Oct 18 '16 at 09:25
  • 1
    Hmm that is weird. I'll try to setup something like this, and reproduce the results – Tunaki Oct 18 '16 at 10:02
  • Feel free to clone the repo linked in the question. The `@Override` annotation was added in `TableEventTest.java`, and removed in a later commit after manual review of the code. It should have been caught earlier by the CI. – Amedee Van Gasse Oct 18 '16 at 10:08

2 Answers2

7

The core of the issue: Maven is still compiling your code with the JDK with which it is launched. Since you're using JDK 8, it is compiling with JDK 8, and to compile with another compiler, you need to use toolchains or specify the path to the right JDK.

Set up

To test this answer, you can have a simple Maven project with the following POM

<?xml version="1.0" encoding="UTF-8"?>
<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>test</groupId>
  <artifactId>test</artifactId>
  <version>1.0-SNAPSHOT</version>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.3</version>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
          <compilerArguments>
            <bootclasspath>/usr/lib/jvm/jdk1.5.0_22/jre/lib/rt.jar</bootclasspath>
          </compilerArguments>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

with a single class to compile sitting under src/main/java/test, being:

package test;

interface I {
  void foo();
}
public class Main implements I {
    public static void main(String[] args) {
        new Main().foo();
    }

    @Override
    public void foo() {
        System.out.println("foo");
    }
}

This looks like a standard Maven project configured to use JDK 5. Notice that the class uses @Override on a method implementing an interface. This was not allowed before Java 6.

If you try to build this project with Maven running under JDK 8, it will compile, despite setting <source>1.5</source>.

Why does it compile?

The Maven Compiler Plugin is not at fault. javac is to blame. Setting the -source flag does not tell javac to compile your project with this specific JDK version. It instructs javac to accept only a specific version of source code. From javac documentation:

-source release: Specifies the version of source code accepted.

For example, if you specified -source 1.4, then the source code you're trying to compile cannot contain generics, since those were introduced to the language later. The option enforces the source compatibility of your application. A Java application that uses Java 5 generics is not source compatible with a Java 4 program using a JDK 4 compiler. In the same way, an application using Java 8 lambda expressions is not source compatible to a JDK 6 compiler.

In this case, @Override is an annotation that was already present in Java 5. However, its semantics changed in Java 6. Therefore, code using @Override, whether it is on a method implementing an interface or not, is source compatible with a Java 5 program. As such, running a JDK 8 with -source 1.5 on such a class will not fail.

Why does it run?

Onto the second parameter: target. Again, this isn't a Maven Compiler concern, but a javac one. While the -source flag enforces source compatibility with an older version, -target enforces binary compatibility with an older version. This flag tells javac to generate byte code that is compatible with an older JVM version. It does not tell javac to check that the compiled code can actually run with the older JVM version. For that, you need to set a bootclasspath, which will cross-compile your code with a specified JDK.

Clearly, @Override on a method implementing an interface cannot run on a Java 5 VM, so javac should bark here. But nope: Override has source retention, meaning that the annotation is completely discarded after compilation has happened. Which also means that when cross-compilation is happening, the annotation isn't there anymore; it was discarded when compiling with JDK 8. As you found out, this is also why tools like the Animal Sniffer Plugin (which enables an automatic bootclasspath with pre-defined JDK versions) won't detect this: the annotation is missing.

In summary, you can package the sample application above with mvn clean package running on JDK 8, and run it without hitting any issues on a Java 5 JVM. It will print "foo".

How can I make it not compile?

There are two possible solutions.

The first, direct one, is to specify the path to javac through the executable property of the Compiler Plugin:

<plugin>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.3</version>
  <configuration>
    <source>1.5</source>
    <target>1.5</target>
    <compilerArguments>
      <bootclasspath>/usr/lib/jvm/jdk1.5.0_22/jre/lib/rt.jar</bootclasspath>
    </compilerArguments>
    <compilerVersion>1.5</compilerVersion>
    <fork>true</fork>
    <!-- better to have that in a property in the settings, or an environment variable -->
    <executable>/usr/lib/jvm/jdk1.5.0_22/bin/javac</executable>
  </configuration>
</plugin>

This sets the actual version of the JDK the compiler should use with the compilerVersion parameter. This is a simple approach, but note that it only changes the JDK version used for compiling. Maven will still use the JDK 8 installation with which it is launched to generate the Javadoc or run the unit tests, or any step that would require a tool for the JDK installation.

The second, global, approach, is to use a toolchains. These will instruct Maven to use a JDK different than the one used to launch mvn, and every Maven plugins (or any plugin that is toolchains aware) will then use this JDK to perform their operation. Edit your POM file to add the following plugin configuration of the maven-toolchains-plugin:

<plugin>
  <artifactId>maven-toolchains-plugin</artifactId>
  <version>1.1</version>
  <executions>
    <execution>
      <goals>
        <goal>toolchain</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <toolchains>
      <jdk>
        <version>1.5</version>
      </jdk>
    </toolchains>
  </configuration>
</plugin>

The missing ingredient is telling those plugins where the configuration for that toolchain is. This is done inside a toolchains.xml file, that is generally inside ~/.m2/toolchains.xml. Starting with Maven 3.3.1, you can define the location to this file using the --global-toolchains parameter, but best to keep it inside the user home. The content would be:

<toolchains>
  <toolchain>
    <type>jdk</type>
    <provides>
      <version>1.5</version>
    </provides>
    <configuration>
      <jdkHome>/usr/lib/jvm/jdk1.5.0_22</jdkHome>
    </configuration>
  </toolchain>
</toolchains>

This declares a toolchain of type jdk providing a JDK 5 with the path to the JDK home. The Maven plugins will now use this JDK. In effect, it will also be the JDK used when compiling the source code.

And if you try to compile again the sample project above with this added configuration... you'll finally have the error:

method does not override a method from its superclass

Tunaki
  • 132,869
  • 46
  • 340
  • 423
  • Looks good. Preliminary testing on my local workstation gives me the expected `method does not override a method from its superclass`. As soon as I'm done implementing this on the Jenkins server, I am going to accept the answer and award you the bonus. I would like to thank you for the well written answer! – Amedee Van Gasse Oct 19 '16 at 12:37
  • 1
    I made a small edit @Amedee because I remembered that you can configure _just_ the compiler to use the specific JDK version, and not all of the plugins (like Javadoc). In practice, using a toolchains would be cleaner since it makes sure the same JDK is used throughout the build. – Tunaki Oct 19 '16 at 18:29
  • 1
    I chose to configure *just* the compiler because a) no changes to `toolchains.xml` required (any user should be able to clone the repo and build, without much config on their system) b) I actually *want* to use JDK 8 in other parts of the build, for example for javadocs. But I will keep it in mind and maybe others can use it. I will award you the bounty because this is a well-written answer that deserves to be a canonical answer. In addition to your answer, I'm still putting it in a separate profile, that only runs on the CI server (as in my original question). – Amedee Van Gasse Oct 21 '16 at 07:26
  • The funny thing is, just after I set the bonus, I figured out for myself what you wrote about **Why does it compile?** and **Why does it run?**, thanks to this blog post: http://kohsuke.org/2012/01/27/override-and-interface/. Thanks to you I learned about toolchains. – Amedee Van Gasse Oct 21 '16 at 07:39
1

Setting target to 1.5 when you use JDK 1.8 does not guarantee that your code will work on 1.5 as explained in the doc of the maven-compiler-plugin.

Merely setting the target option does not guarantee that your code actually runs on a JRE with the specified version. The pitfall is unintended usage of APIs that only exist in later JREs which would make your code fail at runtime with a linkage error. To avoid this issue, you can either configure the compiler's boot classpath to match the target JRE or use the Animal Sniffer Maven Plugin to verify your code doesn't use unintended APIs.

Nicolas Filotto
  • 43,537
  • 11
  • 94
  • 122
  • I am going to try out the Animal Sniffer Maven Plugin, if that works as advertised, then I will accept this answer. I would like to avoid cross-compiling because we're an Open Source project, and I do not want to tell our users that they should install more than one JDK to be able to compile our product. – Amedee Van Gasse Oct 14 '16 at 12:30
  • Last update was 8 months ago, which is alive enough for something relatively stable like a mature Maven plugin. – Amedee Van Gasse Oct 14 '16 at 12:38
  • I'm not sure what your question is anymore @AmedeeVanGasse. Do you want code having `@Override` on methods implementing interfaces to compile or not? Your question is asking why it _seems_ to compile with Java 5, but the answer is, it does not and the Animal Sniffer Plugin will raise you an error (just like `javac` would if you set a `bootclasspath`). And if you want such code to compile and run, you'll have to use a JDK >= 6. – Tunaki Oct 14 '16 at 12:41
  • @Tunaki he wants to see a build failure so he should get want he expects with the Animal Sniffer Plugin – Nicolas Filotto Oct 14 '16 at 12:46
  • I don't care if it's a compilation error or another error. I just want to see something red in Jenkins (failed build) so I know which developer I need to annoy. – Amedee Van Gasse Oct 14 '16 at 12:49
  • 2
    Okay, so Animal Sniffer is very interesting, but it does not detect this particular `@Override` thing, as mentioned in this issue: https://github.com/mojohaus/animal-sniffer/issues/14. Which unfortunately means that I will have to do the ugly cross-compiling hack. – Amedee Van Gasse Oct 17 '16 at 08:35
  • 1
    @Tunaki *"Do you want code having `@Override` on methods implementing interfaces to compile or not?"* --> No, I do **not** want that to compile. I want to get a compilation error. This particular project **must** remain compatible with JDK==1.5. – Amedee Van Gasse Oct 18 '16 at 09:28