具有多个类装入器的 Java ServiceLoaderxbean-finder's ResourceFinder为什么ServiceLoader如此有限?类路径作用域

2022-09-01 04:15:32

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

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

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

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

其他库如何处理此问题?他们是缓存每个类装入器的实现,还是每次都重新解析其配置,还是只是忽略这个问题而只适用于一个类装入器?


答案 1

我个人在任何情况下都不喜欢。它很慢,而且不必要地浪费,你几乎无法优化它。ServiceLoader

我也发现它有点有限 - 如果你想做更多的事情,而不是仅仅按类型搜索,你真的必须竭尽全力。

xbean-finder's 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()
  • 可以自由地只实例化你想要的那些是很好的。
  • 现在你可以支持构造函数参数了!

缩小搜索范围

如果您只想检查特定的URL,您可以轻松完成:

URL url = new File("some.jar").toURI().toURL();
ResourceFinder finder = new ResourceFinder("META-INF/services/", url);

在这里,将仅搜索“一些.jar”,以使用此 ResourceFinder 实例的任何使用情况。

还有一个名为方便类的类,它可以使从类路径中选择URL变得非常容易。UrlSet

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();

替代的“服务”样式

假设您要应用类型概念来重新设计 URL 处理,并为特定协议查找/加载 。ServiceLoaderjava.net.URLStreamHandler

下面介绍了如何在类路径中布局服务:

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

Where 是一个纯文本文件,它像以前一样包含服务实现的名称。现在假设有人创建了一个 URL。我们可以通过以下方式快速找到它的实现:foofoo://...

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");

下面介绍如何将这些属性与 另一个可以为您提供无框架 IoC 的小库一起使用。您只需为它提供类名和一些名称值对,它就会构造和注入。xbean-reflect

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();

该库比内置的JavaBeans API更进一步,但更好一些,而不需要您一直到像Guice或Spring这样的完整IoC框架。它支持工厂方法和构造函数参数以及 setter/field 注入。xbean-reflect

为什么ServiceLoader如此有限?

JVM 中不推荐使用的代码会损坏 Java 语言本身。许多东西在被添加到JVM之前被修剪到骨头上,因为你不能在之后修剪它们。这是一个典型的例子。API是有限的,OpenJDK实现大约500行,包括javadoc。ServiceLoader

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

类路径作用域

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

您的应用程序服务器越大,即使是简单的搜索也需要更长的时间。

如果您打算搜索多个条目,则缓存没有帮助。同样,它可能会增加一些不良泄漏。可能是一个真正的双输场景。

将URL缩小到您真正关心的5或12,您可以执行各种服务加载,并且永远不会注意到命中。


答案 2

您是否尝试过使用两个参数版本,以便可以指定要使用的类装入器?下摆java.util.ServiceLoader.load(Class, ClassLoader)


推荐