Saturday, February 17, 2018

How to configure Maven projects to use a specific JDK version?

Hello friends, today the scope of our discussion would be related to configuring Maven project and  cross-compilation feature of Java. Precisely we would talk about below points:
  • the right way of configuring Maven projects to use specific JDK version
  • important pointers related to Java compiler & it's cross compilation feature 
  • common misconception while configuring the Maven compiler plugin 


By default, Maven uses JAVA_HOME environment variable to find out which JDK version to use independent of platform i.e Linux or Windows, provided there are no special configuration/ plugin used in one's project POM.xml. 

One question might be bothering all of you:
In single system, we can have only one JAVA_HOME environment variable then how does it possible to compile multiple projects using a different JDK? As in real world, it's very common & practical requirement to have multiple projects which needs to be run on different JDK environment. For example: in the build / integration server, projects must be configured to compile using different JDK version i.e. either in JDK 6 or JDK 7 or JDK 8 etc.

We will talk about the "How" in moment but before that some important pointers related Java compiler & it's cross-compilation feature to keep in mind. It will be needed later what we are going to discuss.

2. Important Pointers 


Before starting, get some of the facts straight: 
  1. By default, classes are compiled against the bootstrap(the rt.jar library) and extension classes(lib\ext) of the platform that javac shipped with i.e. using the JDK pointed by JAVA_HOME environment variable. The compiler doesn't have  any knowledge of Java API [rt.jar & lib\ext].
  2. Want to use javac cross-compilation feature: To correctly generate class files for specific JRE version, one should always compile their source files against a target JRE bootstrap & extension classes by using -bootclasspath & -extdirs option along with -target or -source option. Let's understand this by an example:

    Suppose JAVA_HOME points to JDK 8 that means by default JDK 8 compiler i.e.  javac compiles our source code. Our objective is to generate classes that will run on a 1.7 VM.
    How will we achieve this:
    javac -source 1.7 -target 1.7 -bootclasspath \jdk1.7.0\jre\lib\rt.jar \ 
    -extdirs "\jdk1.7.0\jre\lib\ext\*.jar" SourceCode.java
    
    where 
    * The -target 1.7 option ensures that the generated class files will be compatible with the 
      specified target i.e. 1.7 VMs and on later versions, but not on earlier versions of the VM.
    * The -source 1.7 option specifies the version of source code accepted here the compiler 
       accepts code with features introduced in Java SE 7.
    

    As stated in point 1, if we don't explicitly provide -bootclasspath & -extdirs then source files are compiled against JDK 8 bootstrap  classes. We need to tell javac to compile against JDK 1.7 bootstrap instead. If we don't do this then this might allow compilation against a Java 8 API that would not be present on a 1.7 VM and would fail at runtime.  
  3. Projects compiled using higher JDK version can't be run on lower JRE version if one don't use Java cross-compiling feature(use of -target option) [classes compiled using Java 1.8 compiler & without using cross-compilation feature, if run on a lower JRE version  then it will give Unsupported major.minor version error]

3. Misconception 


Adding Maven compiler related properties or merely setting the target or source option in the Maven Compiler Plugin will be enough to compile projects to a different JDK version than what we are currently using. But it doesn't guarantee that:
  • our code actually runs on a JRE with the specified version [target option] or 
  • our code actually compiles on a JDK with the specified version [source option] 

As stated earlier in important pointers section, while using cross compilation feature one also have to use  -bootclasspath and -extdirs, but I haven't find any related out of box configuration parameter in Maven documentation that could be used with Maven Compiler plugin. Therefore I wouldn't recommend using below approaches:
  • By adding two following properties
    <project>
      [...]
      <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
      </properties>
      [...]
    </project>
    
  • By Configuring the Maven Compiler Plugin directly
    <project>
      [...]
      <build>
        [...]
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.7.0</version>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
          </plugin>
        </plugins>
        [...]
      </build>
      [...]
    </project>
As per Maven documentation also, to avoid this issue, one can either configure the compiler's boot classpath to match the target JRE [but told you, haven't got any out of box option ] or use the Animal Sniffer Maven Plugin to verify their code doesn't use unintended APIs.

4. The Right Way: Compiling Projects using a different JDK


