Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 289
Jörn Horstmann Avatar asked Aug 12 '11 11:08

Jörn Horstmann


People also ask

Can you have more than one ClassLoader in Java?

Each application might use different versions of the same libraries, and must thus have a different classloader from the others in order to be able to have different versions of the same classes in a single JVM. but the web server has its own loader.it can have several classloaders.

How many Classloaders are there in Java?

There are three types of built-in ClassLoader in Java. Bootstrap Class Loader – It loads JDK internal classes. It loads rt. jar and other core classes for example java.

How does Java ServiceLoader work?

It searches for service providers on your application's class path or in your runtime environment's extensions directory. It loads them and enables your application to use the provider's APIs. If you add new providers to the class path or runtime extension directory, the ServiceLoader class finds them.

Can we create our own ClassLoader in Java?

We will create our own ClassLoader by extending the ClassLoader class and overriding the loadClass(String name) method. If the class name will start from com. journaldev then we will load it using our custom class loader or else we will invoke the parent ClassLoader loadClass() method to load the class.


2 Answers

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.

like image 132
David Blevins Avatar answered Oct 02 '22 00:10

David Blevins


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

like image 37
Peter Avatar answered Oct 02 '22 00:10

Peter