back to all blogsSee all blog posts

How to package a library as an Open Liberty user feature

image of author
Benjamin Confino on Jun 28, 2024
Post available in languages:

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 the integration/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.