开发者

Java ServiceLoader with multiple Classloaders

What are the best practices for using ServiceLoader in an Environment with multiple ClassLoaders? The documentation recommends to create and save a single service instance at initialization:

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

This would initialize the ServiceLoader using the current context classloader. Now suppose this snippet is contained in a class loaded using a shared classloader in a web container and multiple web applications want to define their own service implementations. These would not get picked up in the above code, it might even be possible that the loader gets initialized using the first webapps context classloader and provide the wrong implementation to other users.

Always creating a new ServiceLoader seems wasteful performance wise since it has to enumerate and parse service files each time. Edit: This can even be a big p开发者_如何学编程erformance problem as shown in this answer regarding java's XPath implementation.

How do other libraries handle this? Do they cache the implementations per classloader, do they reparse their configuration everytime or do they simply ignore this problem and only work for one classloader?


I personally do not like the ServiceLoader under any circumstances. It's slow and needlessly wasteful and there is little you can do to optimize it.

I also find it a bit limited -- you really have to go out of your way if you want to do more than search by type alone.

xbean-finder's ResourceFinder

  • ResourceFinder is a self-contained java file capable of replacing ServiceLoader usage. Copy/paste reuse is no problem. It's one java file and is ASL 2.0 licensed and available from Apache.

Before our attention spans get too short, here's how it can replace a ServiceLoader

ResourceFinder finder = new ResourceFinder("META-INF/services/");
List<Class<? extends Plugin>> impls = finder.findAllImplementations(Plugin.class);

This will find all of the META-INF/services/org.acme.Plugin implementations in your classpath.

Note it does not actually instantiate all the instances. Pick the one(s) you want and you're one newInstance() call away from having an instance.

Why is this nice?

  • How hard is it to call newInstance() with proper exception handling? Not hard.
  • Having the freedom to instantiate only the ones you want is nice.
  • Now you can support constructor args!

Narrowing search scope

If you want to just check specific URLs you can do so easily:

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

Here, only the 'some.jar' will be searched on any usage of this ResourceFinder instance.

There's also a convenience class called UrlSet which can make selecting URLs from the classpath very easy.

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

Alternate "service" styles

Say you wanted to apply the ServiceLoader type concept to redesign URL handling and find/load the java.net.URLStreamHandler for a specific protocol.

Here's how you might layout the services in your classpath:

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

Where foo is a plain text file that contains the name of the service implementation just as before. Now say someone creates a foo://... URL. We can find the implementation for that quickly, via:

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

Alternate "service" styles 2

Say you wanted to put some configuration information in your service file, so it contains more than just a classname. Here's an alternate style that resolves services to properties files. By convention one key would be the class names and the other keys would be injectable properties.

So here red is a properties file

  • META-INF/org.acme.Plugin/red
  • META-INF/org.acme.Plugin/blue
  • META-INF/org.acme.Plugin/green

You can look things up similarly as before.

ResourceFinder finder = new ResourceFinder("META-INF/");

Map<String,Properties> plugins = finder.mapAllProperties(Plugin.class.getName());
Properties redDefinition = plugins.get("red");

Here's how you could use those properties with xbean-reflect, another little library that can give you framework-free IoC. You just give it the class name and some name value pairs and it will construct and inject.

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

Plugin red = (Plugin) recipe.create();
red.start();

Here's how that might look "spelled" out in long form:

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

The xbean-reflect library is a step beyond the built-in JavaBeans API, but a bit better without requiring you to go all the way to a full-on IoC framework like Guice or Spring. It supports factory methods and constructor args and setter/field injection.

Why is the ServiceLoader so limited?

Deprecated code in the JVM damages the Java language itself. Many things are trimmed to the bone before being added to the JVM, because you cannot trim them after. The ServiceLoader is a prime example of that. The API is limited and OpenJDK implementation is somewhere around 500 lines including javadoc.

There's nothing fancy there and replacing it is easy. If it doesn't work for you, don't use it.

