37

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

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

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

总是创建一个新的 ServiceLoader 似乎会浪费性能,因为它每次都必须枚举和解析服务文件。编辑:这甚至可能是一个很大的性能问题,如this answer about java's XPath implementation所示。

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

4

5 回答 5

67

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

我也发现它有点受限——如果你想做的不仅仅是按类型搜索,你真的必须不遗余力。

xbean-finder 的资源查找器

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

在这里,将只搜索“some.jar”这个 ResourceFinder 实例的任何用法。

还有一个名为的便利类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();

替代“服务”风格

假设您想应用类型概念来重新设计 URL 处理并为特定协议ServiceLoader查找/加载。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");

替代“服务”风格 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 更上一层楼,但更好一点,无需您一直使用完整的 IoC 框架,如 Guice 或 Spring。它支持工厂方法和构造函数参数以及设置器/字段注入。

为什么 ServiceLoader 如此有限?

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

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

类路径范围

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

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

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

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

于 2011-08-29T23:28:33.767 回答
6

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

于 2011-08-25T23:30:13.440 回答
4

亩。

在 1x WebContainer <-> Nx WebApplication 系统中,在 WebContainer 中实例化的 ServiceLoader 不会拾取 WebApplications 中定义的任何类,只会拾取容器中的类。在 WebApplication 中实例化的 ServiceLoader 将检测应用程序中定义的类以及容器中定义的类。

请记住,WebApplications 需要保持独立,这样设计,如果你试图规避它,事情就会崩溃,而且它们不是可用于扩展容器的方法和系统 - 如果你的库是一个简单的 Jar,只需放下将其放入容器的相应扩展文件夹中。

于 2011-08-23T10:38:27.537 回答
3

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

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

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

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

于 2011-08-16T13:39:38.047 回答
1

这个问题似乎比我最初预期的要复杂。如我所见,处理 ServiceLoaders 有 3 种可能的策略。

  1. 使用静态 ServiceLoader 实例,并且仅支持从与持有 ServiceLoader 引用的类加载器相同的类加载器加载类。这将在什么时候起作用

    • 服务配置和实现在共享类加载器中,所有子类加载器都使用相同的实现。文档中的示例是针对这个用例的。

      或者

    • 配置和实现被放入每个子类加载器中,并沿每个 webapp 部署在WEB-INF/lib.

    在这种情况下,不可能在共享类加载器中部署服务并让每个 webapp 选择自己的服务实现。

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

    这种查找对性能有影响,但在 xml 解析的情况下,查找实现的时间与实际解析 xml 文档所需的时间相比是很小的。在库中,我设想工厂本身非常简单,因此查找时间将主导性能。

  3. 以某种方式缓存以上下文类加载器为键的实现类。我不完全确定在上述所有情况下这是否可能而不会导致任何内存泄漏。

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

于 2011-08-28T12:17:19.167 回答