具有多个类加载器的Java ServiceLoader

Jör*_*ann 36 java classloader serviceloader

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

private static ServiceLoader<CodecSet> codecSetLoader = ServiceLoader.load(CodecSet.class);
Run Code Online (Sandbox Code Playgroud)

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

始终创建新的ServiceLoader似乎是浪费性能,因为它必须每次枚举和解析服务文件.编辑:这甚至可能是一个很大的性能问题,如关于java的XPath实现的答案所示.

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

Dav*_*ins 61

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);
Run Code Online (Sandbox Code Playgroud)

这将找到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);
Run Code Online (Sandbox Code Playgroud)

这里,只有在使用此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();
Run Code Online (Sandbox Code Playgroud)

替代"服务"风格

假设您想应用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");
Run Code Online (Sandbox Code Playgroud)

替代"服务"风格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");
Run Code Online (Sandbox Code Playgroud)

以下是如何使用这些属性xbean-reflect,另一个可以为您提供无框架IoC的小库.你只需给它类名和一些名称值对,它将构造和注入.

ObjectRecipe recipe = new ObjectRecipe(redDefinition.remove("className").toString());
recipe.setAllProperties(redDefinition);

Plugin red = (Plugin) recipe.create();
red.start();
Run Code Online (Sandbox Code Playgroud)

以下是长篇形式的"拼写":

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();
Run Code Online (Sandbox Code Playgroud)

xbean-reflect库是内置JavaBeans API之外的一步,但更好一点,不需要你一直到像Guice或Spring这样的全面的IoC框架.它支持工厂方法和构造函数args以及setter/field注入.

为什么ServiceLoader如此有限?

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

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

类路径范围

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

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

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

将URL缩小到你真正关心的5或12,你可以做各种服务加载,从不注意到命中.

  • 在使用ResourceFinder最新版本奋斗了一天之后,我在使用ResourceFinder时遇到了很多麻烦.它似乎没有正确处理META-INF/services文件的注释,换行等.我最终使用了JDK 6的ServiceLoader,它第一次运行得非常好.我在使用ServiceLoader的大型项目中没有看到任何性能问题,因此我将坚持使用JDK解决方案. (7认同)

Pet*_*ter 6

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


Cla*_*Bao 3

我真的很喜欢尼尔在评论中添加的链接中的回答。由于我在最近的项目中也有同样的经历。

“使用 ServiceLoader 时要记住的另一件事是尝试抽象查找机制。发布机制非常好、干净且具有声明性。但是查找(通过 java.util.ServiceLoader)非常丑陋,作为类路径实现如果您将代码放入任何不具有全局可见性的环境(例如 OSGi 或 Java EE)中,扫描器会严重崩溃。如果您的代码与此纠缠在一起,那么您以后在 OSGi 上运行它就会遇到困难。更好编写一个抽象,到时候你可以替换它。”

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

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