Conditional import and optional runtime dependencies in Java
In Java, it is not possible to conditionally import a module.
While normally not necessary, there are use-cases for which conditional imports would be very useful.
Some of those use-cases are optional dependencies or platform-specific modules, like the Registry
class described here.
There are multiple possibilities for using this package in a multiplatform project, each one with its own set of tradeoffs.
Facade
We can solve any problem by introducing an extra level of indirection.
The fundamental theorem of software engineering
In this case, the indirection would be creating a facade that on Windows uses registry.jar
and on other platforms it does nothing, returns an error or some other operation, like using a file instead of the registry. The end effect would be that the code with the business logic can unconditionally use the facade.
Contrary to the quote, this indirection, as described, does not solve the issue, but localizes it.
Move the burden to the build system
In Maven or Gradle it is possible to create multiple build targets and compile different files. As a result, there will be a separate jar file for the Windows Platform, and another for all other Platforms.
This is probably the simplest solution but has also some drawbacks.
The first drawback is code duplication.
Every class that uses the Registry package, needs some code duplication, at least all methods declaration (this drawback can be minimized by using the facade described previously). Depending on the size of the class, and the fact that it is confusing to have two files with the same name, in the same relative directory with the same class in the same package, I tend to dislike this solution. Also, most IDEs do not always play well with this setup.
Reflection
With reflection, it is possible to bypass the issue, as it is possible to query at runtime if a class is available
try {
Class c = Class.forName("windows.Registry");
// it is possible to create an instance of Registry
} catch (ClassNotFoundException ex) {
// Registry is not there, do not create an instance
}
Depending on how the code is written, one can see that the class Registry
might not be there.
It is possible to create an instance by writing Object o = c.newInstance();
.
The biggest advantage of this approach is that it is possible to not duplicate any class, and provide a package that works on all platforms.
Use a preprocessor
A "standard" Java build system does not include a preprocessor, but this does not mean that it is not possible to use one to adapt the code before compiling it.
It is possible to use a simple sed
/awk
command or even the C preprocessor. For example, if GCC is available, it is possible to apply only the preprocessor with gcc -E
.
The first drawback is that it is not possible to provide only one package for all platforms. Also (some) source files are not valid Java files anymore (debugging and other features provided by the IDE might be problematic) and the build system gets more complex because of this custom solution.
Always import, but use a dependency only conditionally
Maybe the preferred approach is just to ignore the issue altogether.
It is possible to leverage the fact that "import" does not generate any code, and that the ClassLoader should behave as if all classes are loaded lazily.
For example, suppose that the Registry
class looks like
package windows;
public class Registry {
static Registry instance = null;
static{
System.out.println("print from static block");
}
private Registry() {
System.out.println("Registry instance created!");
}
public static synchronized Registry getInstance() {
if (null == instance) {
instance = new Registry();
}
return instance;
}
public void foo() {
System.out.println("foo called.");
}
}
A program using it:
package main;
import windows.Registry;
public class Main {
public static void test1(boolean onWindows) {
if (onWindows) {
Registry instance = Registry.getInstance();
System.out.println("We are on Windows: ");
instance.foo();
} else {
System.out.println("We are somewhere else!");
}
}
public static void main(String[] args) {
System.out.println("Entered main");
boolean onWindows = args.length > 0 ? Boolean.parseBoolean(args[0]) : false;
test1(onWindows);
}
}
I decided to test different configurations.
First, if it executes as expected on Windows
rm -rf bld; mkdir bld && javac -d bld src/windows/Registry.java src/main/Main.java && java -classpath bld main.Main true
In this case, the output is
Entered main
print from static block
Registry instance created!
We are on Windows
foo called.
On a non-Windows platform
rm -rf bld; mkdir bld && javac -d bld src/windows/Registry.java src/main/Main.java && java -classpath bld main.Main false
In this case, the output is
Entered main
We are somewhere else!
Actually, on a non-Windows platform, it is better to test without the bld/Windows
folder.
rm -rf bld; mkdir bld && javac -d bld src/windows/Registry.java src/main/Main.java && rm -r bld/windows && java -classpath bld main.Main false
In this case, the output is
Entered main
We are somewhere else!
So it does not make a difference if the .class files are there or not, as the class is loaded lazily, even for static methods. Obviously, it is an issue if we remove the .class files that are in fact needed
rm -rf bld; mkdir bld && javac -d bld src/windows/Registry.java src/main/Main.java && rm -r bld/windows && java -classpath bld main.Main true
In this case, the output is, as expected, an error:
Entered main
Exception in thread "main" java.lang.NoClassDefFoundError: windows/Registry
at main.Main.test1(Main.java:9)
at main.Main.main(Main.java:20)
Caused by: java.lang.ClassNotFoundException: windows.Registry
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
Notice that it is possible to see that the static block is executed when the class is referenced at runtime for the first time.
Does this method work reliably?
The example demonstrates that no matter what is written in Registry.java
:
-
no code is executed until the class is referenced
-
no references are resolved until needed at runtime
But it does not prove that this is the expected behavior on all platforms with all Java Runtimes.
As I was not sure, even after testing on different platforms, I decided to ask on StackOverflow if the behavior is documented, and found what I was looking for.
The Java runtime documents very precisely when a class is initialized
A class or interface T will be initialized immediately before the first occurrence of any one of the following:
T is a class and an instance of T is created.
A static method declared by T is invoked.
A static field declared by T is assigned.
A static field declared by T is used and the field is not a constant variable (§4.12.4).
…
12.4.1. When Initialization Occurs
Therefore if a class is never instantiated, or its static member/function accessed, then no code from the class will ever be executed.
The Java runtime also documents how the ClassLoader loads class files
The loading process is implemented by the class ClassLoader and its subclasses. The method defineClass of class ClassLoader may be used to construct Class objects from binary representations in the class file format (§1.4).
Different subclasses of ClassLoader may implement different loading policies. In particular, a class loader may cache binary representations of classes and interfaces, prefetch them based on expected usage, or load a group of related classes together. These activities may not be completely transparent to a running application if, for example, a newly compiled version of a class is not found because an older version is cached by a class loader. It is the responsibility of a class loader, however, to reflect loading errors only at points in the program where they could have arisen without prefetching or group loading.
…
12.2.1. The Loading Process
Thus even if unused, a ClassLoader might try to load Registry.class
. This can also happen if the file does not exist at runtime, but, as loading errors are deferred as if no prefetching would have taken place, it is safe to assume that there is no issue, as long as the class is not referenced at runtime.
What about a custom ClassLoader?
While the documentation should hold also for custom ClassLoaders, a custom ClassLoader can diverge from the expected behavior. It could, for example, load the class Registry.class
explicitly, and throw an error if this is missing.
Such ClassLoader would be non-compliant, but in such a scenario, the documentation does not help much.
I have no idea if such non-compliant ClassLoaders are used in practice, and if it is the case, how hard it is to fix them. Notice that this bug is no different from other bugs, except for the fact that ClassLoader is probably not a widely understood or explicitly used component of a Java program.
At least there is a workaround guaranteed to work with buggy ClassLoaders, based on dead code elimination.
Dead code elimination as a workaround
The javac
compiler performs what is known as dead-code optimization practically forever. If we can thus mark
if (onWindows) {
Registry instance = Registry.getInstance();
System.out.println("We are on Windows");
instance.foo();
}
as dead code, javac
would eliminate it, and there would not be any reference.
Obviously, in the given example this is not possible as onWindows
is determined at runtime, but if the program would look like
public class Main { public static final boolean onWindows = false; public static void test1() { if (onWindows) { Registry instance = Registry.getInstance(); System.out.println("We are on Windows"); instance.write(); } else { System.out.println("We are somewhere else"); } } public static void main(String[] args) { System.out.println("Entered main"); test1(); } }
then the generated bytecode would not have any reference to Registry
.
If there is no Registry, even a buggy ClassLoader has no reason to try to load it.
And in the case a ClassLoader still tries to load the Registry
class, we would have the same issue even if we delete the code that references Registry
, as the generated bytecode would be the same.
But how is it possible to reuse the same source file, but with different constants for different platforms?
The most straightforward answer is to create a Java file for every Platform with such constants, in this case
public class Constants { public static final boolean onWindows = true; }
and
public class Constants { public static final boolean onWindows = false; }
and give the compiler through the build-system the correct Constants.java
file.
An even better approach would be to let the build system generate those files.
The drawback of this workaround is that it is not possible to provide one single archive for all platforms.
At least the scope of the difference between the source code of different artifacts has been reduced to a single constant, instead of a whole class.
Do you want to share your opinion? Or is there an error, some parts that are not clear enough?
You can contact me anytime.