I have a library consisting of multiple modules:
- core
- guava
The core
module is mandatory, while guava
is optional. There are other optional modules (this question represents a minimal testcase).
Each module exposes a set of methods that the user can invoke:
class CoreVerifier
{
MapVerifier verify(Map);
}
class GuavaVerifier
{
MultimapVerifier verify(Multimap);
}
What I want
Provide users a class that exports all the methods in a single place:
class UnifiedVerifier { MapVerifier verify(Map); MultimapVerifier verify(Multimap); }
I want users to be able to use this class even if optional modules (e.g. guava) are missing at runtime. Meaning, the
UnifiedVerifier
is compiled with all libraries on the classpath but at runtimeMultimapVerifier
referenced by the second method is not present.- Users should be able to invoke the first method even if the second method (that depends on the guava module) is not available at runtime.
- If users attempt to invoke the second method (that depends on the missing module) they should get a runtime exception.
What actually happens
If users invoke the first method from application code javac fails with:
Application.java: cannot access MultimapVerifier class file for MultimapVerifier not found
Meaning, even though the first method is well-defined (the core module is available at compile-time) the compiler refuses to proceed because the second method (which they are not using) is referencing a class which is missing from the classpath.
Is there a way to achieve this sort of thing in Java?
Similar technique by assertj
assertj has a clever static-import mechanism whereby they declare a different Assertions
class per module (core, guava) and Java's static import picks up the right method depending on the types you pass in. I am already using a similar mechanism for static methods, but now I want something similar for a case where I can't use static methods.
I figured out a way! You can use class shadowing to achieve the desired behavior.
CoreVerifiers
GuavaVerifiers
GuavaVerifiers
a second time in the guava moduleGuavaVerifiers
interface should declare the methods implemented by that module. So,GuavaVerifiers
in the core module should be empty whileGuavaVerifiers
in the guava module should contain the methods implemented by that module. (Meaning, if users only link against the core module they shouldn't be able to see any guava-related functionality)CoreVerifiersImpl
should implementCoreVerifiers
.GuavaVerifiersImpl
in core module should be empty becauseGuavaVerifiers
in core module is empty. On the other hand,GuavaVerifiersImpl
in the guava module should implement the non-emptyGuavaVerifiers
interface.For each interface in step 2, declare an interface in the core module with default methods that delegate to an existing verifier. For example:
Finally, in the core library, declare an implementation that extends all of the forwarding interfaces:
Now here's the magic:
GuavaVerifier
interface will shadow the interface found in the core module).Performance tips:
CoreVerifiers
) and using default methods for the remaining modules (GuavaVerifiers
)UPDATE: I have posted an updated question for Java 9.
Yes, you can do that like this:
Define an interface (perhaps internal to the module providing these services); I'll call it
ThingyImplementation
.Put the code using the optional library (Guava, etc.) in a distinct class that you don't directly reference in any other code. That class implements
ThingyImplementation
.At runtime, dynamically attempt to load the class using the optional librayr using
Class.forName
, and instantiate it viaClass#newInstance
, assigning the result to a variable declared of typeThingyImplementation
. Use the resulting instance via the interface as part of providing the functionality of the .Loading or instantiating the class in step 3 will throw an exception if the optional library isn't available on the classpath, which you can either propagate or wrap in your own exception.
Note that the outer class (
UnifiedVerifier
) cannot directly refer to types defined by the optional library, so you couldn't haveMultimapVerifier verify(Multimap);
in it. If you have to provide that, it would have to acceptObject
and then cast within the optional module implementation as necessary.