Simple Dependency Injection with ServiceLoader in JDK 6
JDK 6 introduces ServiceLoader.
ServiceLoader loads things based on flat files in the directory
META-INF/services from the Java classpath.
The mechanism of using META-INF/services has
been around since JDK 1.3, under the name the
Java Extension Mechanism. ServiceLoader
makes it actually easy to use.
For those familiar with NetBeans Lookup API,
ServiceLoader is like a severely crippled version of that
(specifically the
default lookup).
But what ServiceLoader does, it does adequately. See below
for details of the differences.
Using ServiceLoader is simple - like the
capability pattern,
it is based around querying for a type with a Class object
and getting back zero or more instances (which may be subclasses) of that type.
Here is a toy example of using ServiceLoader for injection:
package serviceloaderdemo;To register HelloImpl as the injected implementation of
import java.util.ServiceLoader;
public abstract class HelloProvider {
public static HelloProvider getDefault() {
ServiceLoader<HelloProvider> ldr = ServiceLoader.load(HelloProvider.class);
for (HelloProvider provider : ldr) {
//We are only expecting one
return provider;
}
throw new Error ("No HelloProvider registered");
}
public abstract String getMessage();
public static void main(String[] ignored) {
HelloProvider provider = HelloProvider.getDefault();
System.out.println(provider.getMessage());
}
}
package serviceloaderdemo;
public class HelloImpl extends HelloProvider {
@Override
public String getMessage() {
return "Hello World";
}
}
HelloProvider, we just create a folder called
META-INF/services in our source directory,
so it ends up in our JAR file. In that directory we
create a text file named serviceloaderdemo.HelloProvider.
That file contains a single line of text - the name of the
actual implementation of HelloProvider:
serviceloaderdemo.HelloImplNow, this is a toy example. But imagine that
HelloImpl
were in a completely different JAR file. All you have to do to
change the default implementation of HelloProvider is
to put a different JAR file on the classpath.
Let's look at a more real example.
A couple of blogs ago I
showed a cleaned up design for something like
SwingWorker.
It has an interface called TaskStatus with a bunch of setters which
a background task can use to update status information, presumably in a UI of
some kind. What would be very nice is if someone could provide a UI for showing
progress of background tasks just by putting a JAR on the classpath, and
fetching a component and adding it to their window. So it's the perfect case
for dependency injection — somebody should be able to write such a UI
implementation, and then just put it on the classpath, and an application can
pick it up and use it without changing a line of code, and it can be
reused across many applications with almost no code added to those
applications.
We are going to need a factory for TaskStatus objects,
because there might be more than one background process runnning at
a time, and the UI should show all running background tasks. So we'll
create two classes, using the mirror-class pattern discussed
in my previous
blog - we will separate the API and SPI. The UI will get a
ProgressMonitorFactory and get the UI component to add
to its status bar by calling getCapability(Component.class).
All ProgressMonitorFactory will do is sanity check arguments
and thread state, and then delegate to the implementation which is
looked up using ServiceLoader.
But there's an added wrinkle here: There are at least two flavors of status UI we will want - one which just displays in the statusbar, and one which blocks the UI with a modal dialog.
That's okay: ServiceLoader can provide us with more
than one implementation - we just create multiple implementations,
and add more lines to that file in META-INF/services.
So, to indicate different kinds of UI, we'll use annotations, and look up an instance using strings (we actually don't want type-safety here - someone might want to provide still another kind of progress monitor UI - but we will define two defaults):
@Retention (value=RUNTIME)Next we define the SPI class that creates instances of ProgressImpl that can move a progress bar in the UI:
public @interface MonitorKind {
public static final String DEFAULT = "default";
public static final String BLOCKING = "blocking";
String kind();
}
public abstract class ProgressImplFactory {
public abstract ProgressImpl create (Task task);
public <T> T getCapability (Class<T> type) {
return null;
}
}
The last thing we need is the final API mirror-class
for our SPI class. The important method for lookup via annotations is
getInstance(String):
public final class ProgressMonitorFactory {
private static final Set<String> notFoundKinds = new HashSet<String>();
private final ProgressImplFactory delegate;
private ProgressMonitorFactory(ProgressImplFactory p) {
this.delegate = p;
}
public ProgressMonitor create (Task task) {
return new ProgressMonitor (delegate.create(task));
}
public static ProgressMonitorFactory getInstance(String kind) {
if (kind == null) {
throw new NullPointerException ("Kind null");
}
for (ProgressImplFactory p : ServiceLoader.load(ProgressImplFactory.class)) {
//Would be nicer if ServiceLoader let you get the type
//without creating an instance, ala NetBeans Lookup
Class type = p.getClass();
MonitorKind mk = (MonitorKind) type.getAnnotation(MonitorKind.class);
if (mk != null && kind.equals(mk.kind())) {
return new ProgressMonitorFactory(p);
}
}
if (!notFoundKinds.contains(kind)) {
notFoundKinds.add (kind);
Logger.getLogger(ProgressMonitor.class.getName()).log (Level.FINE,
"Requested ProgressMonitor kind '" + kind + "', but it is" +
" not registered on the classpath.",
new IllegalArgumentException());
}
return null;
}
//implementation omitted...
}
Now we just need a JAR that registers an actual implementation
of ProgressImplFactory on the classpath, which
provides a UI component. All an application needs to do to
use it and have a nice UI for background tasks is
ProgressMonitorFactory factory = ProgressMonitorFactory.getDefault();Then all the application author has to do is implement Task and send it off to be run in the background. The UI will work correctly and transparently.
Component statusComp = factory.getCapability (Component.class);
if (statusComp != null) {
statusBar.add (statusComp);
}
The main point here is that the JDK has a useful built-in dependency injection mechanism, and that you can create APIs and implementations of them that are a joy to use and easy to update by using it. While it's not as rich as some of the dependency injection frameworks out there, it has the benefit of being type-safe, and is more than enough for many purposes.
Why You Might Want To Use NetBeans Lookup Instead of ServiceLoader
The differences between NetBeans Lookup and
the JDK's ServiceLoader are as follows:
- You can't instantiate a ServiceLoader yourself; you can use Lookup for inter-object communication, not just loading services from the classpath, and it is very useful for that
- You can listen for changes in a Lookup - really, subscribe to changes the presence/absence/identity of a particular type in that Lookup. That means, if the classpath changes dynamically, you will actually be able to unload and reload objects. This makes it possible to hot-reload JARs without restarting the application.
-
You can find out if a Lookup contains an instance of a type
without actually instantiating an object of that type. With
ServiceLoaderyou can only query by actually instantiating objects. -
You can use
Lookups.forPath(String)to register instances in other subdirectories ofMETA-INF. This makes some interesting things possible, such as using path information as metadata. For example, if you wanted to register different implementations of the same type for different MIME types, you could simply callLookup lkp = Lookups.forPath ("text/plain");
SomeObject thingy = lkp.lookup (SomeObject.class);
-
You can compose multiple lookups together with
ProxyLookup, and even change the set of lookups being proxied on the fly and get appropriate events.
If you want to use Lookup and you have a copy of NetBeans,
you already have a copy of Lookup — look in
$NB_HOME/platform9/modules/org-openide-util.jar.
The Javadoc
can be found here.
Lookup is not tied to any other NetBeans APIs, nor specifically to
GUI applications. You can use it anywhere you want this sort of
dependency injection.
From http://weblogs.java.net/blog/timboudreau/
- 4577 reads
- Printer-friendly version
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)









