As applications grow larger, modularity becomes more important. There are multiple reasons why you would want to make your application modular. Maybe you are developing a uniform platform for all of your company's applications? Maybe you want to be able to offer different configurations of your application to different customers depending on their needs and wallets? Maybe you want different teams to be able to work on different features simultaneously? Or maybe you have a completely different motivation for making your application modular?
When speaking of modular Java-applications, the first thing that pops into many developers' minds is OSGi. OSGi is one alternative to achieve modularity, especially since there are JEE application servers out there that support OSGi out-of-the-box. However, developing enterprise applications on top of OSGi does come with a cost, especially if your developers are not familiar with the ins and outs of OSGi from before. If your requirements are such that e.g. hot deployment of new modules is a must-have, then your choice is clear: go with OSGi. However, for many applications it is perfectly acceptable to do a redeploy when adding, updating or removing modules, and in this case there are easier alternatives to OSGi. In this blog post, I'm going to talk about two of them that I have successfully used myself in different projects.
The basic principle behind the two alternatives is the same; it is just implemented in different ways. The principle is simple: when your application starts up, or your Vaadin UI instance is created, it checks whether there are any modules in the classpath. If there are, the modules are allowed to register themselves with the application:
Exactly what this module registration means is completely up to you and your needs. Here are a few examples:
-
Add filters to a pipes-and-filters process
-
Add data sources for different databases
-
Add reports to your reporting engine
-
Add menu items to a menu
-
Add a window to your UI
-
Add a tab to a tabsheet
-
Add a section to an accordion sidebar
-
Add a widget to your dashboard view
Even though all of these examples are different, they still share the same design:
Option 1: Use SPI
SPI (Service Provider Interface) has been around since Java 1.6, but still I've found that surprisingly many Java developers are not aware of its existence or how it works. One reason for this is probably that SPI is used in frameworks and when it works properly, you don't notice it at all. Since we are also building a framework for our modules, SPI is a good alternative, especially since it is a standard Java API that does not require any third party libraries. If this is the first time you pop into SPI, now would be a good time to read through the JavaDocs before continuing. The first thing that needs to be done when implementing a modular application using SPI is to define the service interfaces that will be a part of the shared API. These very much depend on what extension points you are exposing. For example, a simple service interface that allows a module to add tabs to a tabsheet could look like this:
package foo.bar.spi;
public interface TabRegistration {
void addTabs(TabSheet tabs);
}
The UI would then use the following code to look up the modules and let them add their tabs:
public class MyUI extends UI {
private static ServiceLoader
tabRegistrations = ServiceLoader.load(TabRegistration.class);
@Override
public void init(VaadinRequest request) {
TabSheet tabs = new TabSheet();
setContent(tabs);
for (TabRegistration tr : tabRegistrations) {
tr.addTabs(tabs);
}
}
}
Now, in order to add a new module to the application, we would need to do the following: Create a class that implements the foo.bar.spi.TabRegistration
interface. Create a provider configuration file META-INF/services/foo.bar.spi.TabRegistration that contains the fully qualified name of the class created in the previous step. Package the module into a JAR, not forgetting the META-INF/services directory. Repackage and redeploy the WAR so that it includes the module JAR. One thing that is important to remember when using SPI is that the service instances are cached and reused. Therefore, it is good practice to implement the services as factories to avoid situations where e.g. two UI-instances are sharing the same component instance (which would be A Bad Thing™).
Option 2: Use CDI
One drawback with the SPI option is that it requires your modules to be packaged as separate JARs because of the provider configuration files. It is possible to package them directly into a WAR as well, but then you'd have to merge the contents of all the provider configuration files.
If you are using a CDI-enabled container (such as any Java EE 6 compliant web server), there is a better alternative that does not require any configuration files at all.
The CDI-approach is sort of the opposite of the SPI approach. Instead of iterating through all service providers, a single registration event is fired on the CDI event bus. The modules contain observers that will receive this event and register themselves accordingly.
Continuing with the tab registration example, the event interface could look like this (we could also use a class):
public interface TabRegistrationEvent {
public TabSheet getTabs();
}
The UI would then use the following code to fire the event:
@CDIUI
public class MyUI extends UI {
@Inject
javax.enterprise.event.Event<TabRegistrationEvent> tabRegistrationEvent;
@Override
public void init(VaadinRequest request) {
final TabSheet tabs = new TabSheet();
setContent(tabs);
tabRegistrationEvent.fire(new TabRegistrationEvent() {
@Override
public TabSheet getTabs() {
return tabs;
}
});
}
}
Please note that for this to work, the UI must also be a CDI managed bean. This can be achieved by using the Vaadin CDI add-on, or by implementing a custom UIProvider
that retrieves the UI instances from the BeanManager
instead of using the new
operator. Exactly how to do this is outside the scope of this blog post.
Now, in order to add a new module to the application, we would need to do the following:
- Create an event observer that observes the
TabRegistrationEvent
. This observer should normally have dependent scope, so that a new instance is created for each observed event. Session scope could also work if the event observer contains data that should be shared by all UI instances within the same session. - Optionally package the module into a JAR, not forgetting the META-INF/beans.xml marker file.
- Repackage and redeploy the WAR so that it includes the class files of the module.
Summary
I have now introduced you to two different approaches of making your Vaadin application modular. In my opinion they are both equally good, and which alternative to choose very much depends on the project. If you are deploying to a CDI-enabled container and are going to use CDI in your application, then the CDI approach is of course the best one. Otherwise, go with the SPI approach.
One important topic that I haven’t covered in this post is how to set up the project structure and build system to make it as easy as possible to package and deploy modular Vaadin applications. I am going to cover this in my next blog post, but if you are impatient, have a look at the example code for some hints.
Example Code
You can find the example code of this blog post on GitHub.