I'm currently using gradle and it seems like I can set sourcecompatibility and targetcompatibility through java-plugin
I wonder what's the reasons for us to use sourcecompatibility/targetcompatibility other than to be backward compatible with older JDK?
would it be easier to upgrade to latest java if sourcecompatibility/targetcompatibility isn't set?
Should sourcecompatibility and targetcompatibility be used at all?
The sourceCompatibility and targetCompatability variables are directly related to the -source and -target switches on javac. These are used for cross-compiling (relevant documentation for javac, relevant SO question). The sole purpose of these values is to make your class files backwards compatible. They don't really serve any other purpose. This is a feature that's added more or less for security purposes. Instead of installing an older, less secure JDK on your machine, you can install a newer one and use these values to compile your code.
These flags have one major caveat though: just because you set these values doesn't mean the code you write will work on older JVMs. This is because while javac will compile your classes to make them compatible with an older JVM, you can still end up binding to a method that doesn't exist in that older JVM.
For example, if you set your compatibility values to '1.6' and you're running your builds using JDK 8, you can still write code like this:
Objects.equals(obj1, obj2);
And the compiler will not complain. It will still build without complaining about that line. However, if you tried to run it with Java 6, you would get an error stating that Objects.equals doesn't exist because Objects was introduced in Java 7.
Put differently: -source and -target make your .class files compatible with older JVMs, but they do not validate classes and methods used by your code. The only way to do that is with -bootclasspath, at which point, you may as well just download and compile with the older JDK since you have to do that to get the rt.jar file needed for that (relevant SO question).
As far as upgrading is concerned, setting these values don't necessarily limit your ability to upgrade. Code you write will have to be compatible with whatever value you use if you plan on using it in that version, but nothing stops you from changing them and starting to use the newer features and APIs.
Related
Recently, a teammate used the following function in our Java 8 code: Matcher.replaceAll​(Function replacer).
The function was introduced in Java 9, but because he is using a newer compiler, the API function was simply found in the JDK's rt.jar and nobody noticed this won't work under real Java 8 environments.
The compatibility settings are correctly set, and the gradle subproject has the following settings:
sourceCompatibility = 1.8
targetCompatibility = 1.8
I had very similar issues at the time when I first used the Java 6 function String.isEmpty in Java 5 code - the code made it into the release and crashed there.
What can I do to enforce the usage of the correct API. As it is a shared library, do I have to use (and install, maintain..) a different JDK for this gradle subproject, or is there some kind of compatibility scanner which runs through a built jar and checks all rt references?
As you've noticed, the two compatibility configurations does not consider the APIs of older versions - only the syntax, semantics and the resulting byte code.
There are two options you can take. One is to have JDK 8 installed on your computer, and the configure Gradle to use it when compiling your project. It looks like this:
tasks.withType(JavaCompile) {
options.fork = true
options.forkOptions.executable = "$java8Home/bin/javac"
options.bootstrapClasspath = files("$java8Home/jre/lib/rt.jar")
}
The disadvantage here is that you will need to have JDK 8 installed in the first place, and as it will probably be installed in different locations, you will need probably want to configure it with an environment variable or property (I've called it java8Home here).
However, since Java 9, the JDK now knows about the documented APIs of previous versions, and you can select which one to use with a new --release flag. This is not going to work if you use undocumented APIs, but it means you can compile your project with any versions of Java and still make the resulting classes compatible with Java 8. You can do it like this:
tasks.withType(JavaCompile) {
if (JavaVersion.current() > JavaVersion.VERSION_1_8) {
options.compilerArgs.addAll(['--release', '8'])
}
}
Note that the 'if' statement is only there in case you still need to support running Gradle with Java 8 (through your JAVA_HOME variable). If you are only using later versions, it can be removed so you always set the 'compilerArgs'.
For some versions of Java, it is possible build Java code on a newer JDK to run on an older JDK / JRE. You have already discovered the --source and --target options for javac and the corresponding Gradle settings. The other thing you can do is to use --bootclasspath to tell javac to compile against the runtime libraries for an older version of Java.
Since you are using Gradle, check out "gradle-java-cross-compile-plugin" (https://github.com/nebula-plugins/gradle-java-cross-compile-plugin). I can't find any documentation for it, but it apparently deals with --target and --bootclasspath.
Having said that, I don't think cross-compiling Java is a good solution.
I would actually recommend that you set up a Continuous Integration (CI) server (e.g. Jenkins) with JDK installations for all of the Java versions you are interested in supporting. Then set up jobs to build your code and run your unit tests for each Java versions.
Note that simply compiling your code against the older Java libraries is not sufficient to verify backwards compatibility. Sometimes the behavior of libraries changes. You need to run your tests, and your tests need to cover the cases where compatibility issues may exist.
We're using java 8 for most modules/projects, but for some of the modules, we use java 6 (customer requirements).
The developers have java 8 installed and we compile the java 6 projects using these flags:
compileJava {
sourceCompatibility = 1.6
targetCompatibility = 1.6
}
We thought we're all good until we upgraded guava from v20 to latest - 28.1-jre.
To our surprise, the build was successful but failed at runtime.
We have a workaround for building for java 6 using a specific javac found in JDK 6. See more info here. This workaround wields the error class file has wrong version 52.0, should be 50.0 in compile time. The downside is that it requires a download+config+usage of JDK 6 for developers.
Is there a way to validate the dependencies' java version at compile time when using a higher java version? (without installing lower version java) Thanks.
Setting -source and -target values to 1.6 is insufficient to ensure that the resulting output is compatible with 1.6. The program itself must not have any library API dependencies on later versions, and the -source and -target options don't do that. (GhostCat said pretty much the same thing.)
For example, in Java 8, ConcurrentHashMap added a covariant override for the keySet method that returns a new type ConcurrentHashMap.KeySetView. This type didn't exist in earlier versions of Java. However, in the class binary, the return type is encoded at the call site. Thus, even if the source code is compiled with -source 1.6 -target 1.6, the resulting class file contains a dependency on the Java 8 class library API.
The only solution to this is to ensure that only Java 1.6 compatible libraries are in the classpath at compile time. This can be done using the -Xbootclasspath option to point to a JDK 1.6 class library, or it might be simpler just to use a JDK 1.6 installation in the first place.
This applies to external libraries in addition to the JDK, as you've discovered with Guava. The Animal Sniffer project provides plugins for Ant and Maven that checks library dependencies for version problems. Offhand I don't know if there is something similar for Gradle. There might be a way to get Animal Sniffer to work with Gradle, but I have no experience with doing that.
Is there a way to validate the dependencies' java version at compile time when using a higher java version? (without installing lower version java).
You specify your dependencies. When you tell your built system to explicitly use some library X in version Y, then you made a very clear statement.
And you see, it is not only about the class file version number. What if some person doesn't pay attention, and compiles something with Java8 ... with Java6 target, but forgets that the code bases uses Java8-only API calls?!
In other words: you are looking in the wrong place.
The person who makes updates to the build description, and changes a library version from Y to Y+8, that person needs to carefully assess that change. For example by reading release letters.
I agree that a really clever build system could check if libraries you are using come in with a matching class file version. But as said, that is only one aspect of the problem. So instead of looking into a technical solution, I think the real answer is: don't step version numbers because you can, but because you have to. And that manual step of changing that version number, that is something that requires due diligence (on the side of the human doing it).
Thus: I think the most sane approach here is to compile the Java6 deliverables within their own specific build setup. Which you only touch after careful inspection of such details. And sure: convince your customer to move on, and give up a long dead version of Java.
The target system, on which my application is supposed to run, uses Java 6. On my development machine, I have Java 7. Can I do the development, without downloading Java 6?
I found on http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html one example for cross compilation:
javac -source 1.6 -target 1.6 -bootclasspath C:\jdk1.6.0\lib\rt.jar -extdirs "" OldCode.java
However, this too requires the existence of a rt.jar, which belongs to Java 6. Is there a simpler way?
New Java versions generally change both the Java Language (source and class file format) and the Java API.
The Java compiler can emit class files in the old format, even if the source is in a new format (these versions are specified by -target and -source, respectively). Therefore, you don't need the old compiler to target an old JVM.
However, the changes to the Java API are somewhat harder to account for. The easiest is to compile using the API of the Java version you target (-bootclasspath). Of course, you may feel confident that you are not using newer APIs, and skip this step, but the only way to make sure is actually compiling against, and testing on, the old runtime library.
In short, while cross compilation is helpful in that the same source can be used with different Java versions, you should compile and test against the actual Java version you intend to use, which does require the old JRE (or JDK).
BTW, all of these settings are also available in Java IDEs. For instance, in eclipse, one would set the compliance level in the project's compiler settings, and add the appropriate "JRE System Library" to the project's "Build Path":
The below command should suffice to meet your requirement.
'javac -source 1.6 -target 1.6 OldCode.java'
With this command you are telling that the compiler should generate class file that is compatible with java 6. Any java 7 specific will result in compilation error. Regarding rt.jar, you don't need to have java6 specific version. As mentioned the above command automatically ensures output is java6 compatible.
UPDATE/CORRECTION:
After going through the following link http://www.javaworld.com/article/2077388/core-java/what-version-is-your-java-code.html it is clear why it is recommended and is important to use -bootstrap flag along with -source and/or -target flags.
The article "Don’t be fooled by javac -target 1.4" shows that compiling with -target 1.4 still can create class files which will not run on JRE 1.4.
It also describes how the -bootclasspath parameter can solve this problem.
Now my question: if I compile with the Sun JDK 1.6 version of javac, using the source and target parameters set to 1.3, is safe to assume that the compiled classes will work on a JRE 1.3?
They won't work if you use any methods or classes in the standard library that didn't exist in 1.3, as these files are not compiled into your program, but are part of the JVM's libraries.
In fact, like the article says, your code will be bytecode compatible with 1.3, but not linkable with 1.3. As a consequence, it is possible for you to use post 1.3 classes or method (as states their example) as long as you use a rt.jar coming from a version more recent than the expected 1.3.
As a consequence, the only sure way to develop for 1.3 is to use a 1.3 JDK ... or to rely on verification tools, like ... oh crap ! i'm sure there is one maven reporting plugin that can check your code is compatible with expected JDK, but can't find back how it's named.
However, notice there exist tools like Retrotranslator, that will take your post 1.5 code to adapt it to previous versions ... as long as your code solely relys on JDK, as they may not be able to handle all external APIs dependencies
I'm looking to write some Groovy code to perform tasks inside of install anywhere but because of the platforms we support I'm restricted to java 1.5. Any idea if this will be sufficient to run the latest groovy?
Java 1.5 should work. The Groovy docs state that you need at least 1.4. I use Groovy with Java 1.5, and everything works fine as far as I can tell.
As of early 2015, the build environment for groovy 2.4.0 (in the build.gradle file) indicates:
sourceCompatibility = 1.6
targetCompatibility = 1.6
So, for modern groovy, you want 1.6 or better. The documentation for 2.4 indicates JDK 1.5 support, which disagrees with the build environment, so be warned if you're stuck on 1.5.
In addition, there's support for use of the invokeDynamic instruction in later releases of JDK 1.7 (it was present but known to be buggy in releases older than JDK 7u60) which may offer some performance improvements.