我尝试使用JDK 11的ModuleLayer
,我创建了两个模块Implementation
和Model
。考虑Implementation
模块提供了一个返回类型为Foo
的对象的方法,Foo
类在Model
模块中定义。
模块Implementation
requires Model;
和模块Model
导出其所有的命名空间。
我是一个单独的应用程序,我正在创建Configuration
来解决这两个模块并创建ModuleLayer
。通过使用layer.findLoader("Implementation").loadClass()
,我从Implementation
模块加载了一个类,并使用method.invoke(null, "test");
调用静态方法返回Object
.
现在我不明白了,我该如何将这个对象转换为Foo
?我不能使用(Foo) obj
,因为Model
模块的jar不在类路径上。即使我把它放进去,也会得到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
我理解的原因,任何jar(即使它是一个模块)在类路径将被视为未命名模块。但是,为什么我不能投射它呢?有什么办法可以做到吗?
@Holger的评论指出了问题:Foo
不是Foo
,因为它们都是由不同的类/模块加载器加载的。
让我简述两种不同的方法:
方案1:模块方式
这里的目标是使Foo
类只加载一次。要实现这一点,上面代码的应用程序也应该在一个模块中,并且需要Model
:
module App {
requires Model;
}
要使此工作,您还必须避免通过所提供的代码第二次加载Model
。这可以通过(a)而不是将其放入"\model\lib\EC"
中来实现。或者通过(b)改变Configuration.resolve()
中ModuleFinder
s的顺序:
Configuration cf = parent.configuration().resolve(ModuleFinder.of(), finder. Set.of("Implementation"));
这样,Implementation
将使用在调用代码的模块路径中存在的Model
版本,而不是通过finder
加载它。
解决方案2:使用代理这要求Foo
是一个接口。
你可以有一个方法proxyOf
,看起来像这样:
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);
}
这样你可以有两个版本的Foo
。您只需要确定,应用程序代码实际上被允许访问方法delegate
。因此,它(以及包含它的实现类)必须是public
,或者在调用它之前需要一个额外的delegate.setAccessible(true);
。如果应用程序位于模块内(如解决方案1),则必须通过opens
子句打开delegate
包:
module Implementation {
requires transitive Model;
exports org.example.impl;
opens org.example.impl;
}
请不要认为存在主要缺陷代理解决方案中的:只要您传递JRE的类型,它就能很好地工作,但是一旦您传递自己的类型,method.getParameterTypes()
就会返回错误的类型,导致NoSuchMethodException
。即使您设法获得了正确的类型,您也需要传递该类型的代理。然后,所有这些将变得非常复杂,你将以自己的"来回代理"告终。框架。
最后指出
不幸的是,你没有描述你实际上想要实现什么。所以我们不知道你想用它解决什么问题,为什么你需要上面的代码。老实说,让别人来维护你的代码对我来说是相当困难的。也许你可以通过使用ServiceLoader
来实现同样的事情?