How to package a library as an Open Liberty user feature
Learn how to integrate pre-existing software libraries into Open Liberty
Recently, a customer asked me how to package their library as a user feature in Open Liberty so that applications could use it without packaging it directly. If you wish to do something similar, or have some other reason to create an Open Liberty user feature, this guide will walk you through doing so step by step. In this tutorial, you will learn the following skills:
-
How to import a maven artifact as a dependency and repackage it into an Open Liberty user feature.
-
How to expose parts of your library as an API that can be imported
-
How to expose parts of your library to CDI so it can be injected into user applications
Prerequisites
Maven version 3.8.6 or later
Source code
The source code for this post is in the liberty-user-feature-guide repository, where you will find the following resources:
-
finish
This directory contains the completed project you should have at the conclusion of this guide. You can use it as a reference. -
start
This directory contains a skeleton you will modify to create the user feature. -
scripts
You can ignore this directory.
XML variables
One of the trickiest things about packaging a liberty user feature with maven is ensuring variable names are constant across multiple XML files. To make this clearer, any variable that appears in two or more files will be set as a property in the root XML file.
There is one exception in BOM/pom.xml which has to be a raw string, it is clearly labelled in the example finish/bom/pom.xml
file.
The feature names in an OpenLiberty server.xml
file need to match an IBM-ShortName; in this case the one defined in the esa/pom.xml
file. In the vast majority of cases, the server.xml
is configured outside maven, so I have done the same and left labels.
Example libraries
For the purpose of this tutorial, Apache Commons is used as an example library that stands in for the library you wish to package as a user feature.
Structure
The project has the following structure:
root ├── BOM ├── demo ├── ESA └── integration
-
BOM
is the Bill of Materials. It tells Open Liberty which Maven artifacts are part of the feature. -
demo
is a simple demo application. -
integration
is an OSGi bundle containing new code written to integrate the pre-existing libraries into Open Liberty, in this case by exposing library classes to CDI. -
ESA
is an OSGi Enterprise Subsystem Archive that packages up integration and the pre-existing libraries.
Depending on your needs, the integration
bundle might not be required. If you expect developers to use your library with plain old java syntax like new LibraryClass()
, static accessors, or if it already has CDI producers or annotations, you might be able to ignore integration.
Try what you will build
The finish
directory contains a completed version of this tutorial. Inside finish
, have a look at demo/src/main/java/example/app/TestServlet.java
. You will see that this class uses a class from our pre-existing library in two separate ways, once by injecting it via CDI and once as a plain old java class.
Next have a look at integration/src/main/java/com/ibm/example/cdi/CDIProducer.java
and you will see the @Producer
method that turns a pre-existing class into an injectable bean.
Lets compile it and give it a try, return to the finish
directory and run the following commands:
mvn install cd demo mvn liberty:create mvn liberty:prepare-feature mvn liberty:install-feature
-
mvn liberty:create
creates a server locally. -
mvn liberty:prepare-feature
populates your maven repository with details about Liberty features so the Liberty feature manager can install them. -
mvn liberty:install-feature
instructs that feature manager to install our new feature.
Have a quick look inside target/open-liberty-integration.war
. You will see that it does not contain Apache Commons
. Thanks to our integration code, Apache Commons
is now provided by Liberty.
Now use mvn liberty:run
to start the server. Visit the http://localhost:8080/open-liberty-integration/ URL and you will see the two ConstantInitializer
objects output Hello World.
Step one: Create the integration bundle
The integration bundle will become an extension to Open Liberty, exposing new options to hosted applications. You can write a bundle that simply repackages an existing library as an Open Liberty feature. However, ours will go further by registering classes with the CDI subsystem so that applications can inject them.
Write the Java code
Navigate to the start/integration/src/main/java/com/ibm/example/cdi/
directory. You will see two packages: internal
and api
. When you are finished, the contents of api
will be available to import into application code. The contents of internal
will not be, even though they can affect application classes.
It is unlikely that a real integration glue package will need to be exposed as an API. You will see a more realistic example of exposing a pre-existing API in the next section.
api
has one file: ExampleQualifier.java
. This is a normal CDI qualifier that you can ignore.
internal
has two files: CDIProducer.java
and CDIIntegrationMetaData.java
.
-
CDIIntegrationMetaData.java
will implement an Open Liberty SPI that can register new beans. -
CDIProducer.java
will be the new bean that produces other beans after constructing the contained object. In a real feature, it might read configuration and construct the bean using a factory object or the builder pattern.
Open CDIProducer.java
and add a producer method by adding the following code:
public ConstantInitializer<String> getConstantInitializer()
{
return new ConstantInitializer<String>("Hello");
}
This method will do everything you need to create and return a fully configured object. However, CDI will not yet be aware it should invoke this method without the proper annotations. Add the following:
-
@Produces
- so CDI knows this method is a source of an injectable bean, the bean’s type will come from the method’s return type. -
@Dependent
- This will be the scope of the bean. We are using@Dependent
because ConstantInitializer’s only constructor needs a parameter to make it non-proxiable. -
@ExampleQualifier
- We’re adding a qualifier to the bean only so we have an example of an API class.
Finally, since CDIProducer
is itself a bean, it needs a scope. As CDIProducer
has no state, add @ApplicationScoped
to the class. All together, CDIProducer
should look like the following example:
package com.ibm.example.cdi.internal;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import org.apache.commons.lang3.concurrent.ConstantInitializer;
import com.ibm.example.cdi.api.ExampleQualifier;
@ApplicationScoped
public class CDIProducer
{
@Produces
@Dependent
@ExampleQualifier
public ConstantInitializer<String> getConstantInitializer()
{
return new ConstantInitializer<String>("Hello");
}
}
Next, open the CDIIntegrationMetaData.java
file. To complete this class, register it as an OSGi component so that Open Liberty will provide it to the CDI framework when it looks for its lists of extensions. And then we’ll have to register CDIProducer
as a bean.
Add @Component(service = CDIExtensionMetadata.class, configurationPolicy = IGNORE)
and implements CDIExtensionMetadata
to the class to make it an OSGi component.
Then add the following method
public Set<Class<?>> getBeanClasses() {
return Set.of(CDIProducer.class);
}
Before proceeding to the next step, review the Javadoc for CDIExtensionMetadata.
It is also important to be aware that getBeanClasses()
is a unique Open Liberty idiom. The normal way to add a new bean would be to make a class that implements javax.enterprise.inject.spi.Extension
and register it via META-INF/services
.
If you wish to use Extension
for compatibility with other Jakarta EE servers or because your integration requires the power of a full Extension
, then CDIExtensionMetadata
has a different method you can use for this purpose. If you want to register your extension via META-INF/services
rather than ` CDIExtensionMetadata`, see the BELL feature documentation.
Write the pom.xml
Open the start/integration/pom.xml
file.
The pom.xml
already contains all the dependencies we need to compile and build an unconfigured Maven bundle plugin. That is the next step.
The bundle needs a human readable <Bundle-Name>
, a machine readable <Bundle-SymbolicName>
, and we need to provide a list of packages to include in the bundle.
Inside <instructions>
add the line <Bundle-Name>example.user.feature.human.name</Bundle-Name>
and <Bundle-SymbolicName>example.user.feature.integration.machine.name</Bundle-SymbolicName>
.
<Bundle-SymbolicName>
example.user.feature.integration.machine.name
</Bundle-SymbolicName>
<Bundle-Name>example.user.feature.human.name</Bundle-Name>
Also inside <instructions>
you will find the tag <Export-Package>
, populate it with the following code:
${new.integration.code.api.package};version="1.0.0",
${new.integration.code.private.package};version="1.0.0"
These classes will not be registered correctly without a version number.
The instructions section of integration/pom.xml
should now look something like this:
<instructions>
<Export-Package>
${new.integration.code.api.package};version="1.0.0",
${new.integration.code.private.package};version="1.0.0",
</Export-Package>
<Bundle-SymbolicName>
example.user.feature.integration.machine.name
</Bundle-SymbolicName>
<Bundle-Name>example.user.feature.human.name</Bundle-Name>
<Bundle-Version>1.0.0</Bundle-Version>
</instructions>
Going back to the parent pom.xml
, set these properties:
<new.integration.code.private.package>com.ibm.example.cdi.internal</new.integration.code.private.package>
<new.integration.code.api.package>com.ibm.example.cdi.api</new.integration.code.api.package>
Step two: Create the ESA
Open Liberty features are packaged as an Enterprise Subsystem Archive (ESA). We will create one that includes both our new integration code and the pre-existing library.
Open the esa/pom.xml
file.
The first thing we need to do is ensure our ESA will have a manifest.mf
file. Set <generateManifest>true</generateManifest>
in the <configuration>
section of the esa-maven-plugin
.
Now, in instructions we will set a subystem symbolic name <Subsystem-SymbolicName>example.user.feature.esa.machine.name;visibility:=public</Subsystem-SymbolicName>
. Setting the visibility to public
is required.
We will also need an IBM shortname. Add <IBM-ShortName>${feature.name}</IBM-ShortName>
inside instructions
. Then, in the properties
section of the root pom.xml
file, set ${feature.name}
to example-feature-1.0
.
Finally, in esa/pom.xml
, add the following code under <IBM-API-Package>
.
${pre.existing.library.package};version="3.14.0",
${new.integration.code.api.package};version="1.0.0"
This will make those two packages visible to applications at runtime.
When you finish, the esa-maven-plugin
<configuration>
will be similar to the following example:
<configuration>
<generateManifest>true</generateManifest>
<archiveContent>all</archiveContent>
<instructions>
<Subsystem-SymbolicName>
example.user.feature.esa.machine.name;visibility:=public
</Subsystem-SymbolicName>
<Subsystem-Vendor>IBM</Subsystem-Vendor>
<IBM-Feature-Version>2</IBM-Feature-Version>
<IBM-ShortName>${feature.name}</IBM-ShortName>
<Subsystem-Type>osgi.subsystem.feature</Subsystem-Type>
<Subsystem-Version>1.0.0</Subsystem-Version>
<IBM-API-Package>
${pre.existing.library.package};version="3.14.0",
${new.integration.code.api.package};version="1.0.0"
</IBM-API-Package>
</instructions>
</configuration>
The ESA is now complete. But there is one final step, set ${pre.existing.library.package}
to org.apache.commons.lang3.concurrent
by defining the property in the parent pom.xml
file:
<properties>
...
<pre.existing.library.package>org.apache.commons.lang3.concurrent</pre.existing.library.package>
...
</properties>
Step three: Create the Bill of Materials
The liberty-maven-plugin
requires a bill of materials to find and install features. In the real world, the Bill of Materials might be defined in the ESA pom.xml
file, but this tutorial will keep them separate for clarity.
Open the bom/pom.xml
file and add the following dependency.
<dependency>
<groupId>com.ibm.example.user.feature</groupId>
<!-- This is ${esa.artefact.id}. A variable cannot be used here -->
<!-- As this needs to be readable outside this project. -->
<artifactId>liberty-feature</artifactId>
<version>1.0-SNAPSHOT</version>
<type>esa</type>
<scope>provided</scope>
</dependency>
Step four: Add your Liberty user feature to a Liberty server
Go to demo/src/liberty/server.xml
and add the following feature definition inside the featureManager
element:
<featureManager>
...
<feature>usr:example-feature-1.0</feature>
</featureManager>
usr:
is prepended for all user features, and the second part of the feature name is the IBM-ShortName
for the feature.
Naturally, a liberty server.xml
cannot read properties from a pom.xml
, so we have to put usr:example-feature-1.0
in as a raw string.
Gotchas
Here are a few non-obvious risks and things to be aware off.
-
The use of injection for libraries is limited. You can take classes found in the library and inject them into application classes, but you can’t take classes provided by Open Liberty itself, or application code, and inject them into your library’s classes. Incidentally, the way to get a Config object from MicroProfile Config in OpenLiberty without injection is
org.eclipse.microprofile.config.ConfigProvider.getConfig(Thread.currentThread().getContextClassLoader());
-
The
<Export-Package>
tag in theintegration/pom.xml
file controls what packages are included in the bundle. Make sure you get everything you need. -
If a package isn’t listed as
IBM-API-PACKAGE
, applications will not be able to access classes from that package. This means trying to@Inject
those classes will fail.