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.
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.
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.
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.
I have a PowerMac and it is giving me bad version number on some .jars. I would love to make it seem like I am running Java 6. How would I spoof the version? Let me also say I am running PowerPC and Leopard
The most likely problem is that you have Java 6 JAR files and you are trying to run them on an old Java installation.
How would I spoof the version?
The answer to your question is that you can't. The way to run Java 6 specific JAR files it to use a Java 6 (or later) JRE or JDK.
The problem is that the format of Java class files has changed, and your installation can't cope with the new format. And this is not a gratuitous change that you can pretend doesn't exist. Java 6 (actually Java 5) has support for generic types, enums, annotations and other things. Assuming that the JARs contain code that uses these new language features, an older JRE simply won't know what to do with them.
There are two solutions:
Upgrade your Java installations to the required level on all machines. This is the best solution ... if it is an option ... because it means your users will get the benefit of security and bug fixes and performance enhancements. (And progress of your project won't be held back by the constraint of supporting legacy platforms.)
Compile all of your code for compatibility with the oldest version of Java that you still have to use. Either compile on the corresponding old JDK, or on a more recent JDK using appropriate -source / -target / -Xbootclasspath options ... as described by the javac manual page.
The catch with the second solution is that if the source code for the JAR files in question uses recently added Java language features or APIs, then recompiling for the older platform will fail. To fix this you will need to rewrite your code to replace the nice modern stuff with archaic stuff. Not a good solution, IMO.
The other possibility is that you are seeing corrupted JAR files. This is unlikely, but it can happen if you are using applets or webstart, and the server is delivering error pages instead of JAR files.
The third possibility is that you simply haven't configured your Mac's Java installation's correctly. Making Java 7 the default should allow you to run everything without class version problems. (Thanks #paulsm4) Note that I can't help you with that ... 'cos I don't use Java on a Mac.
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