Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to support different versions of main (and test) source sets for different Java versions (6, 7, 8)

I have a library project in Java. I want to implement, test, and probably release several versions of the project, intented to be used with different Java versions: 6, 7, 8.

The simpliest way is just to copy-paste project and support several source trees, but I want to avoid this because It's tedious and error-prone.

Another possible way is to factor "base" project, and several Java version specific projects depending. Versions differs very slightly, but I don't what to reflect this technical development issue in class hierarchy.

So I'm looking for

  • a kind of precompilers
  • and/or standard Maven options
  • and/or Maven plugins

which could help to support several Java version-specific versions of the library from a single source tree, and transparently for the lib users.

like image 292
leventov Avatar asked Oct 04 '14 17:10

leventov


People also ask

Can you have 2 different Java versions?

It is very possible to run multiple versions of Java on the same machine so you can run your existing applications and Ignition at the same time.

What is the difference between Java 8 and other versions?

Java 8 is a major update to the programming language which introduced a significant upgrade to the functional programming called the Lambda Expressions. Java 8 also gets a new and improved Date/Time API, an enhanced JavaScript engine, new streaming API. Concurrent accumulators, secure random generation, and much more.

Can program developed with Java 7 be run on Java 8?

Binary Compatibility Except for the noted incompatibilities, class files built with the Java SE 7 compiler will run correctly in Java SE 8. Class files built with the Java SE 8 compiler will not run on earlier releases of Java SE.


7 Answers

You can generate one jar for each Java version (6, 7, 8) from a single pom.xml file.

All the relevant work takes place in the maven-compiler-plugin:compile mojo.

The trick is to execute the mojo 3 times, each time writing the resulting file to a different outputFileName. This will cause the compiler to run multiple times, each time using different versions and spitting an appropriately named file.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <executions>
        <execution>
            <id>compile-1.6</id>
            <goals>
                <id>compile</id>
            </goals>
            <phase>compile</phase>
            <configuration>
                <source>1.6</source>
                <target>1.6</target>
                <executable>${env.JAVA_HOME_6}/bin/javac</executable>
                <outputFileName>mylib-1.6.jar</outputFileName>
            </configuration>
<!-- START EDIT -->
            <dependencies>
                <dependency>
                    <groupId>com.mycompany</groupId>
                    <artifactId>ABC</artifactId>
                    <version>1.0</version>
                </dependency>
            </dependencies>
<!-- END EDIT -->
        </execution>
        <execution>
            <id>compile-1.7</id>
            <phase>compile</phase>
            <goals>
                <id>compile</id>
            </goals>
            <configuration>
                <source>1.7</source>
                <target>1.7</target>
                <executable>${env.JAVA_HOME_7}/bin/javac</executable>
                <outputFileName>mylib-1.7.jar</outputFileName>
            </configuration>
<!-- START EDIT -->
            <dependencies>
                <dependency>
                    <groupId>com.mycompany</groupId>
                    <artifactId>XYZ</artifactId>
                    <version>2.0</version>
                </dependency>
            </dependencies>
<!-- END EDIT -->
        </execution>

        <!-- one more execution for 1.8, elided to save space -->

    </executions>
</plugin>

Hope that helps.

EDIT

RE: additional requirement that each run compile against different sources.

See edits to pom snippet above.

Each execution can define its own dependencies library list.

So JDK6 build depends on ABC.jar but JDK7 depends on XYZ.jar.

like image 177
333kenshin Avatar answered Nov 03 '22 09:11

333kenshin


You can conditionally include some source directories by creating separate profiles for each java version. Than you can run maven with profile name which will define which sources for which version you would like to use. In this case all common sources remain in src/main/java, while java version dependant files are placed in /src/main/java-X.X directories.

<profiles>
    <profile>
        <id>java-6</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>build-helper-maven-plugin</artifactId>
                    <version>1.5</version>
                    <executions>
                        <execution>
                            <id>add-sources</id>
                            <phase>generate-sources</phase>
                            <goals>
                                <goal>add-source</goal>
                            </goals>
                            <configuration>
                                <sources>
                                    <source>${basedir}/src/main/java-1.6</source>
                                </sources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
    <profile>
        <id>java-7</id>
        (...)
                                    <source>${basedir}/src/main/java-1.7</source>
    </profile>
    <profile>
        <id>java-8</id>
        (...)
                                    <source>${basedir}/src/main/java-1.8</source>
    </profile>
</profiles>

You can probably do this even more dynamic by replacing hardcoded java-X.X by property which you will pass to maven together with profile. This would be something like:

    <profile>
        <id>conditional-java</id>
        (...)
                                    <source>${basedir}/src/main/java-${my.java.version}</source>
    </profile>
</profiles> 

And later when you run it you just pass mvn -Pconditional-java -Dmy.java.version=1.6.

This requires you to put java version dependant files in separate directories. In your IDE when you develop against specific version of java simply mark directory relevant to your java version as source folder (because by default IDEs will only recognize src/main/java as source dir).

The same way you can pass the compiler level to maven compiler plugin:

