How to cast object returned from Module
Asked Answered
T

1

0

I trying ModuleLayer with JDK 11, I have created two modules Implementation and Model. Consider the Implementation module provides a method which returns an object which is of type Foo, the class Foo is defined in Model module.

So module Implementation requires Model; and module Model exports all its namespaces.

I a separate application, I am creating Configuration to resolve both the modules and created ModuleLayer as well. By using layer.findLoader("Implementation").loadClass() I have loaded a class from Implementation module and invoked static method using method.invoke(null, "test"); which returns an Object.

Now I don't understand, how should I can cast this object to Foo? I cannot use (Foo) obj because the jar of Model module is not on classpath. and even if I put it I end up with exception

Caused by: java.lang.ClassCastException: class com.test.model.Foo cannot be cast to class com.test.model.Foo (com.test.model.Foo is in module Model of loader jdk.internal.loader.Loader @e9b78b0; com.test.model.Foo is in unnamed module of loader com.company.container.internal.Interpreters$ClassLoaders$$anon$1 @45815ffc)

Object provider=null;
try
{
   var path = Path.of(Core.getConfiguration().getBasePath().getPath() ,"\\model\\lib\\EC");
   ModuleFinder finder = ModuleFinder.of(path);
   var parent = ModuleLayer.boot();
   Configuration cf = parent.configuration().resolve(finder, ModuleFinder.of(), Set.of("Implementation"));
   ClassLoader scl = this.getClass().getClassLoader(); // intentionally using this class's loader  
   ModuleLayer layer = parent.defineModulesWithOneLoader(cf, scl);
   var cl = layer.findLoader("Implementation");
   Class<?> classTest = cl.loadClass("com.test.AutoConfiguration");
   var method = classSendMailFromX.getMethod("getProvider", String.class);
   provider = method.invoke(null, "test");
}
catch (Exception e) {
   throw new CoreException(e);
}
if(provider == null)
{
   throw new Exception("provider cannot be empty");
}

var myProvider = (Foo) provider; // This line compiles but calls fails at runtime if I put `Model` module jar on classpath

I understand the reason, that any jar (even if it is a module) at classpath will be treated as unnamed module. But, why I cannot able to cast it? And is there any way to do it?

Thaumatology answered 26/11, 2022 at 12:14 Comment(1)
Your problem is entirely unrelated to modules. If you load class Foo through Interpreters$ClassLoaders$$anon$1@45815ffc and another class Foo through Loader@e9b78b0, these are different classes. Whether these Foo classes belong to a module, doesn’t matter.Boloney
C
1

The comment from @Holger names the problem: That Foo isn't Foo as soon as they are both loaded by different class/module loaders.

Let me sketch two different approaches:

Solution1: The module way

The goal here is to have class Foo only loaded once. To achieve this the application with the code above should be in a module as well and require Model:

module App {
    requires Model;
}

To make this work you have additionally to avoid having Model loaded a second time through the presented code. This can be achieved either (a) by not having it placed into "\\model\\lib\\EC". Or by (b) changing the order of ModuleFinders in Configuration.resolve():

Configuration cf = parent.configuration().resolve(ModuleFinder.of(), finder. Set.of("Implementation"));

This way Implementation will use the version of Model present in the module path of the calling code and not load it via finder.

Solution 2: Use a proxy

This requires Foo to be an interface.

You could have a method proxyOf that looks like this:

    private Foo proxyOf(Object result) {
        InvocationHandler handler = (proxy, method, args) -> {
            Method delegate = result.getClass().getMethod(method.getName(), method.getParameterTypes());
            return delegate.invoke(result, args);
        };
        return (Foo) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{ Foo.class }, handler);
    }

This way you can have two versions of Foo. You just have to get sure, that the application code is actually allowed to access method delegate. So it (and the containing implementation class) must either be public or you will need an additional delegate.setAccessible(true); before calling it. If the application is within a module (like in Solution 1), the package of delegate must be opened through the opens clause:

module Implementation {
    requires transitive Model;
    exports org.example.impl;
    opens org.example.impl;
}

Please not that there is a major pitfall in the proxy solution: It works well as long as you are passing types of the JRE, but as soon as you pass your own types method.getParameterTypes() will return the wrong types leading to a NoSuchMethodException. Even when you manage to get the correct types you would need to pass proxies of that types. Then all this will get really complicated and you will end up with your own little "back and forth proxyfication" framework.

Final notes

Unfortunately you do not describe what you actually want to achieve. So we cannot know what problem you want to solve with it and why you need the code above. Honestly, it seems quite difficult to me that your code could be maintained by someone else. Maybe you could achieve the same thing by using ServiceLoader?

Craniology answered 14/7, 2023 at 15:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.