具有多个类加载器的Java ServiceLoader

时间:2011-08-12 11:31:55

标签: java classloader serviceloader

在具有多个ClassLoader的环境中使用ServiceLoader的最佳做法是什么?文档建议在初始化时创建并保存单个服务实例:

private static ServiceLoader<CodecSet> codecSetLoader = ServiceLoader.load(CodecSet.class);

这将使用当前上下文类加载器初始化ServiceLoader。现在假设此片段包含在使用Web容器中的共享类加载器加载的类中,并且多个Web应用程序想要定义自己的服务实现。这些不会在上面的代码中被提取,甚至可能使用第一个webapps上下文类加载器初始化加载器并向其他用户提供错误的实现。

始终创建新的ServiceLoader似乎是浪费性能,因为它必须每次枚举和解析服务文件。 修改:这可能是一个很大的性能问题,如this answer regarding java's XPath implementation所示。

其他图书馆如何处理这个问题?他们是否缓存每个类加载器的实现,他们是否每次都重新分析它们的配置,还是只是忽略了这个问题而只适用于一个类加载器?

5 个答案:

答案 0 :(得分:58)

在任何情况下,我个人都不喜欢ServiceLoader。这是缓慢且不必要的浪费,你几乎无法做到优化它。

我也发现它有点受限 - 如果你想做的不仅仅是按类型搜索,你真的不得不走开。

xbean-finder的ResourceFinder

  • ResourceFinder是一个独立的java文件,可以替换ServiceLoader的使用。复制/粘贴重用没问题。它是一个java文件,已获得ASL 2.0许可,可从Apache获得。

在我们的注意力范围太短之前,以下是它如何取代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()有多难?不难。
  • 可以自由地只实例化你想要的那些。
  • 现在你可以支持构造函数args了!

缩小搜索范围

如果您只想查看特定网址,可以轻松完成:

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类型概念来重新设计网址处理,并为特定协议查找/加载java.net.URLStreamHandler

以下是在类路径中布局服务的方法:

  • META-INF/java.net.URLStreamHandler/foo
  • META-INF/java.net.URLStreamHandler/bar
  • META-INF/java.net.URLStreamHandler/baz

其中foo是一个纯文本文件,其中包含与以前一样的服务实现的名称。现在说某人创建了一个foo://...网址。我们可以通过以下方式快速找到实施方案:

ResourceFinder finder = new ResourceFinder("META-INF/");
Map<String, Class<? extends URLStreamHandler>> handlers = finder.mapAllImplementations(URLStreamHandler.class);
Class<? extends URLStreamHandler> fooHandler = handlers.get("foo");

备用“服务”样式2

假设您想在服务文件中放置一些配置信息,因此它不仅包含一个类名。这是一种将服务解析为属性文件的替代样式。按照惯例,一个键是类名,其他键是可注入的属性。

所以这里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 injection。

为什么ServiceLoader如此受限?

JVM中不推荐使用的代码会破坏Java语言本身。在添加到JVM之前,很多东西都会修剪到骨骼中,因为之后无法修剪它们。 ServiceLoader就是一个很好的例子。 API是有限的,OpenJDK实现大约500行,包括javadoc。

那里没有什么花哨的东西,取而代之的很容易。如果它不适合您,请不要使用它。

类路径范围

除了API之外,纯粹的实用性缩小搜索到的URL的范围是这个问题的真正解决方案。应用服务器本身拥有相当多的URL,不包括应用程序中的jar。例如,OSX上的Tomcat 7仅在StandardClassLoader中有大约40个URL(这是所有webapp类加载器的父级)。

即使是简单的搜索,您的应用服务器也越大。

如果您打算搜索多个条目,缓存无济于事。同样,它可以添加一些不良泄漏。可能是一个真正的双输局面。

将您真正关心的网址缩小到5或12,您可以进行各种服务加载,从不会注意到该网页。

答案 1 :(得分:5)

您是否尝试过使用这两个参数版本,以便指定要使用的类加载器?即,java.util.ServiceLoader.load(Class, ClassLoader)

答案 2 :(得分:4)

亩。

在1x WebContainer&lt; - &gt;中在Nx WebApplication系统中,在WebContainer中实例化的ServiceLoader将不会获取WebApplications中定义的任何类,只是容器中的那些类。在WebApplication中实例化的ServiceLoader将检测应用程序中定义的类以及容器中定义的类。

请记住,WebApplications需要保持独立,按照这种方式设计,如果你试图绕过它, 会破坏它们,并且它们不是扩展容器的方法和系统 - 如果您的库是一个简单的Jar,只需将其放入容器的相应扩展文件夹中即可。

答案 3 :(得分:3)

我非常喜欢Neil在我评论中添加的链接中的答案。由于我在最近的项目中有相同的经历。

“ServiceLoader要记住的另一件事是尝试抽象查找机制。发布机制非常好,干净且声明性。但查找(通过java.util.ServiceLoader)和地狱一样丑陋,实现作为一个类路径扫描程序,如果你把代码放到没有全局可见性的任何环境(如OSGi或Java EE)中,可能会破坏性。如果你的代码与之纠缠在一起,那么你将很难在OSGi上运行它后来。最好写一个抽象,你可以在时机成熟时取而代之。“

我实际上在OSGi环境中遇到了这个问题,实际上它只是我们项目中的eclipse。但我很幸运地及时修复了它。我的解决方法是使用我想要加载的插件中的一个类,并从中获取classLoader。这将是一个有效的解决方案。我没有使用标准的ServiceLoader,但我的过程非常相似,使用属性来定义我需要加载的插件类。我知道还有另一种方法可以了解每个插件的类加载器。但至少我不需要使用它。

老实说,我不喜欢ServiceLoader中使用的泛型。因为它限制了一个ServiceLoader只能处理一个接口的类。它真的有用吗?在我的实现中,它不会强迫您受此限制。我只是使用一个加载器实现来加载所有的插件类。我没有看到使用两个或更多的原因。由于消费者可以从配置文件中了解接口和实现之间的关系。

答案 4 :(得分:1)

这个问题似乎比我最初预料的要复杂得多。我看到了 它,有3种可能的策略来处理ServiceLoaders。

  1. 使用静态ServiceLoader实例,仅支持从中加载类 与持有ServiceLoader引用的类加载器相同的类加载器。这个

    • 服务配置和实现位于共享的类加载器中 并且所有子类加载器都使用相同的实现。这个例子 在文档中面向着用例。

    • 将配置和实现放入每个子类加载器中 在WEB-INF/lib

    • 中的每个webapp上部署

    在此方案中,无法在共享类加载器中部署服务 并让每个webapp选择自己的服务实现。

  2. 在每次访问时初始化ServiceLoader,传递上下文类加载器 当前线程作为第二个参数。这种方法是JAXP 和JAXB apis,虽然他们使用自己的FactoryFinder实现 而不是ServiceLoader。因此可以将xml解析器与webapp捆绑在一起 并自动获取它,例如DocumentBuilderFactory#newInstance

    此查找具有performance impact,但是在xml解析的情况下 与需要的时间相比,查找实施的时间很短 实际上解析一个xml文档。在图书馆,我正在设想工厂 本身非常简单,因此查找时间将主导性能。

  3. 以某种方式使用上下文类加载器作为键来缓存实现类。 我不完全确定在没有这些情况下是否有可能 导致任何内存泄漏。

  4. 总之,我可能会忽略这个问题并要求库 部署在每个webapp中,即上面的选项1b。