Modularity makes writing software, especially as a team, fun! Here are some benefits to modular development on DXP:
- Liferay DXP’s runtime framework is lightweight, fast, and secure.
- The framework uses the OSGi standard. If you have experience using OSGi with other projects, you can apply your existing knowledge to developing on DXP.
- Modules publish services to and consume services from a service registry. Service contracts are loosely coupled from service providers and consumers, and the registry manages the contracts automatically.
- Modules’ dependencies are managed automatically by the container, dynamically (no restart required).
- The container manages module life cycles dynamically. Modules can be installed, started, updated, stopped, and uninstalled while Liferay is running, making deployment a snap.
- Only a module’s classes whose packages are explicitly exported are publicly visible; OSGi hides all other classes by default.
- Modules and packages are semantically versioned and declare dependencies on specific versions of other packages. This allows two applications that depend on different versions of the same packages to each depend on their own versions of the packages.
- Team members can develop, test, and improve modules in parallel.
- You can use your existing developer tools and environment to develop modules.
There are many benefits to modular software development with OSGi, and we can only scratch the surface here. Once you start developing modules, you might find it hard to go back to developing any other way.
Modules
It’s time to see what module projects look like and see Liferay DXP’s modular development features in action. To keep things simple, only project code and structure are shown: you can create modules like these anytime.
These modules collectively provide a command that takes a String and uses it in a greeting. Consider it “Hello World” for modules.
API
The API module is first. It defines the contract that a provider implements and a consumer uses. Here is its structure:
greeting-api
src
main
java
com/liferay/docs/greeting/api
Greeting.java
bnd.bnd
build.gradle
Very simple, right? Beyond the Java source file, there are only two other files:
a Gradle build script (though you can use any build system you want), and a
configuration file called bnd.bnd
. The bnd.bnd
file describes and configures the module:
Bundle-Name: Greeting API
Bundle-SymbolicName: com.liferay.docs.greeting.api
Bundle-Version: 1.0.0
Export-Package: com.liferay.docs.greeting.api
The module’s name is Greeting API. Its symbolic name–a name that ensures
uniqueness–is com.liferay.docs.greeting.api
. Its semantic version is declared
next, and its package is exported, which means it’s made available to other
modules. This module’s package is just an API other modules can implement.
Finally, there’s the Java class, which in this case is an interface:
package com.liferay.docs.greeting.api;
import aQute.bnd.annotation.ProviderType;
@ProviderType
public interface Greeting {
public void greet(String name);
}
The interface’s @ProviderType
annotation tells the service registry that
anything implementing the interface is a provider. The interface’s one method
asks for a String
and doesn’t return anything.
That’s it! As you can see, creating modules is not very different from creating other Java projects.
Provider
An interface only defines an API; to do something, it must be implemented. This is what the provider module is for. Here’s what a provider module for the Greeting API looks like:
greeting-impl
src
main
java
com/liferay/docs/greeting/impl
GreetingImpl.java
bnd.bnd
build.gradle
It has the same structure as the API module: a build script, a bnd.bnd
configuration file, and an implementation class. The only differences are the
file contents. The bnd.bnd
file is a little different:
Bundle-Name: Greeting Impl
Bundle-SymbolicName: com.liferay.docs.greeting.impl
Bundle-Version: 1.0.0
The bundle name, symbolic name, and version are all set similarly to the API.
Finally, there’s no Export-Package
declaration. A client (which is the third
module you’ll create) just wants to use the API: it doesn’t care how its
implementation works as long as the API returns what it’s supposed to return.
The client, then, only needs to declare a dependency on the API; the service
registry injects the appropriate implementation at runtime.
Pretty cool, eh?
All that’s left, then, is the class that provides the implementation:
package com.liferay.docs.greeting.impl;
import com.liferay.docs.greeting.api.Greeting;
import org.osgi.service.component.annotations.Component;
@Component(
immediate = true,
property = {
},
service = Greeting.class
)
public class GreetingImpl implements Greeting {
@Override
public void greet(String name) {
System.out.println("Hello " + name + "!");
}
}
The implementation is simple. It uses the String
as a name and prints a hello
message. A better implementation might be to use Liferay’s API to collect all
the names of all the users in the system and send each user a greeting
notification, but the point here is to keep things simple. You should
understand, though, that there’s nothing stopping you from replacing this
implementation by deploying another module whose Greeting implementation’s
@Component
annotation specifies a higher service ranking property (e.g.,
"service.ranking:Integer=100"
).
This @Component
annotation defines three options: immediate = true
, an empty
property list, and the service class that it implements. The immediate = true
setting means that this module should not be lazy-loaded; the service registry
loads it as soon as it’s deployed, instead of when it’s first used. Using the
@Component
annotation declares the class as a Declarative Services component,
which is the most straightforward way to create components for OSGi modules. A
component is a POJO that the runtime creates automatically when the module starts.
To compile this module, the API it’s implementing must be on the classpath. If
you’re using Gradle, you’d add the greetings-api
project to your dependencies { ... }
block. In a
Liferay Workspace
module, the dependency looks like this:
compileOnly project (':modules:greeting-api')
That’s all there is to a provider module.
Consumer
The consumer or client uses the API that the API module defines and the provider module implements. DXP has many different kinds of consumer modules. Portlets are the most common consumer module type, but since they are a topic all by themselves, this example stays simple by creating an command for the Apache Felix Gogo shell. Note that consumers can, of course, consume many different APIs to provide functionality.
A consumer module has the same structure as the other module types:
greeting-command
src
main
java
com/liferay/docs/greeting/command
GreetingCommand.java
bnd.bnd
build.gradle
Again, you have a build script, a bnd.bnd
file, and a Java class. This
module’s bnd.bnd
file is almost the same as the provider’s:
Bundle-Name: Greeting Command
Bundle-SymbolicName: com.liferay.docs.greeting.command
Bundle-Version: 1.0.0
There’s nothing new here: you declare the same things you declared for the provider.
Your Java class has a little bit more going on:
package com.liferay.docs.greeting.command;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import com.liferay.docs.greeting.api.Greeting;
@Component(
immediate = true,
property = {
"osgi.command.scope=greet",
"osgi.command.function=greet"
},
service = Object.class
)
public class GreetingCommand {
public void greet(String name) {
Greeting greeting = _greeting;
greeting.greet(name);
}
@Reference
private Greeting _greeting;
}
The @Component
annotation declares the same attributes, but specifies
different properties and a different service. As in Java, where every class is a
subclass of java.lang.Object
(even though you don’t need to specify it by
default), in Declarative Services, the runtime needs to know the type of class
to register. Because you’re not implementing any particular type, your parent
class is java.lang.Object
, so you must specify that class as the service.
While Java doesn’t require you to specify Object
as the parent when you’re
creating a class that doesn’t inherit anything, Declarative Services does.
The two properties define a command scope and a command function. All commands
have a scope to define their context, as it’s common for multiple APIs to have
similar functions, such as copy
or delete
. These properties specify you’re
creating a command called greet
in a scope called greet
. While you get no
points for imagination, this sufficiently defines the command.
Since you specified osgi.command.function=greet
in the @Component
annotation, your class must have a method named greet
, and you do. But how
does this greet
method work? It obtains an instance of the Greeting
OSGi
service and invokes its greet
method, passing in the name
parameter. How is
an instance of the Greeting
OSGi service obtained? The GreetingCommand
class
declares a private service bean, _greeting
of type Greeting
. This is the
OSGi service type that the provider module registers. The @Reference
annotation tells the OSGi runtime to instantiate the service bean with a service
from the service registry. The runtime binds the Greeting
object of type
GreetingImpl
to the private field _greeting
. The greet
method uses the
_greeting
field value.
Just like the provider, the consumer needs to have the API on its classpath in order to compile, but at runtime, since you’ve declared all the dependencies appropriately, the container knows about these dependencies, and provides them automatically.
If you were to deploy these modules to a DXP instance, you’d be able to attach to the Gogo Shell and execute a command like this:
greet:greet "Captain\ Kirk"
The shell would then return your greeting:
Hello Captain Kirk!
This most basic of examples should make it clear that module-based development is easy and straightforward. The API-Provider-Consumer contract fosters loose coupling, making your software easy to manage, enhance, and support.
A Typical Liferay Application
If you look at a typical application from Liferay’s source, you’ll generally find at least four modules:
- An API module
- A Service (provider) module
- A Test module
- A Web (consumer) module
This is exactly what you’ll find for some smaller applications, like the
Mentions application that lets users mention other users with the @username
nomenclature in comments, blogs, or other applications. Larger applications like
the Documents and Media library have more modules. In the case of the Documents
and Media library, there are separate modules for different document storage
back-ends. In the case of the Wiki, there are separate modules for different
Wiki engines.
Encapsulating capability variations as modules facilitates extensibility. If you have a document storage back-end that Liferay doesn’t yet support, you can implement Liferay’s document storage API for your solution by developing a module for it and thus extend Liferay’s Documents and Media library. If there’s a Wiki dialect that you like better than what Liferay’s wiki provides, you can write a module for it and extend Liferay’s wiki.
Are you excited yet? Are you ready to start developing? Here are some resources for you to learn more.