Java ServiceLoader with multiple Classloaders
Asked Answered
H

5

38

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 performance 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?

Hectograph answered 12/8, 2011 at 11:31 Comment(4)
hi Horstmann Look this you.#1960491Uranyl
Take a look at Apache Camel - it handles this problem elegantly, both in JEE apps and OSGi (note there's a specific module for jboss classloading in the Camel extras package hosted on googlecode)Dysentery
@earcam: can you point me to some more details on how Apache Camel solves this problem?Vesture
@Jörn: there's not a great deal of documentation about (as not part of public API), apart from camel.apache.org/pluggable-class-resolvers.html Take a look at org.apache.camel.spi.ClassResolver and it's subclasses (several in camel-core (e.g. OSGi),see camel-extras project code.google.com/a/apache-extras.org/p/camel-extra/source/browse/… ). Alternatively JBossOSGi claims to be handle ServiceLoader jbossosgi.blogspot.com/2010/01/…Dysentery
F
69

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.

Flory answered 29/8, 2011 at 23:28 Comment(6)
Nice answer, I'm not sure yet if I will use this class but I think it deserves the bounty. Does this also work in a container like JBoss which I think uses its own vfs url scheme internally?Vesture
That might throw it for a loop as it understands "jar:" and "file:" internally. That said, it isn't a lot of code and adding support for another URL scheme would be easy if it could be mapped back to "file:" and "jar:" eventually. If you hack, issue a pull request bit.ly/qfJQXzFlory
After struggling for a day with ResourceFinder latest version, I had a lot of trouble using ResourceFinder. It does not seem to process the META-INF/services file properly for comments, newlines etc. I ended up using ServiceLoader from JDK 6 and it worked perfectly first time. I do not see any performance issues in my large project with ServiceLoader so I will stick with the JDK solution here.Bagel
Thanks for the note though, very appreciated. I'll make sure those things are fixed.Flory
So is this able to find an implementation class itself or the the properties file that points to the location?Madonnamadora
Just noticed this project from the cloudbees guys and thought of this SO post: github.com/jglick/sezpozGogol
L
8

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

Lattermost answered 25/8, 2011 at 23:30 Comment(1)
concise and flawless!Baecher
T
4

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.

Taritariff answered 23/8, 2011 at 10:38 Comment(0)
U
3

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.

Uranyl answered 16/8, 2011 at 13:39 Comment(3)
The "breaking horrible" part is exactly what I'm trying to avoid. The classloader hierarchy should not matter for the user of my library. I would like it to work the same in a standalone script and inside a jee container, without requiring additional frameworks or containers like osgi.Vesture
@Horst I have reviewed the factory config access things in your link. Luckily we had loaded the config into memory since we didn't use the standard ServiceLoader.Uranyl
I am not sure how in detail Neil did. But I know how I did it. In my implemtation. I will load those plugins with my own classloader.And for those dependencies, luckily they are together with the Loader class itself, so I can rely on the classloader of Loader class to find all the dependency class. And those plugin classes are cached in a map with classname as key. So I don't think I have performance concern. It don't rely on any framework. But you just need to ensure it can work in OSGi or JEE environment.Uranyl
H
1

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.

Hectograph answered 12/8, 2011 at 11:32 Comment(4)
ServiceLoader.load(CodecSet.class); will use classload in the current thread context.ServiceLoader.load(CodecSet.class,ClassLoader cl); will load the class with classloader you specified. So I think you can use the second API, with classloader used in each webapp and certainly each webapp should do this in the initial time. Which means they use different service loader instance. But you can put the libraries in a shared place. Is it right?Uranyl
If the static ServiceLoader reference is in a shared class then it would initialize itself with the context classloader of the first thread accessing it and perhaps loading an implementation that is not accessible to other webapps. Furthermore it would contain a reference to this webapps classloader causing a memory leak on redeploy.Vesture
The fact is the classloaders used in webapps are different with the classloader used in loading the shared library. So if the classes in the shared library has dependencies on the classes in the webapp then it will be a big trouble to implement the correct classloader. But if the classes in the shared library are independent ,then it is possible that we use a system class loader to load them. Then these classes can be shared among all webapps?Uranyl
Using classloaders as a key is going to be rather leaky. Better to use a WeakHashMap<Integer, Loader>, and use the hashCode of the classloader as key, instead of an instance of said classloader. That way, if a thread gets GC'd, it's classloader won't leak, and if no other threads see the Loader, it will get GC'd as well.Cupronickel

© 2022 - 2024 — McMap. All rights reserved.