I have a library consisting of multiple modules:
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 runtime MultimapVerifier
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
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.
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 via Class#newInstance
, assigning the result to a variable declared of type ThingyImplementation
. 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 have MultimapVerifier verify(Multimap);
in it. If you have to provide that, it would have to accept Object
and then cast within the optional module implementation as necessary.
I figured out a way! You can use class shadowing to achieve the desired behavior.
- Declare two modules:
- In the core library, declare an interface for the functionality exposed by each module. For example:
CoreVerifiers
GuavaVerifiers
- Declare
GuavaVerifiers
a second time in the guava module
- Each
GuavaVerifiers
interface should declare the methods implemented by that module. So, GuavaVerifiers
in the core module should be empty while GuavaVerifiers
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)
- Implement the interfaces in each module. For example
CoreVerifiersImpl
should implement CoreVerifiers
. GuavaVerifiersImpl
in core module should be empty because GuavaVerifiers
in core module is empty. On the other hand, GuavaVerifiersImpl
in the guava module should implement the non-empty GuavaVerifiers
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:
public interface ForwardingCoreVerifiers
{
CoreVerifiers coreVerifiers();
default CoreVerifiers method1()
{
coreVerifiers().method1();
}
}
Finally, in the core library, declare an implementation that extends all of the forwarding interfaces:
public final class Verifiers
implements ForwardingCoreVerifiers, ForwardingGuavaVerifiers
{
public CoreVerifiers coreVerifiers()
{
return new CoreVerifiersImpl(...);
}
public CoreVerifiers guavaVerifiers()
{
return new GuavaVerifiersImpl(...);
}
}
Now here's the magic:
- If users include the core module in their classpath then the compiler will prevent them from invoking any guava-related methods.
- But, if users include the guava module in front of the core module on the classpath then suddenly users will see the guava-related methods (because the guava module
GuavaVerifier
interface will shadow the interface found in the core module).
Performance tips:
- In Java 8, default methods don't perform as well as abstract classes: https://stackoverflow.com/a/30314501/14731
- As such, I recommend using an abstract class for the core functionality (
CoreVerifiers
) and using default methods for the remaining modules (GuavaVerifiers
)
UPDATE: I have posted an updated question for Java 9.