There are two ways to achieve this. Either using the Maven toolchains or by configuring the Maven Compiler plugin properly. Both the approaches have some similarity like
  • Both involves specifying the path to the right JDK for a project. 
  • Both allows projects to be built using a specific version of JDK independent from the one Maven is running with.
But the preferable way is to use Maven toolchains because:
  • It allows us to share configuration across different plugins. For example: one can make sure that the plugins like compiler, surefire, javadoc, webstart etc. all use the same JDK for execution. 
  • Similarly to maven-enforcer-plugin, it also allows us to control environmental constraints in the build.

Let's proceed our discussion by stating How to use above approaches:
  1. Using Maven Toolchains
    Toolchains will only work in Maven 2.0.9 and higher versions. There are many plugins available in the market that are toolchains aware like maven-compiler-plugin, maven-jarsigner-plugin, maven-javadoc-plugin, maven-surefire-plugin, animal-sniffer-maven-plugin etc.

    To get started with toolchains, one has to follow two steps:
    - Add the maven-toolchains-plugin in the project POM
    <plugins>
     ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-toolchains-plugin</artifactId>
        <version>1.1</version>
        <executions>
          <execution>
            <goals>
              <goal>toolchain</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <toolchains>
            <jdk>
              <version>1.8</version>
            </jdk>
          </toolchains>
        </configuration>
      </plugin>
      ...
    </plugins>
    
    Create configuration file i.e. toolchains.xml on the building machine
    Here we need to set the installation paths of our toolchains like JDK path.This file should be placed in the ${user.home}/.m2 directory. But starting with Maven 3.3.1, one can put the toolchains.xml file wherever they like by using the --toolchains file or -t file option but it is recommended to locate it into ${user.home}/.m2/.
    <?xml version="1.0" encoding="UTF8"?>
    <toolchains>
      <!-- JDK toolchains -->
      <toolchain>
        <type>jdk</type>
        <provides>
          <version>1.7</version>
        </provides>
        <configuration>
          <jdkHome>C:\Program Files (x86)\Java\jdk1.7.0</jdkHome>
        </configuration>
      </toolchain>
      <toolchain>
        <type>jdk</type>
        <provides>
          <version>1.8</version>
        </provides>
        <configuration>
          <jdkHome>C:\Program Files\Java\jdk1.8.0_144</jdkHome>
        </configuration>
      </toolchain>
    </toolchains>
    
  2. Configuring the Compiler Plugin
    One need to set few optional configuration parameter of Maven Compiler plugin and we are good to go like executable, fork, compilerVersion etc. Don't forget to set fort to true along with the executable otherwise it won't work.
    <project>
      [...]
      <build>
        [...]
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.7.0</version>
            <configuration>
              <verbose>true</verbose>
              <fork>true</fork>
              <executable><!-- path-to-javac --></executable>
              <compilerVersion>1.8</compilerVersion>
            </configuration>
          </plugin>
        </plugins>
        [...]
      </build>
      [...]
    </project>
    
    You must be wondering how to avoid hard-coding a file system path for the executable as i's bad practice. Don't worry Maven has got you covered there also. One have to use a property in the place of executable and set this property in the project specific or global setting.xml file.

    - Using the property
    <executable>${JAVA_1_8_HOME}/bin/javac</executable>
    
    - Setting the Property
    <settings>
      [...]
      <profiles>
        [...]
        <profile>
          <id>compiler</id>
            <properties>
              <JAVA_1_8_HOME>C:\Program Files\Java\jdk1.8.0_144</JAVA_1_8_HOME>
            </properties>
        </profile>
      </profiles>
      [...]
      <activeProfiles>
        <activeProfile>compiler</activeProfile>
      </activeProfiles>
    </settings>
    

5. Conclusion


When all of the Maven based projects are using the same JDK version then one don't require any extra configurations in their project POM. Maven will use the JDK pointed by JAVA_HOME environment variable. But if there are multiple projects which requires compilation using different JDK version or needs to run on different JVM version then one should always configure their project by using Maven toolchains or by using Maven compiler plugin. This allows project to be built on specific version of JDK independent from the one, Maven is running with. Needless to say, one will save extra effort in making Jenkins configuration related to JDK for each project separately. 

Thank you for reading this article. Feel free to connect with me for any queries and suggestion. See you soon. Happy Learning !!

References

No comments:

Post a Comment