Classpath scope

APIs aside, in pure practicality narrowing the scope of the URLs searched is the true solution to this problem. App Servers have quite a lot of URLs all by themselves, not including the jars in your application. Tomcat 7 on OSX for example has about 40~ URLs in the StandardClassLoader alone (this is the parent to all webapp classloaders).

The bigger your app server the longer even a simple search will take.

Caching doesn't help if you intend to search for more than one entry. As well, it can add some bad leaks. Can be a real lose-lose scenario.

Narrow the URLs down to the 5 or 12 that you really care about and you can do all sorts of service loading and never notice the hit.


Have you tried using the two argument version so that you can specify which classloader to use? Ie, java.util.ServiceLoader.load(Class, ClassLoader)


Mu.

In a 1x WebContainer <-> Nx WebApplication system, the ServiceLoader instantiated in the WebContainer will not pick up any classes defined in WebApplications, just those in the container. A ServiceLoader instantiated in a WebApplication will detect classes defined in the application in addition to those defined in the container.

Keep in mind WebApplications will need to be kept separate, are designed that way, things will break if you try and circumvent that, and they are not the method and system available to extend the container - if your library is a simple Jar, just drop it into the appropriate extension folder of the container.


I really like Neil's answer in the link I added in my comment. Due to I have same experences in my recent project.

"Another thing to bear in mind with ServiceLoader is to try to abstract the lookup mechanism. The publish mechanism is quite nice and clean and declarative. But the lookup (via java.util.ServiceLoader) is as ugly as hell, implemented as a classpath scanner that breaks horribly if you put the code into any environment (such as OSGi or Java EE) that does not have global visibility. If your code gets tangled up with that then you'll have a hard time running it on OSGi later. Better to write an abstraction that you can replace when the time comes."

I actually met this problem in OSGi environment actually it's just eclipse in our project. But I luckily fixed it in a timely fashion. My workaround is using one class from the plugin I want to load ,and get classLoader from it. That will be a valid fix. I didn't use the standard ServiceLoader, But my process is quite similiar, use a properties to define the plugin classes I need to load. And I know there is another way to know each plugin's classloader. But at least I don't need to use that.

Honest, I don't like the generics used in ServiceLoader. Because it limited that one ServiceLoader can only handle classes for one interface. Well is it really useful? In my implementation, it don't force you by this limitation. I just use one implementation of loader to load all the plugin classes. I don't see the reason to use two or more. Due to the consumer can know from the config files about the relationships between interfaces and implementation.


This question seem to be more complicated than I first anticipated. As I see it, there are 3 possible strategies for dealing with ServiceLoaders.

  1. Use a static ServiceLoader instance and only support loading classes from the same classloader as the one holding the ServiceLoader reference. This would work when

    • The service configuration and implementation are in a shared classloader and all child classloaders are using the same implementation. The example in the documentation is geared towards theis use case.

      Or

    • Configuration and implementation are put into each child classloader and deployed along each webapp in WEB-INF/lib.

    In this scenario it is not possible to deploy the service in a shared classloader and let each webapp choose its own service implementation.

  2. Initialize the ServiceLoader on each access passing the context classloader of the current thread as the second parameter. This approach is taken be the JAXP and JAXB apis, although they are using their own FactoryFinder implementation instead of ServiceLoader. So it is possible to bundle a xml parser with a webapp and have it automatically get picked up for example by DocumentBuilderFactory#newInstance.

    This lookup has a performance impact, but in the case of xml parsing the time to look up the implementation is small compared to the time needed to actually parse a xml document. In the library I'm envisioning the factory itself is pretty simple so the lookup time would dominate the performance.

  3. Somehow cache the implementation class with the context classloader as the key. I'm not entirely sure if this is possible in all the above cases without causing any memory leaks.

In conclusion, I will probably be ignoring this problem and require that the library gets deployed inside each webapp, i.e. option 1b above.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