<project>
  [...]
  <build>
    [...]
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>${my.java.version}</source>
          <target>${my.java.version}</target>
        </configuration>
      </plugin>
    </plugins>
    [...]
  </build>
  [...]
</project>
like image 38
walkeros Avatar answered Nov 03 '22 10:11

walkeros


Why don't you collect the methods that are supposed to be different in each version of java, wrap them in a "utility" project, make your own api that your main code can call, and on distribution time add whichever utility jar you want?

something like:

util-api.jar (methods that your main project calls)

util-1.6.jar (whichever implementation applies, even "no operation" if needed when nothing is to be done)

I've successfully done this many times with similar problems as the one you have now.

like image 27
Luis Matos Avatar answered Nov 03 '22 11:11

Luis Matos


The guys working on Hibernate wrestled with this very problem for some time before deciding to migrate to a Gradle based build.

Have a look at Gradle: why? for more information.

like image 39
Steve C Avatar answered Nov 03 '22 10:11

Steve C


One way is embedded in code : From log4j2 source code :

   //  JDK 1.1 doesn't support readResolve necessary for the assertion 
   if (!System.getProperty("java.version").startsWith("1.1.")) {
        assertTrue(obj == Level.INFO);
    }

You could also use https://github.com/raydac/java-comment-preprocessor and set variables based on java version to change code. Though would do this in as few places as it will be difficult to debug. Or at least print a log before the dynamic code is run so you know which version / real line has issue.

like image 32
tgkprog Avatar answered Nov 03 '22 10:11

tgkprog


For something like this, I would

  • move JVM-version dependent files into their own package
  • alternatively, use a naming convention to identify JVM-version specific files
  • use Ant, instead of Maven, for compilation. Ant makes it easy to exclude source files based on name or location. You can also easily create several targets with Ant.
  • if the library uses the same API for all JVM versions, I would create interfaces for the JVM-specific classes and then instantiate the appropriate implementations at runtime depending on JVM version.
like image 32
Tim Jansen Avatar answered Nov 03 '22 09:11

Tim Jansen


I added another answer here in response to Leventov's last comment about requiring an explicit cast, and am offering this advice. It may or may not be bad practice, but I have found it very useful for defining my own interfaces in some cases where I wanted to inject some pre-or-post processing or provide my own layer of abstraction overtop of somebody else's (for instance, we defined one like this for Hibernate's Work classes so that if Hibernate's API changes in the future, our implementations don't have to - only the default method in our Interface), and it might make life a bit easier with the two versions of the same interface.

Consider this: The beauty of the functional interface is that it has only one abstract method, so you can pass a lambda as that method's implementation.

But when you extend an interface, which you still want to have that same functionality (pass a lambda, and work in all cases), another beauty of Java 8 comes into play: Defender methods. Nothing anywhere says that you have to leave the SAME method abstract as the parent class did. You can extend an interface as a sort of interceptor interface. So you can define YOUR MyConsumer like this:

public interface MyConsumer<T> extends Consumer<T> {
    default void accept(T t){ // Formerly the abstract functional method
        getConsumer().accept(t);
    }
    public Consumer<T> getConsumer(); //Our new abstract functional method
}

Our interface, instead of defining accept(T) as an abstract method, implements accept(T), and defines an abstract method getConsumer(). This makes the lambda to instantiate a MyConsumer different from the one required to instantiate a j.u.f.Consumer, and removes the compiler's class conflict. Then you can define your class that implements Iterable to instead implement your own custom interface extending Iterable.

public interface MyIterable<T> extends Iterable<T> {    
    default void each(MyConsumer<? super T> action){
        //Iterable.super.forEach((Consumer<? super T>) action);
        //
        // Or whatever else we need to do for our special
        // class processing
    }       
    default void each(Consumer<? super T> action){
        if (action instanceof MyConsumer){
            each((MyConsumer<? super T>) action);
        } else {
            Iterable.super.forEach(action);
        }
    }
    @Override
    default void forEach(Consumer<? super T> action){
        each(action);   
    }
}

It still has a forEach method, allowing it to comply to the iterable iterface, but all of your code can call each() instead of forEach() in all versions of YOUR api. This way you also partially future-proof your code - if the underlying Java api were to be deprecated years down the line, you could modify your default each() method to do things the new way, but in every other place all your existing implementation code will still be functionally correct.

Thus, when you call api.each, instead of requiring an explicit cast, you simply pass the lambda for the other method... in MyConsumer, the method returns a consumer, so your lambda is really simple, you just add the lambda zero-arg constructor to your previous statement. The accept() method in Consumer takes one argument and returns a void, so if you define it with no arguments, Java knows that it wants an interface that has an abstract method which takes no arguments, and this lambda instantiates MyConsumer.

api.each(()->System.out::println);

while this one instantiates j.u.f.Consumer

api.each(System.out::println);

Because the original abstract method (accept) is there, and implemented, it's still a valid instance of Consumer, and will work in all cases, but because we called the 0-arg constructor, we've explicitly made it an instance of our custom interface. This way, we still fulfill the interface contract of Consumer, but we can differentiate our interface's signature from Consumer's.

like image 32
Steve K Avatar answered Nov 03 '22 11:11

Steve K