How to Share Packages Between OSGI Bundle-liferay-7.4

         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