Developing User Features for Open Liberty can be a very interesting project. Open Liberty is very extensible. Sadly, the documentation only exists for IBM WebSphere Liberty and requires a lot of prior knowledge. It does not even mention build tools like Apache Ant, Apache Maven or Gradle at all.
This guide will get you started so you can develop your own features. I will also provide a demo project.
Get me straight to the code!
OK, here it is: https://github.com/bmarwell/cowsay-liberty-feature-showcase
The cowsay-liberty-feature-showcase is a minimal feature for Open Liberty. Configuration and everything from below is used to show how to set up a Liberty feature.
Description:
This feature will implement the famous "cowsay" on startup like so:
server.xml
server.xml
setup<?xml version="1.0" encoding="UTF-8"?>
<server description="cowsay">
<featureManager>
<feature>usr:cowsay-1.0</feature>
</featureManager>
<cowsay message="Hello, Liberty" />
</server>
Example output
Launching defaultServer (WebSphere Application Server 22.0.0.5/wlp-1.0.64.cl220520220425-0301) on OpenJDK 64-Bit Server VM, version 1.8.0_332-b09 (en_US)
[AUDIT ] CWWKE0001I: The server defaultServer has been launched.
[AUDIT ] CWWKE0100I: This product is licensed for development, and limited production use. The full license terms can be viewed here: https://public.dhe.ibm.com/ibmdl/export/pub/software/websphere/wasdev/license/base_ilan/ilan/22.0.0.5/lafiles/en.html
[INFO ] CWWKE0002I: The kernel started after 0.544 seconds
[INFO ] CWWKF0007I: Feature update started.
[ERROR ] COWSA0001I: ________________
[ERROR ] COWSA0001I: < Hello, Liberty >
[ERROR ] COWSA0001I: ----------------
[ERROR ] COWSA0001I: \ ^__^
[ERROR ] COWSA0001I: \ (oo)\_______
[ERROR ] COWSA0001I: (__)\ )\/\
[ERROR ] COWSA0001I: ||----w |
[ERROR ] COWSA0001I: || ||
[AUDIT ] CWWKF0012I: The server installed the following features: [usr:cowsay-1.0].
[INFO ] CWWKF0008I: Feature update completed in 0.075 seconds.
[AUDIT ] CWWKF0011I: The defaultServer server is ready to run a smarter planet. The defaultServer server started in 0.620 seconds.
Maven module layout
First of all, we need a maven reactor project with some modules. The minimum setup is:
pom.xml
|- bundle
| ` pom.xml
`- feature
` pom.xml
In other words: We need to define three modules:
- root
The reactor root project.
- bundle
The jar file containing the OSGi module and the Java code.
- feature
The feature description and
.esa
packaging.
We could combine them into one project in theory, but I find one-module-per-use-case far more helpful when writing tests. E.g. the bundle module could easily be tested within an integration test module.
Minimum files in the bundle project
Since this is a bundle project, you will at least set up the maven reactor project as a bundle type.
Setting up a type=bundle maven project
In your bundle/pom.xml
, set the type to »bundle« using <type>bundle</type>
. For this type to be recognized, setup the appropriate plugin as well:
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
</plugin>
</plugins>
</build>
Explanation:
- extensions
Setting this to
true
will enable maven to build abundle
type project (i.e. recognize the<type>bundle</type>
directive).
Activator class
You will need some Java code which is going to be executed at SOME point during the Open Liberty runtime. The entry point is a class usually called Activator
(or MyFeatureActivator
or similar) implementing both org.osgi.framework.BundleActivator
and org.osgi.service.cm.ManagedService
. The BundleActivator
will introduce start and stop methods, while the ManagedService
will provide an update mechanism (i.e. changed configuration during the runtime).
I will skip the description of the methods emitStartMessage()
and
Implementing the start()
method
The start method will be executed on activation, obviously. The only thing you need is a registration call:
@Override
public void start(BundleContext context) throws Exception {
configRef = context.registerService(ManagedService.class, this, this.getConfigDefaults());
}
protected Dictionary<String, ?> getConfigDefaults() {
Hashtable<String, Object> configDefaults = new Hashtable<>();
configDefaults.put(org.osgi.framework.Constants.SERVICE_PID, "cowsay");
return configDefaults;
}
The 2nd method is almost copied from the docs. It sets the service PID, which is the configuration tag name in your server.xml
.
Implementing the stop()
method
As we do not implement a "stop message" in this showcase feature, we just need to unregister the service.
@Override
public void stop(BundleContext context) throws Exception {
this.configRef.unregister();
}
Implementing the updated()
method
Now this is the method which is called when the configuration gets updated. Caveat: It seems to get called AFTER the start method, but I have seen overlapping calls of those methods as well.
I implemented it in a way that there is one cowsay call after reading in the variables — if set. Since there is no further check, any configuration update will also trigger a "moo".
@Override
public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
if (properties == null) {
return;
}
Object cowfile = properties.get("cowfile");
if (cowfile instanceof String) {
this.cowfile = (String) cowfile;
}
Object message = properties.get("message");
if (message instanceof String) {
this.message = (String) message;
}
emitStartMessage();
}
Bundle description
We will need to add a manifest description OSGi can read. We can either write it into a file, but I find XML in this case much more useful. IDEs will usually help you with completion, and you can more easily use maven properties here (without configuring filtered resources).
bundle/pom.xml
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Import-Package>
org.osgi.framework,
org.osgi.service.cm
</Import-Package>
<Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
<Embed-Transitive>true</Embed-Transitive>
<Export-Package>io.github.bmarwell.liberty.cowsay</Export-Package>
<Private-Package>io.github.bmarwell.liberty.cowsay.*</Private-Package>
<Bundle-Activator>io.github.bmarwell.liberty.cowsay.Activator</Bundle-Activator>
<!--
<Bundle-Version> is assumed to be "${pom.version}",
but is normalized to the OSGi version format of "MAJOR.MINOR.MICRO.QUALIFIER".
For example "2.1-SNAPSHOT" would become "2.1.0.SNAPSHOT".
https://felix.apache.org/documentation/subprojects/apache-felix-maven-bundle-plugin-bnd.html
-->
<Bundle-RequiredExecutionEnvironment>JavaSE-1.8</Bundle-RequiredExecutionEnvironment>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
Bundle lazy loading Do NOT activate lazy loading. This will result in (Open) Liberty not executing your feature AT ALL! |
Explanation of what we added:
- Import-Package
Here, all used imports from dependencies need to be declared on a package level. The bundle-plugin will create this list for you.
HOWEVER.
If any of your dependencies is not an OSGi bundle, this will fail. In this case, just add the packages you are using like in the example above.- Embed-Dependency
We will need to bring all dependencies in our bundle, because we cannot do that in our
.esa
file easily. This will results in all runtime-dependencies to be included in our final.jar
file.- Embed-Transitive
In addition to this, we will need to include all transitive packages. Otherwise Open Liberty would not be able to find them.
- Export-Package
You can set this (optionally), but this is also auto-generated from all packages not containing
.impl
or.internal
in their package name. In this case it is so simple, I will just keep it.- Private-Package
By default, everything containing
.internal
will go here. This means, those files are being hidden from Open Liberty.- Bundle-Activator
This is one of the most important option: You will need to define the point-of-entry. Usually, the class is in the topmost package of your package structure and called either
MyModuleActivator
or justActivator
(see below).- Bundle-RequiredExecutionEnvironment
Can be set to
JavaSE-1.8
, but you will need Java 1.8 anyway to run Open Liberty. But you can set it to something likeJavaSE-11
to make this plugin incompatible with Java 8 runtimes.
Setting up the Open Liberty feature definition
Now that we have the bundle, we need to add some code to the feature definition. Luckily, you won’t need much of configuration either.
Creating an .esa
output file
Change the type to .esa
by using <type>esa</type>
. Then, add the esa-maven-plugin
and enable its extensions like so:
<build>
<plugins>
<plugin>
<groupId>org.apache.aries</groupId>
<artifactId>esa-maven-plugin</artifactId>
<extensions>true</extensions>
</plugin>
</plugins>
</build>
This would almost run, but we need to add some more information for Open Liberty to pick it up.
Prettier version numbers
Now you might notice, liberty feature version numbers and maven version numbers don’t match and mix well. For this reason, I decided to create a new property project.version.libertyfeature
which contains just the major and minor version of the maven project.
To achieve this, do not define this property on your project. If you do, the following will not work:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>set-liberty-version</id>
<inherited>true</inherited>
<goals>
<goal>regex-property</goal>
</goals>
<configuration>
<name>project.version.libertyfeature</name>
<value>${project.version}</value>
<regex>^([0-9]+)\.([0-9]+)\.([0-9]+)(-SNAPSHOT)?$</regex>
<replacement>$1.$2</replacement>
<failIfNoMatch>true</failIfNoMatch>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Now, this will execute on every reactor module and extract the version number using regex, re-assemble it using the replacement and store the result into project.version.libertyfeature
.
I will use the property $\{project.version.libertyfeature}
in the next paragraph.
Writing the .esa
manifest
Liberty needs specific information in the .esa
manifest file. The full documentation is on IBMs website. Let’s go through this with simple words.
- IBM-Feature-Version
2
.
Must be present and always be2
.- Subsystem-Type
Must be present and set to
osgi.subsystem.feature
.- Subsystem-SymbolicName
The name of the feature and some configuration about how Liberty can use it. The default visiblilty must be set to public, and you can define whether multiple versions of your feature can be loaded at the same time (i.e. singleton: true, default is false) or specify whether your feature is superseeded by another feature with a different name.
Example value:cowsay-$\{project.version.libertyfeature}; visibility:=public
.
These are optional values:
- Subsystem-Name
A nice name for your feature.
- Subsystem-Description
A longer description for your feature.
- Subsystem-Vendor
The creator. You can use a company name or just your name (or omit it as it is optional).
- Subsystem-Localization
You will only need this, if you have translations for your
server.xml
configuration or internal strings. I recommend keeping everything in English. It always annoys me that Liberty spits out German messages whenever I run a German app.
Value: set it to the resource path of your localizations within the bundle, e.g.OSGI-INF/l10n/loc
.- IBM-ShortName
What you will actually use as a feature name in your
server.xml
. Usually the same as the SymbolicName. But you can change it here.- IBM-AppliesTo
Most commonly used for defining a minimum Liberty version like so:
<IBM-AppliesTo>com.ibm.websphere.appserver;productVersion=20.0.0.0+</IBM-AppliesTo>
Installing the feature
Building the project
As always, just execute mvn verify
. You will get a bundle file (./bundle/target/cowsay-bundle-1.0.0-SNAPSHOT.jar
), a feature definition (./feature/target/cowsay-1.0/OSGI-INF/SUBSYSTEM.MF
) and an esa file (./feature/target/cowsay-1.0.esa
).
Using featureManager
on IBM WebSphere Liberty Profile
The tool featureManager
is only available on IBM WebSphere Liberty Profile, but not on Open Liberty. This is one of the few differences between those two.
To install the built esa file, use this command:
# Step 0 (optional): uninstall any previouss version.
$HOME/.local/apps/wlp-22.0.0.5/bin/featureManager uninstall --verbose cowsay-1.0
# Step 1: Install the feature from a file.
$HOME/.local/apps/wlp-22.0.0.5/bin/featureManager install --verbose --to=usr $PWD/feature/target/cowsay-1.0.esa
Using featureManager
on Open Liberty
While the featureManager
utility is not available on Open Liberty, you can use any WLP installation to install the feature to OL. Just set the USER dir via an environment variable:
export WLP_USER_DIR="$HOME/.local/apps/ol-22.0.0.5/usr"
# Step 0 (optional): uninstall any previouss version.
$HOME/.local/apps/wlp-22.0.0.5/bin/featureManager uninstall --verbose cowsay-1.0
# Step 1: Install the feature from a file.
$HOME/.local/apps/wlp-22.0.0.5/bin/featureManager install --verbose --to=usr $PWD/feature/target/cowsay-1.0.esa
As I have the variable WLP_USER_DIR
set to $HOME/.local/share/wlp-usr
in my $HOME/.profile
file, I do not need to set any variable and will see the shared servers on any version of (Open)Liberty.
Installing the feature manually
If you do not want IBM WebSphere Liberty Profile to hit your harddisk for whatever reason, you can install the feature manually.
Assume you are in either your $WLP/usr or $WLP_USER_DIR dir. The resulting layout should look like this:
bmarwell@wells ~/.local/share/wlp-usr $ tree extension -lFh
[ 6] extension/
└── [ 116] lib/
├── [ 18] features/
│ └── [ 545] cowsay.mf
└── [1.5M] io.github.bmarwell.liberty.cowsay.bundle_1.0.0.jar
2 directories, 2 files
copy the bundle file
./bundle/target/cowsay-bundle-1.0.0-SNAPSHOT.jar
to$WLP_USER_DIR/extensions/lib/${groupid}.bundle_${version}.jar
.copy the feature descriptor
./feature/target/cowsay-1.0/OSGI-INF/SUBSYSTEM.MF
to$WLP_USER_DIR/extensions/lib/features/${featurename}.mf
.
___________________________________ < That's it! Thank you for reading! > ----------------------------------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || ||
Where to go from here
This article only covered the basics. There are many things on my mind which need to be explained in further blog posts.
Be sure to follow my Open Liberty tag on this blog!
- Using and relying on existing features
You can use existing features, like JNDI for browsing registered datasources or servlet and JSPs for creating simple endpoints. It gets even more complicated with version ranges, so I will dedicate another article to this topic.
- Adding integration tests
While you can probably easily add unit tests to cover your code logic, integration tests are a different thing. You’d want to see if your plugin actually can be installed and started on Liberty, right?
As of writing, the liberty-maven-plugin
sadly does not support maven property resolution nor maven reactor feature resolution yet. I have opened an issue on GitHub for the reactor esa module as well as for the maven property resolution.
As a workaround, manually copying works. You can see this in action in the actual code repo: https://github.com/bmarwell/cowsay-liberty-feature.
- Manually writing
.esa
andbundle
manifest files This is what I had to do on my other non-public project. Some statements, like
Subsystem-Content
are auto-generated and cannot be overriden. The only way around this is to enable resource-filtering and configure the plugins to use the filtered manifest file instead.- Extending the idea of this feature
Yes, of course I created a "real" repository! Head over to https://github.com/bmarwell/cowsay-liberty-feature. :-)
… and many more ideas.