For those looking for a way to create modular and extendible code using some sort of ‘plugin’ architecture, one of the possibilities is to use the ServiceLoader class.
A service provider is a factory for creating all known implementations of a particular class or interface S. The known implementations are read from a configuration file in META-INF/services/.
Here are some examples of it’s usage in Java:
Creating Extensible Applications With the Java Platform
What I was after was use it in Android, and to be able to put the service implementation classes in their own libray project or jar file, so they could be used independently in a modular fashion. A possible use of this structure is to have different ‘plugins’ for different versions of an app.
There are other similar java plugin frameworks out there, but I thought I’d stick with something that already exists in Java rather than learning yet another API.
However there are some issues with using ServiceLoader in Android, mostly having to do with where to put the configuration file that contains the list of plugin implementation classes. Unfortunately ServiceLoader is hard-coded to look for the file at ‘META-INF/services/’, which makes it hard if we want to make it work in Android.
This article ‘Using serviceloader on android‘ gives some ideas about how use it with Ant. However I wanted a solution that could also be used with Eclipse.
Customizing ServiceLoader
Because ServiceLoader is a final class, it cannot be subclassed but we can still customize it by making our own copy from the Java SDK source code. For example, let’s get a copy of ServiceLoader.java from JDK 1.6 source, and rename it to CustomServiceLoader.
Looking at the source, the first thing we notice is that the location for the config file is hard-coded:
private static final String PREFIX = "META-INF/services/";
Then here is where that location is used (it is only used when hasNext() is called on the iterator due to the lazy initialization pattern):
public boolean hasNext() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
} ...
There are several ways we could customize this class to make it easier to use with Android.
Option 1: Replace the hard-code PREFIX field with your own location
So in our custom class, we could just change the location for the PREFIX constant to point to somewhere else in our classpath. This is the quickest way, but suffers from the same inflexibility as the original ServiceLoader.
private static final String PREFIX = "com.yourdomain.services/";
Since the location is being looked up internally using getResources() or getSystemResources() from the classloader, all we have to do is to make sure the configuration file is in the classpath somewhere, and then tell CustomServiceLoader where to search for it.
Another idea for this option was to do it the ‘Android’ way and use the AssetManager to find the configuration file in the assets directory. This would mean passing the AssetManager as an extra parameter in the public API methods for CustomServiceLoader.
However the Android build process will only take into account the assets directory in the main project and ignore the assets directory in library projects, so the file would have to go in the main project. I prefer the configuration to exist in the library along with it’s code so that it could function as an independent unit, so I did not use the assets folder.
Option 2: Pass the location
Rather than hard-coding the location, we could just pass the location as an additional parameter when you want to load the services. This would mean changing the method signatures to pass the location parameter through (or store it) until it is needed. Hence instead of calling …
CustomServiceLoader.load(MyService.class)
you would call …
CustomServiceLoader.load(MyService.class, "com.yourdomain.services/")
I did not end up using this option, since it would have required more substantial changes to the original class.
This is one of the caveats regarding copying source code and changing it, and why I generally dislike doing this. If you make any significant changes to it, then there is always the chance you will introduce bugs into the code (therefore requires more testing!). Also if the original code base changes, your code will not include those changes.
Making it Modular for Multiple Plugins
The above solutions would work if you only had one plugin (i.e. a jar or project containing some service implementation classes and a configuration file), but what if you wanted to be able to handle any number of plugins. My idea was to have each plugin as a independent unit, each with it’s own configuration file.
Then the app would load all the plugin classes it found and aggregate them to make all the service implementations available.
Why would you want to do this? Well, many Android apps have multiple versions, i.e. a paid vs a free version, or a lite vs a pro version. So you could have 1 plugin for the lite version offering a limited number of services.
Then for the pro/paid version, you could either replace the plugin with another version that offers more services or just add those extra services as addition plugins.
It would require a bit more work on CustomServiceLoader to allow it to handle this situation.
This means that, for instance, if the location you want is at ‘com.yourdomain.services’, then if you have 2 plugin libraries or jars with their configuration files at that same location, then the apk will build since it would consider them to be duplicates:
Error generating final archive: Found duplicate file for APK: com.yourdomain.services/com.yourdomain.plugin.MyService
Let’s have a look at the code again:
private static final String PREFIX = "com.yourdomain.services/";
...
public boolean hasNext() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
} ...
The problem is that the code is only looking at 1 location as specified by the PREFIX constant, but having the same location in multiple plugin libraries will break the build process due to duplicate file problem.
We can put the configuration files in different locations in each plugin library, but then how does CustomServiceLoader find them?
I’m still working on this, but got some ideas:
1. Add some code to be able to handle wildcards in the location.
e.g.
Plugin1 would have the location of it’s configuration file at ‘com.yourdomain.services1/’.
Plugin2 would have the location at ‘com.yourdomain.services2/’.
We could pass is a wildcard location, such as ‘com.yourdomain.services*’.
In the Spring Framework, there are classes such as ResourcePatternResolver and PathMatchingResourcePatternResolver that does something like this. This is what enables Spring to find ApplicationContext.xml in different locations. This seems to be the most flexible way of doing it, but requires more work.
2. Another way would be to use option 2 above (passing the location as a parameter), but instead of just 1 location, pass in multiple locations (in a Collection or array) and aggregate the results of the getResources() calls.
This is easier, but far from ideal, as you have to hard-code the locations of the configuration file in each plugin library somewhere.