[英]Java ServiceLoader with multiple Classloaders
在具有多个ClassLoader的环境中使用ServiceLoader的最佳实践是什么? 文档建议在初始化时创建并保存单个服务实例:
private static ServiceLoader<CodecSet> codecSetLoader = ServiceLoader.load(CodecSet.class);
这将使用当前上下文类加载器初始化ServiceLoader。 现在假设此片段包含在使用Web容器中的共享类加载器加载的类中,并且多个Web应用程序想要定义自己的服务实现。 这些不会在上面的代码中被提取,甚至可能使用第一个webapps上下文类加载器初始化加载器并向其他用户提供错误的实现。
始终创建新的ServiceLoader似乎是浪费性能,因为它必须每次枚举和解析服务文件。 编辑:这甚至可能是一个很大的性能问题,如关于java的XPath实现的答案所示。
其他库如何处理这个? 他们是否缓存每个类加载器的实现,他们每次都重新分析他们的配置还是只是忽略这个问题而只适用于一个类加载器?
在任何情况下我个人都不喜欢ServiceLoader
。 这是缓慢而且不必要的浪费,你几乎无法做到优化它。
我也发现它有点受限 - 如果你想做的不仅仅是按类型搜索,你真的不得不走开。
在我们的注意力跨度太短之前,以下是它如何取代ServiceLoader
ResourceFinder finder = new ResourceFinder("META-INF/services/");
List<Class<? extends Plugin>> impls = finder.findAllImplementations(Plugin.class);
这将在类路径中找到所有META-INF/services/org.acme.Plugin
实现。
请注意,它实际上并未实例化所有实例。 选择你想要的那个,你就是一个newInstance()
调用,而不是拥有一个实例。
为什么这很好?
newInstance()
有多难? 不难。 如果您只想检查特定网址,则可以轻松完成:
URL url = new File("some.jar").toURI().toURL();
ResourceFinder finder = new ResourceFinder("META-INF/services/", url);
这里,只有在使用此ResourceFinder实例时才会搜索'some.jar'。
还有一个名为UrlSet
的便捷类,可以很容易地从类路径中选择URL。
ClassLoader webAppClassLoader = Thread.currentThread().getContextClassLoader();
UrlSet urlSet = new UrlSet(webAppClassLoader);
urlSet = urlSet.exclude(webAppClassLoader.getParent());
urlSet = urlSet.matching(".*acme-.*.jar");
List<URL> urls = urlSet.getUrls();
假设您希望应用ServiceLoader
类型概念来重新设计URL处理并查找/加载特定协议的java.net.URLStreamHandler
。
以下是在类路径中布局服务的方法:
META-INF/java.net.URLStreamHandler/foo
META-INF/java.net.URLStreamHandler/bar
META-INF/java.net.URLStreamHandler/baz
其中foo
是一个纯文本文件,其中包含与以前一样的服务实现的名称。 现在说有人创建了一个foo://...
URL。 我们可以通过以下方式快速找到实施方案:
ResourceFinder finder = new ResourceFinder("META-INF/");
Map<String, Class<? extends URLStreamHandler>> handlers = finder.mapAllImplementations(URLStreamHandler.class);
Class<? extends URLStreamHandler> fooHandler = handlers.get("foo");
假设您想在服务文件中放置一些配置信息,因此它不仅仅包含一个类名。 这是一种将服务解析为属性文件的替代样式。 按照惯例,一个键是类名,其他键是可注入的属性。
所以这里red
是一个属性文件
META-INF/org.acme.Plugin/red
META-INF/org.acme.Plugin/blue
META-INF/org.acme.Plugin/green
你可以像以前一样看待事物。
ResourceFinder finder = new ResourceFinder("META-INF/");
Map<String,Properties> plugins = finder.mapAllProperties(Plugin.class.getName());
Properties redDefinition = plugins.get("red");
以下是如何将这些属性与xbean-reflect
一起使用,这是另一个可以为您提供无框架IoC的小库。 你只需给它类名和一些名称值对,它将构造和注入。
ObjectRecipe recipe = new ObjectRecipe(redDefinition.remove("className").toString());
recipe.setAllProperties(redDefinition);
Plugin red = (Plugin) recipe.create();
red.start();
以下是长篇形式的“拼写”:
ObjectRecipe recipe = new ObjectRecipe("com.example.plugins.RedPlugin");
recipe.setProperty("myDateField","2011-08-29");
recipe.setProperty("myIntField","100");
recipe.setProperty("myBooleanField","true");
recipe.setProperty("myUrlField","http://www.stackoverflow.com");
Plugin red = (Plugin) recipe.create();
red.start();
xbean-reflect
库是一个超越内置JavaBeans API的步骤,但更好一点,而不需要你一直到像Guice或Spring这样的全功能IoC框架。 它支持工厂方法和构造函数args以及setter / field注入。
JVM中不推荐使用的代码会破坏Java语言本身。 在添加到JVM之前,很多东西都会修剪到骨骼中,因为之后无法修剪它们。 ServiceLoader
就是一个很好的例子。 API是有限的,OpenJDK实现大约500行,包括javadoc。
那里没有任何花哨的东西,取而代之的很容易。 如果它不适合您,请不要使用它。
除了API,纯粹的实用性缩小搜索URL的范围是这个问题的真正解决方案。 应用服务器本身拥有相当多的URL,不包括应用程序中的jar。 例如,OSX上的Tomcat 7仅在StandardClassLoader中有大约40个URL(这是所有webapp类加载器的父级)。
您的应用服务器越大,即使是简单的搜索也会越长。
如果您打算搜索多个条目,缓存无济于事。 同样,它可以添加一些不良泄漏。 可以是一个真正的双输局面。
将URL缩小到你真正关心的5或12,你可以做各种服务加载,从不注意到命中。
您是否尝试过使用这两个参数版本,以便指定要使用的类加载器? 即, java.util.ServiceLoader.load(Class, ClassLoader)
亩。
在1x WebContainer < - > Nx WebApplication系统中,在WebContainer中实例化的ServiceLoader将不会获取WebApplications中定义的任何类,只接收容器中的类。 在WebApplication中实例化的ServiceLoader将检测应用程序中定义的类以及容器中定义的类。
请记住,WebApplications需要保持独立,以这种方式设计,如果你试图绕过它就会破坏它们,并且它们不是扩展容器的方法和系统 - 如果你的库是一个简单的Jar,只需要删除它进入容器的相应扩展文件夹。
我非常喜欢Neil在我评论中添加的链接中的答案。 由于我在最近的项目中有相同的经历。
“使用ServiceLoader要记住的另一件事是尝试抽象查找机制。发布机制非常好,干净且声明性。但查找(通过java.util.ServiceLoader)和地狱一样难看,实现为类路径如果你将代码放入没有全局可见性的任何环境(例如OSGi或Java EE)中,那么扫描器可能会破坏。如果你的代码与之纠缠在一起,那么你将很难在OSGi上运行它。编写一个可以在时机成熟时替换的抽象。“
我实际上在OSGi环境中遇到了这个问题,实际上它只是我们项目中的eclipse。 但我很幸运地及时修复了它。 我的解决方法是使用我想要加载的插件中的一个类,并从中获取classLoader。 这将是一个有效的解决方案。 我没有使用标准的ServiceLoader,但我的过程非常类似,使用属性来定义我需要加载的插件类。 我知道还有另一种方法可以了解每个插件的类加载器。 但至少我不需要使用它。
老实说,我不喜欢ServiceLoader中使用的泛型。 因为它限制了一个ServiceLoader只能处理一个接口的类。 它真的有用吗? 在我的实现中,它不会强迫您受此限制。 我只是使用一个加载器实现来加载所有的插件类。 我没有看到使用两个或更多的原因。 由于消费者可以从配置文件中了解接口和实现之间的关系。
这个问题似乎比我最初预料的要复杂得多。 在我看来,处理ServiceLoader有3种可能的策略。
使用静态ServiceLoader实例,仅支持从与持有ServiceLoader引用的类加载器相同的类加载器中加载类。 这将适用于
服务配置和实现位于共享的类加载器中,所有子类加载器都使用相同的实现。 文档中的示例适用于该用例。
要么
配置和实现被放入每个子类加载器中,并在WEB-INF/lib
每个webapp上进行部署。
在这种情况下,无法在共享类加载器中部署服务,并让每个Web应用程序选择自己的服务实现。
在每次访问时初始化ServiceLoader,将当前线程的上下文类加载器作为第二个参数传递。 这种方法是JAXP和JAXB apis,尽管它们使用自己的FactoryFinder实现而不是ServiceLoader。 因此,可以将xml解析器与webapp捆绑在一起,并通过DocumentBuilderFactory#newInstance
自动获取它。
此查找会对性能产生影响 ,但在xml解析的情况下,与实际解析xml文档所需的时间相比,查找实现的时间很短。 在库中,我设想工厂本身非常简单,因此查找时间将主导性能。
以某种方式缓存实现类,并将上下文类加载器作为键。 我不完全确定在上述所有情况下是否可行,而不会导致任何内存泄漏。
总之,我可能会忽略这个问题,并要求在每个webapp中部署库,即上面的选项1b。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.