Writing Pluggable Java EE Applications, The Explanation
Tuesday, July 10, 2012 |I recently posted the slides and the source code from the presentation I gave at JAXConf San Francisco. While that’s helpful for those who were in my session, it’s probably less so for those who weren’t. What I’ll do in this post, then, is discuss the slides and code in detail, skipping over the introductory slides, and getting right to the heart of the matter.
The idea of plugins is easy. The how is, often the hard part. To provide the "how", I’ve broken the question into two parts: how do I get access to the plugins, and how do I write the plugins.
Problem #1: ClassLoading
Perhaps the first question one should ask is, "How do I load the plugin data?" After all, if you can’t load the plugins, who cares if you can write them? There are many, many options, of course, but we’ll take a look at three, to keep things simple: repackaging, manual class loading, and OSGi.
Repackaging
Perhaps the simplest, safest approach is simply to repackage the application. This is exactly what it sounds like: you take the original application archive, and .war, for example, and add the plugin jars. This avoids some of the potential problems we’ll see in other approaches, as well as pushing the heavy lifting on to your container. You can do this through some sort of Ant- or Maven-based system, or a simple shell script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
DIST=$1
if [ "$DIST" == "" ] ; then
echo "You must specify the distribution .war"
exit 1
fi
BASE=`echo $DIST | sed -e 's/\.war//'`
rm -rf work
mkdir work
cd work
jar xf ../$DIST
cp ../plugins/*jar WEB-INF/lib
jar cf ../$BASE-repackaged.war *
cd ..
rm -rf work
The upside to this is that you maintain access to all of the technologies provided by your container: EJB, JMS, etc. The downside is that deployment/upgrade takes a bit more work, and means that if you forget to run this process, you’ve suddenly lost all of your plugins. Not a major thing, but certainly a thing. :)
Manual ClassLoading
From a technical perspective, this is my favorite approach, simply because of the pure hardcode geekiness of it. In a nutshell, we read the bytecode from the jar file(s), and shove it into the ClassLoader. Fraught with peril, sure, but fun nonetheless.
In the source tarball, you’ll find a project called Plummer. Plummer, so called because it provides some of the plumbing for the plugin system. before we dive into what Plummer provides, we should note that the plugin approach we’ll look at is based very heavily on CDI, a new specification in Java EE 6. What we need to do in this part of plugin equation is to provide these plugin classes to the CDI runtime. In Plummer, this happens in the PluginLoader class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PluginLoader implements Extension {
protected static final String SERVICES_NAME = "com.steeplesoft.plummer.finders";
private static final Logger logger = Logger.getLogger(PluginLoader.class.getName());
private static List<PluginFinder> pluginFinders;
List<Class<? extends PluginFinder>> pluginFinderClasses;
public void beforeBeanDiscovery(@Observes BeforeBeanDiscovery bbd, BeanManager beanManager) {
for (PluginFinder pluginFinder : getPluginFinders()) {
try {
for (Class<?> clazz : pluginFinder.getClasses()) {
final AnnotatedType<?> annotatedType = beanManager.createAnnotatedType(clazz);
logger.log(Level.INFO, "Adding AnnotatedType for {0}", annotatedType.toString());
bbd.addAnnotatedType(annotatedType);
}
} catch (Exception ex) {
Logger.getLogger(PluginLoader.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
// ...
}
Here we see CDI show up for the first time. What have here is a CDI portable extension (for more information, see the CDI RI (Weld) documentation). We also see another CDI concept that we’ll revisit in the next section, Events. Since we’re a portable extension, the class is loaded by the runtime very early in the process. In this case, our Extension
observes the BeforeBeanDiscovery
event, as you can see in the aptly-named method. This method asks the system for a list of the PluginFinder
instances configured in the system (I got a little over-excited and added in the ability to have more than one, Just In Case). It then asks each PluginFinder
for a list of the Classes it found. For each class, it creates an AnnotatedType
, which it then adds to the BeforeBeanDiscovery
class. When this method exits, the CDI runtime will, eventually, scan our plugin classes for CDI annotations, and, voila! our plugins are registered with the system.
The bytecode loading is handled, in this case, by FilesystemPluginFinder
. This class iterates over a list of jar files found in, by default, $HOME/.plugins
. For each .class
file in each jar, the bytes are read and the class is defined in the ContextClassLoader. We’ll not show how that happens here, but I will say that it seems to work pretty well. I’ve not found any issues with it so far, but I also have not been able to drive it very hard yet.
When using this approach, it’s important to note some of its restrictions. Given how and when the classes are loaded, certain Java EE technologies are not available. Namely, this includes EJB, JMS, etc., as the related containers are unable to scan the classes for annotations. I have not tested JPA support yet. CDI seems to be completely and well supported.
OSGi
It seems that any discussion of plugins (or modules, if you will) would be incomplete without talking about OSGi. As I started thinking about this topic, Web Application Bundles came to mind pretty quickly. I will admit that I am far from an OSGi expert, but it seems that WABs aren’t quite what we want. In fact, they seem to be designed to solve different issues. WABs and Plummer are not mutually exclusive, however. In fact, I have very young, incomplete code in Plummer to allow a system to deliver plugins as OSGi bundles.
Plummer assists in this by providing, in plummer-api
, the PluginActivator
class. This bundle activator runs, of course, when the bundle is started. You can look at the code for details, but it passes the Bundle
to Plummer’s PluginTracker
, which calls Bundle.findEntries()
to find the class files in the bundle. The information is stored, and ultimately passed to the PluginLoader
mentioned above.
In terms of drawbacks, this one has the most severe of the approaches we’ve discussed. In addition to having the same limitations (most likely) as the manual class loading approach, it has one even more significant one: it does not currently work. :) I think the theory/approach is sound, but I have not had the chance to finish and test the code to date. For those interested, you know what to do. :)
Problem #2: Application Design
Now that we’ve answered any questions anyone would have — ever — about plugin loading, let’s spend some time on the more interesting part: how do I design the plugins. I would like to note that it’s probably not possible to write a complete, generally useful plugin system that works across applications, problem domains. If someone were determined enough to prove me wrong and actually did just that, I’d be willing to be that it would be difficult to use, heavy, etc. If this person were truly determined, gifted, etc. to prove me wrong yet again, well… good for him. :) For the rest of us (or, in this case, just me), Java EE makes this so easy that you really don’t need any extra frameworks. Specifically, we’ll take a look at JSF, CDI, and JAX-RS.
View Extensibility
Let’s start by taking a look at view extensibility, as, after all, even if you have the greatest plugin system in the world, if it can’t affect the view, then it’s pretty much worthless. To demonstrate this technique, we’re going to use JavaServer Faces, as it is the Java EE standard for web applications. You may prefer another framework, such as Spring MVC, Wicket, or GWT, or you may even be using desktop technologies such as Swing, SWT, or JavaFX to build views for your Java EE application. The technique here should work the same regardless of framework, more or less. You’ll just have to determine how to integrate into your technology of choice.
For a plugin to add content to the view, it will have to provide what we on the GlassFish Console team ended up calling view fragments. These fragments are exactly what they sound like, small pieces of UI…widgets that are added at specific points in the view. These fragments are categorized, by the plugin, into types, as defined by the consuming application. This means that the app might declare the types tab, treeNode, and widget. Aplugin, then, might add a tab to a configuration page, a treeNode to the navigation system, and a recent tweets widget to the sidebar. As we’ll see, how complex or simple the categorization/differentiaton exposed by the application is is completly up to you as the application author/architect.
Having defined the terms, then, how might one implement this? First off, let’s take a look at ViewFragment.java
in the plummer-api
module:
1
2
3
4
public @interface ViewFragment {
String type();
String parent() default "";
}
This simple interface defines type
and parent
properties. A plugin author would use it like this:
1
2
3
4
5
6
7
8
9
public class SamplePlugin implements Plugin {
@ViewFragment(type: "foo")
public static String sample1 = "sample1.xhtml";
@Override
public int getPriority() {
return 500;
}
}
There are several things to note here. First, let’s look at the annotation. Here, we are defining a ViewFragment
of type foo. It is attached to a public static final String
, whose value is sample.xhtml. When the system processes this annotation, it will store the value sample.xhtml in a Map
, keyed by the value foo. When the view asks for view fragments of type foo, this piece of markup will be included. That file, by the way, is a simple JSF 2 Facelets file:
1
2
3
4
5
6
7
8
9
10
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:fragment xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets">
<h1>Plugin Fragment</h1>
This text comes from a fragment. Shiny!
</ui:fragment>
Very simple. The question that should come to mind now is, "How does the system find this annotation, and then how do I tell the system to insert this into my view?" The answer to the first half of that question is the Plugin
interface. Those interested in the nitty gritty can read the code (PluginService.java
in plummer-kernel
), but for those not that curious, CDI again saves the day. In a nutshell, we ask CDI for all the beans that implement Plugin
, scan them for fields annotated with ViewFragment
, and store the metadata. On the view side, we use the pl:viewFragment
custom component that Plummer offers:
1
<pl:viewFragment type: "foo"/>
The system does the rest. What you put in your view fragments is completely up to you. We’ve put everything from simple markup to `h:form`s with no known issues. One note with regard to resources: since the resources are stored in JARs and not in the application’s document root, you will need to use JSF 2’s resource mechanism to reference images, javascript, CSS, etc:
1
2
<h:graphicImage value="#{resource['myImage.JPG']}" height="200"
title: "Here's a picture of something really cool!"/>"
You can see a complete example of this in plummer-sample2
.
One final note before we move on: The code in Plummer is mostly standards-compliant, but some Mojarra-specific classes needed to be used get access to the FaceletFactory
needed to insert the view fragments into the component tree. MyFaces users can still use Plummer, but someone will need to implement the MyFaces-specific code to reproduce this functionality. It would be great if the spec could expose this kind of functionality, but that would require someone to file a request and then, preferably, submit the spec prose and implementation code to make that happen, and I just haven’t had the time. ;)
Application Extensibility
The real work, of course, is done at lower levels. Here we’ll see just how simple Java EE makes things. Specifically, we’ll look at two parts of CDI, events, and what we’ll simply refer to as programmatic bean lookup.
CDI events is, conceptually, just a simple pub/sub system. One part of the system fires, or publishes, events, and another observes (subscribes). This makes it very easy to loosely couple parts of the system: the core of your application need not worry about what, if anything handles, the event. It also easily allows multiple recipients to respond to the event fired. Again, the system doesn’t care. In Ron Popeil style, you just "set it and forget it".
So what does this look like in practice? To demonstrate that in a meaningful way, we need a sample application, so we’ll write a very simple blogging system. If you’ve ever interacted with a blog, either as an author or a reader, you’ve likely seen the option by which a user can subscribe and get notifications of new posts. Let’s implement that. First up, we’ll need a way to create blog entries. You can find this BlogBean.java
in the webapp, but here are the interesting parts:
1
2
3
4
5
6
7
8
9
10
11
// ...
@Inject
private Event<BlogPostedEvent> blogPostedEvents;
public String addEntry() {
entries.add(entry);
blogPostedEvents.fire(new BlogPostedEvent(entry));
entry = null;
return null;
}
// ...
For the sake of brevity here (too late, right?), you can find the view in examples/webapp/src/main/webapp/blog.xhtml
. First, notice the @Inject
. Here, we’re asking CDI to inject an Event
that takes a BlogPostEvent
payload. We use this in addEntry()
, when we call blogPostedEvents.fire(new BlogPostedEvent(entry))
. The code, simple as it is, should be pretty self-explanatory: we’re firing an event of type BlogPostedEvent
, which looks like this:
1
2
3
4
5
6
7
8
9
10
11
public class BlogPostedEvent {
private String blogEntry;
public BlogPostedEvent(String blogEntry) {
this.blogEntry = blogEntry;
}
public String getBlogEntry() {
return blogEntry;
}
}
In this example, our payload is very simple. In a real world, this could be much more complex if your application’s needs warrant. Responding to this event is just as simple as firing it:
1
2
3
public void sendEmail(@Observes BlogPostedEvent event) {
emailService.sendEmail(event.getBlogEntry());
}
That’s really all there is to it. By using CDI events, we are able to push data into our plugins in a loosely coupled manner. Again, in a real world application, the data push and the processing required to handle will likely be more complex, but the means of pushing it will not be. CDI for the win!
Perhaps you need to allow a plugin to process data in the system. For example, in our system we want allow plugins to translate the blog entry into another language. To do so, we first need to define the interface by which the plugin will be called:
1
2
3
4
public interface BlogEntryProcessor extends Serializable {
String getName();
String process(String text);
}
From our blogging system, we can get a list of all of the BlogEntryProcessor
instances, if any, with this CDI injection:
1
2
@Inject @Translator
Instance<BlogEntryProcessor> translators;
This gives us an Instance
instance that contains any BlogEntryProcessor`s defined in the system. We’ll come back to `@Translator
in a bit. Next, we can provide a way for the user to pick a language with this code:
1
2
3
4
5
6
<h:form>
<h:selectOneMenu value="#{blogBean.translator}" converter="#{translatorConvertor}">
<f:ajax render=":entries" event="change" execute="@form"/>
<f:selectItems value="#{blogBean.translators}" var="t" itemLabel="#{t.name}" />
</h:selectOneMenu>
</h:form>
and
1
2
3
4
5
6
7
public List<BlogEntryProcessor> getTranslators() {
List<BlogEntryProcessor> list = new ArrayList<BlogEntryProcessor>();
for (BlogEntryProcessor t : translators) {
list.add(t);
}
return list;
}
This lets us change the language, but how do we get a default? Let’s define a Qualifier
:
1
2
3
4
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
public @interface English { }
This simple class lets us differentiate at injection time:
1
2
3
@Inject
@English
private BlogEntryProcessor translator;
Instead of injecting Instance<BlogEntryProcessor>
, we’re injecting a single..um…instance. Since there might be more than BlogEntryProcessor
on the system, we have to qualify which one we mean:
1
2
3
4
5
6
@English
@Singleton
@Translator
public class EnglishTranslator implements BlogEntryProcessor {
// ...
}
EnglishTranslator
is-a
BlogEntryProcessor
, and it has been marked as @English
, which means this instance, which is also a singleton, will satisfy the injection above. We could have annotated this with @Default
, both here and at the injection point, but the creation of a custom @Qualifier
is a good exercise. :)
But what’s up with that @Translator
? That’s another @Qualifier
, which must be applied to any BlogEntryProcessor
that is intended to act as a translator (and which we document clearly in our system documentation, right? ;). Why is that important? In a simple system, we wouldn’t need that, but we’re going to intentionally muddy things a bit and introduce a different type of BlogEntryProcessor
, one which allows for tags.
One common type of plugin in systems like Wordpress allows a user to wrap certain text in a tag. This entry, for example, uses the code
tag to get syntax highlighting. In our system, we’ll implement a tag that creates links to Google Maps. For example:
1
Disneyland can be found at [map]1313 North Harbor Boulevard, Anaheim, CA[/map].
How is this implemented? Just like the translators:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Tag
public class GoogleMapsProcessor implements BlogEntryProcessor {
@Override
public String getName() {
return "Google Maps Processor";
}
@Override
public String process(String text) {
Pattern pattern = Pattern.compile("\\[map\\](.*?)\\[\\/map\\]");
String replaceStr = "<a href=\\\"https://maps.google.com/maps?q=$1\\\">$1</a>";
Matcher matcher = pattern.matcher(text);
String result = matcher.replaceAll(replaceStr);
return result;
}
}
This looks just like the translators, right? The only difference is the @Tag
qualifier, whose source you can see in the bundle. In BlogBean
, we access it and the translators in getEntries()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Inject
@Tag
Instance<BlogEntryProcessor> tags;
public List<String> getEntries() {
List<String> list = new ArrayList<String>();
for (String text : entries) {
for (BlogEntryProcessor tag : tags) {
text = tag.process(text);
}
text = translator.process(text);
list.add(text);
}
return list;
}
You can build and deploy the system to see this in action. Very simple, but very effective.
REST Resources
We’ve seen how to expose functionality to plugins loaded in the system, but what if we want to allow these plugins to expose this functionality to external clients, say, via REST? Again, Java EE makes this incredibly simple, using two specs in concert, CDI and JAX-RS.
One of the ways one might configure a JAX-RS application is to provide a custom Application
class, one which extends javax.ws.rs.core.Application
. Plummer provides such an Application
, so all Plummer users need do is configure it in the web application:
1
2
3
4
5
6
7
8
<servlet>
<servlet-name>Jersey Web Application</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>javax.ws.rs.Application</param-name>
<param-value>com.steeplesoft.plummer.kernel.rest.RestApplication</param-value>
</init-param>
</servlet>
Oddly, note that the Application
class is standardized, but the REST servlet is not (unlike, for example, JSF’s FacesServlet
), so if you’re not using Jersey like we are here, then you’ll need to use the Servlet
appropriate for your JAX-RS implementation.
So how does RestApplication
work? It uses CDI, but since it’s not handled by the CDI runtime, we can’t rely on injection. Instead, we’ll perform a manual look up of the BeanManager
, a class provided by CDI’s excellent portable extension mechanism. We then query the BeanManager
for our desired classes. But how do we identify our REST resources? Remember the Plugin
marker interface? Plummer defines another marker, RestResource
, to mark the JAX-RS resources we want to load, which are typical JAX-RS resources with the exception of this extra interface:
1
2
3
4
5
6
7
@Path("myurl")
public class PluginRestResource implements RestResource {
@GET
public String test(@QueryParam("text") String text) {
return "You sent " + text;
}
}
When the REST application is initialized, this class is loaded and exposed at /myurl
as you would expect.
Conclusion
There are many, many plugin systems available for Java applications. It might be that one of these systems, modeled after or borrowed from, for example, Hudson or others, is the best choice for your application. I think that chances are good, though, that you need not resort to such a relatively complex system. The Java EE platform provides a rich set of APIs that will allow you to implement an domain-specific plugin system very simply, and with the introduction of another external dependency.