问题
- I have a legacy Java 5 project that will still be maintained until at least 31 December 2017.
- My default system JDK is 1.8.
- JDK 1.5 is also installed, as described in Install Java 5 in Ubuntu 12.04 and https://askubuntu.com/questions/522523/how-to-install-java-1-5-jdk-in-ubuntu.
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.
回答1:
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
回答2:
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.
来源:https://stackoverflow.com/questions/40042878/override-annotation-on-implemented-method-of-interface-in-java-5-code-doesnt-g