How to create OSGi Modules and how they share packages
Overview
This guide walks through creating two OSGi modules in Liferay 7.4 and sharing a Java package between them using the standard API + Implementation pattern. This is the most common and correct approach for exposing reusable services in a Liferay-based architecture.
In OSGi, a bundle (module) does not expose its internal packages by default — everything is private unless explicitly exported. To share code between modules, the standard practice is to split functionality into:
An API module — contains interfaces/DTOs and exports the package
A Service (Implementation) module — implements the interface, imports the package, and registers the implementation as an OSGi component/service
The consuming module then simply imports the API package and looks up the service using @Reference injection.
Step 1: Create the API Module
Using Liferay Workspace + Blade CLI, generate a new API module:
blade create -t api -p com.sgc.greeting greeting-api
This generates a Gradle module under modules/greeting-api with the following structure:
greeting-api/
└─ src/main/java/com/sgc/greeting/api/GreetingService.java
GreetingService.java
package
com.sgc.greeting.api;
public interface GreetingService {
String sayHello(String name);
}
Export the Package in bnd.bnd
Liferay/OSGi uses the bnd.bnd file (bnd tool) to control exports. Open greeting-api/bnd.bnd and add:
Bundle-Name:
Greeting API
Bundle-SymbolicName:
com.sgc.greeting.api
Bundle-Version: 1.0.0
Export-Package:
com.sgc.greeting.api
The Export-Package line is what makes this package visible to other bundles — this is the core of “sharing” a package in OSGi.
Step 2: Create the Service / Implementation Module
blade create -t service-builder -p com.sgc.greeting greeting-service
Update
your service.xml file
<?xml
version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.4.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_4_0.dtd">
<service-builder package-path="com.sgc.greeting">
<author>Arman</author>
<namespace>GREETING</namespace>
<entity name="Greeting" local-service="true" remote-service="false">
<!-- PK fields -->
<column name="greetingId" type="long" primary="true" />
<!-- Group instance -->
<column name="groupId" type="long" />
<column name="companyId" type="long" />
<!-- Audit fields -->
<column name="userId" type="long" />
<column name="userName" type="String" />
<column name="createDate" type="Date" />
<column name="modifiedDate" type="Date" />
<!-- Other fields -->
<column name="name" type="String" />
<column name="message" type="String" />
<order by="asc">
<order-column name="name" />
</order>
<finder name="Name" return-type="Collection">
<finder-column name="name" />
</finder>
</entity>
</service-builder>
Add a Dependency on the API Module
In greeting-service/build.gradle:
dependencies
{
compileOnly project(":modules:greeting-api")
compileOnly group: "com.liferay.portal", name:
"com.liferay.portal.kernel", version: "default"
compileOnly group: "org.osgi", name:
"org.osgi.service.component.annotations", version:
"1.3.0"
}
Implement the Interface as an OSGi Component
package
com.sgc.greeting.service.impl;
import
com.sgc.greeting.api.GreetingService;
import
org.osgi.service.component.annotations.Component;
@Component(
immediate = true,
service =
GreetingService.class
)
public class GreetingServiceImpl
implements GreetingService {
@Override
public String sayHello(String name) {
return "Hello,
" + name + "! Welcome to Liferay 7.4 OSGi modules.";
}
}
service = GreetingService.class registers this implementation in the OSGi Service Registry under that interface — this is how Liferay's Declarative Services (DS) model publishes the service.
Step 3: Consume the Shared Package in a Third Module
Create a consumer module, for example an MVC portlet:
blade create -t mvc-portlet -p com.sgc.greeting.web greeting-web
Add the API Dependency in build.gradle
dependencies
{
compileOnly project(":modules:greeting-api")
}
Inject the Shared Service via @Reference
package
com.sgc.greeting.web.portlet;
import
com.sgc.greeting.api.GreetingService;
import
org.osgi.service.component.annotations.Component;
import
org.osgi.service.component.annotations.Reference;
import
javax.portlet.*;
import java.io.IOException;
@Component(
immediate = true,
property = {
"javax.portlet.name=com_sgc_greeting_web",
"javax.portlet.display-name=Greeting Portlet"
},
service = Portlet.class
)
public class GreetingPortlet
extends MVCPortlet {
@Reference
private
GreetingService greetingService;
@Override
public void doView(RenderRequest request, RenderResponse response)
throws IOException, PortletException {
request.setAttribute("greetingMessage",
greetingService.sayHello("Arman"));
super.doView(request, response);
}
}
@Reference tells the OSGi Declarative Services runtime to inject whatever component is registered for GreetingService — this is the actual “sharing” moment, resolved dynamically at runtime.
Step 4: Deploy in the Correct Order
Order matters because of dependency resolution:
blade gw :modules:greeting-api:deploy
blade
gw
:modules:greeting-service:greeting-service-service:buildService
blade
gw :modules:greeting-service:greeting-service-api:deploy
blade gw :modules:greeting-service:greeting-service-service:deploy
blade
gw :modules:greeting-web:deploy
Or deploy all modules at once from the workspace root:
blade gw deploy
Step 5: Verify in Gogo Shell
Connect to Liferay's OSGi console:
telnet localhost 11311
Check bundle states:
lb greeting
You should see all three bundles as Active. Then verify the exported package is visible:
package com.sgc.greeting.api
This lists which bundle exports the package and which bundles import it — confirming the sharing worked correctly.
Key Points to Remember
Concept |
Purpose |
Export-Package in bnd.bnd |
Makes a package visible outside its own bundle |
compileOnly project(":modules:x-api") |
Gradle-level dependency so code compiles against the API |
@Component(service = X.class) |
Registers implementation in the OSGi Service Registry |
@Reference |
Injects a registered service from another bundle |
Deployment order |
API → Implementation → Consumer (OSGi resolves dynamically, but deploying API first avoids resolution errors) |
Common Mistake to Flag
Forgetting Export-Package in the API module's bnd.bnd is a frequent error. The module deploys fine but stays in the Installed state instead of Active, and the Gogo shell will show an unresolved import in the consuming bundle.
Comments
Post a Comment