Maven/Retrolambda: how to detect dependencies on Java 8 classes - java

Background:
We have maven-based java project, which targets JRE 1.7, but the source code uses lambdas, so we use retrolambda for transforming Java 8 source code to Java 7. Also we use StreamSupport backport library when we need streams, function.*, Optional, etc.
Usage of retrolambda involves configuring the project's both source and target language level to 1.8.
Everything works fine if there are no dependencies on java8 classes or methods (like java.util.stream.*, java.util.Optional, or methods introduced in java8 like Collection.forEach). If there are such usages then build passes, but it fails in runtime, when running under JVM of Java 8.
Question:
My goal is to fail the build in case when such dependencies exist. Is there any way of detecting dependencies on new Java 8 classes/methods in build-time?
I thought about 2 possible options, but I'm not sure whether either of them is doable:
Some kind of bytecode analyzer for detecting depdencies on predefined classes and methods. Are there such tools/maven plugins?
Lint (lint4j) rules. Not sure whether it's possible to detect dependency on class/method using lint

You can use the Animal Sniffer Maven Plugin for this. It allows you to check that your code only uses APIs from a specified baseline (called the "signature"). In your case you'd use the org.codehaus.mojo.signature:java17:1.0 signature.
As others pointed out, you also could set up the bootstrap classpath, but that a) requires a JDK 7 to be set up and b) makes the build a bit more complex as you need to point to the JDK 7 install. Animal Sniffer is is much easier to work with in my experience.

Related

How to validate dependencies' java version when compiling using higher version JDK?

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.

Does there exist a Babel like compiler for Java?

With javascript if we want to make sure that our code runs in all browser versions we can use Babel. Is there something like this for Java, where we could write our code in Java 9, but it will run in a Java 6 runtime?
For example can Kotlin target multiple JVM runtime versions?
I was hoping for something like Kotlin to target multiple JVM runtimes - I guess we just have to dream for now.
You can compile Kotlin code to JDK6, JDK7, JDK8, JDK9 or any JDK above JDK6. This is what meant by supporting Java 1.6 level byte code. All features of Kotlin will stay the same, except for libraries, which can require different JDK versions.
The byte code generated by Kotlin will generally stay the same independent of the target JVM version. An exception is if you set a compiler option jvmTarget = "1.8", then the compiler may (or may not) use some features of JDK8 as an optimization.
IMHO this question got all the minuses because of how unexpected it is. Tools like Babel are unique to JavaScript because in all other languages they are called compilers. Since JS decided it could do without a compiler, I has such problems with deployments. There are (very limited) back porting tools for Java, but they are just plugins to the compiler. Kotlin doesn't have any, because its development is independent of JDK and it has to support all previous JDK versions above 1.6.
To sum it up, if you use Kotlin for JVM or JS development, your dream have come true - you can use any version of Kotlin, with any JVM library, probably any JS library above ES5.1, and get consistent runtime representation.
There is an unofficial library retrolambda which compiles Java 8 feature lambda expression into Java 6(just like Babel).
I guess you will enjoy it, and here it is: https://github.com/orfjackal/retrolambda
You can check teaVM ! http://teavm.org/ also there is dukeScript

How to run two java versions in the same project?

I have created a project in Java Eclipse which uses java 1.7. But I need to run some specific modules in it using java 1.8.
Is it possible? How?
you can set the Java compiler level only project wise not the module wise.
So in your case you should set the compiler level 1.8 which will support both the version.
In eclipse you can set the compiler level by below option.
Right click on Project->Properties->Java Compiler.
Simply spoken: you shouldn't do that.
If you are really talking about one project; then you have to make sure that all "components" within your project are on compatible levels.
In your case: if component A requires Java 1.8; and others are fine with 1.7 ... then you should go forward and use 1.8 (you still can use libraries that were compiled for older versions of Java; no need to update/recompile them). And well, if one part needs 1.8; and another only works 1.7 ... then there is no easy solution to that.
The point is: if you deviate from this practice, you will have to use multiple JVMs later on to run your "single" project - and that is of course a contradiction in itself.
The alternative is to dissect the one project you have right now into smaller parts (nowadays you would call them microservices) and define an architecture that allows you to run different parts of your application using different technology. But as others have pointed out: that adds a whole new layer of complexity to your setup.

How to list the JDK compatibility (like "sourceCompatibility") of all of the dependencies with a gradle project?

I need to know the Java runtime compatibility of all of the dependencies I'm using in my gradle project (as if it were checking each project's "sourceCompatibility" setting so to speak), preferrably without perusing each dependency's documentation to find it, if it's there at all. Is there a way to do this with gradle? Or even some other automating tool?
(Specifically I'm trying to see which dependencies might be using Java 8 features like Streams, since I'm trying to compile for Android with retrolambda, and iOS with RoboVM.)
Such information probably does not exist, and is very tedious to generate.
"sourceCompatibility" does not exist after the source is compiled-- it's just to tell the compiler how to interpret the source syntax. What you're more likely to be interested in is the "targetCompatibility", or the class file format major version: Java 8 is 52, Java 7 is 51, etc.
This tells Java that Java 8 is required to understand the class format and bytecodes contained in the class, so you could download and unpack every dependency in your project, and all of their dependencies, and then look at the version number of every class file, except...
Simply looking at the class file format version doesn't tell you whether the class makes reference to methods and fields that exist only in specific versions of the JDK. I've not tested with Java 8, but in Java 7 it's possible to set the source/target compatibility to 1.6 and still reference new methods that were added in Java 7. A Java 6 JVM will load and run the file, but fail with NoSuchMethodException despite otherwise looking perfectly fine.
The only way to realistically check if a dependency is completely compatible with a different version of Java than the one it was compiled for is to go through the constant pool of every class, find every class and method reference, and then verify that they are valid for the desired JRE.
You will want an automated tool for this (The JVM could do it if you have the desired version of Java installed and 100% coverage in your unit tests...), but I don't know of a standalone tool that does, and neither gradle nor project documentation usually includes this sort of info.

Java - Write code that older version ignore

I try to write my code compatible with older versions of java, so it will work everywhere.
on the other hand there is very powerful tools in the new version - java 8, and I want use them.
So I'm forced to choose between compatibility or richest code.
And I'm wondering if by any chance I can write some methods in java 8, and somehow prevent the compiler of older version to ignore these methods, so my class is compatible "partially" with older version.
Thanks.
You can write two classes and use some toll like ant, maven or gradle to chose which file use for compiling with concrete Java version.
You can set the java compiler to compile against an older jdk (ie jdk 1.5) even if you use jdk 1.8. see javac source and target options
I think the short and easy answer is no.
See this thread: Can Java 8 code be compiled to run on Java 7 jvm?
You can use the java reflection api to check if methods exist in the jvm the code runs on. This allows you to make your code fail-safe even when a method or class is unavailable in the jvm. Doing this is very cumbersome however and I'm pretty sure it's not what your're looking for.

Categories

Resources