7.4 Custom Bundle Compatibility

Introduction

So my friend Thiago Moreira posted the following (slightly edited) message to our internal Slack:

Hey, so I've built a module for 7.4 GA5, but when it is deployed to 7.4 GA16, I get a bunch of "unresolved requirement" exceptions and the module won't be available. Is there something I need to set so my module will work on both GA5 and GA16?

I was actually kind of surprised to hear this. Historically, when building an artifact I always say to target the minimum version you can run on such as GA1, and they would work even on later GAs as long as the APIs weren't actually changed. Certainly this should be the same in 7.4?

How was this possible in earlier 7.x versions?

OSGi Package Version Dependency Handling

In 7.x versions prior to 7.4, I always recommended (and adhered to myself) the policy of targeting the earliest GA/FP that provided the necessary functionality, api, whatever and let OSGi handle ensuring your bundle was deployed in a container at that version or newer.

This was a solid strategy because when you define a dependency on, say, portal-kernel for version 7.3 GA1, this will internally be declared as a number of package dependencies, i.e. dependencies on com.liferay.portal.kernel.util, com.liferay.portal.kernel.dao, etc. As developers we don't really see this unless we crack open the compiled jar and review the MANIFEST.MF file's Import-Package declaration.

If you do actually crack it open and check out the Import-Package line, you'll see a bunch of entries like:

Import-Package: aQute.bnd.annotation.metatype;version="[1.45,2)",com.l
 iferay.petra.io;version="[1.3,2)",com.liferay.petra.string;version="[
 1.4,2)",com.liferay.portal.configuration.metatype.annotations;version
 ="[1.3,2)",com.liferay.portal.configuration.metatype.bnd.util;version
 ="[2.0,3)",com.liferay.portal.kernel.exception;version="[8.4,9)",...

This is the data that OSGi uses at runtime to determine if the available package version(s) in the environment will satisfy the requirement or whether you get the dreaded unresolved requirement exception.

Each package declaration in the manifest has its own version range, so for example, the bundle I pulled the example from has a dependency on the com.liferay.portal.kernel.exception package with a version range of 8.4 or greater, but less than 9.

Under 7.0 through 7.3, the Liferay convention was to only change major package version number on major releases, so we might have seen the com.liferay.portal.kernel.exception package be versioned at 7.x in 7.2, 8.x in 7.3, and we would expect 9.x in Liferay 7.4.

The range then, for the [8.4,9) syntax, means that the module itself ran on some fixpack version of 7.3 or later, but it would not run on 7.4. This was a really effective way to build our bundle once and know that, regardless of what fixpack was applied into the environment, our module would continue to resolve.

Note that although the bundle might resolve in a later 7.3 fixpack, that wouldn't necessarily mean the bundle wouldn't break due to changes that some fixpack introduced. Most of the time, though, we were fortunate in that Liferay attempted to limit introducing breaking changes in fixpacks to avoid this hassle.

Changed Conventions

So when Thiago shared his message, at first I thought there was something wrong in his project. Certainly if he was compiling and targeting GA5, the convention I assumed was still in place would mean that the bundle would also work on GA16 too.

But, I found out that my assumption was wrong.

That's why we're here today ;-)

In Liferay 7.4, the convention has changed so that major package versions can be tied to the CE 7.4 GA releases and the DXP 7.4 U releases.

There are a number of good reasons for this change:

  • It can force developers to review their customizations at each bundle release to ensure they are still compatible.
  • It allows Liferay to remove deprecated classes and methods rather than carry that legacy debt forward any longer than necessary.
  • It allows Liferay to introduce new features and capabilities.

For the development team, though, this could introduce some new complexities:

  • Deploying an updated bundle would require coordination w/ the development team to rebuild artifacts targeting new bundles.
  • Tracking artifacts targeting specific bundles in an environment where dev, test, QA and prod might be at different bundle versions could get tricky.
  • Depending upon your software configuration management policies, building and deploying a new artifact whose only change might be a Liferay target bundle version change may still force a complete regression test requirement on these "new" builds.

Options

Depending upon your perspective, you may fall on the side of "Boy, am I glad Liferay made this change..." or you could be "We can't abide this new model...".

The good news that I have for you my friends, there are options that will make either side happy...

If you're in the camp that says it is good to rebuild your modules to target a specific bundle release to ensure maximum compatibility, your option is easy. Update the liferay.workspace.product property in your gradle.properties file in your workspace to the new target version and then do a clean build. All of your artifacts will be built for the specific bundle release and you'll know if your code is using a previously deprecated method or class that has been removed.

If you're in the camp that says a single build should resolve under any U bundle and I'll deal with issues (such as ClassNotFoundExceptions or NoSuchMethodExceptions when using a deprecated object that was removed) when/if they come up, your option is also easy. Just open the bnd.bnd file in the modules that you want this coverage in and add the following:

-consumer-policy: ${replacestring;${range;[==,==]};.*,(.*)];$1}
-provider-policy: ${replacestring;${range;[==,==]};.*,(.*)];$1}

These two BND directives effectively define the default range for package version matching for you, going from [8.4,9) to [8.4,infinity).

This will allow OSGi to match on 8.4 or any greater version, but you do still have control if you need to limit version range. For example, if you knew that a class was only available in the com.liferay.portal.kernel.exception package through package version 12.x, you could add this restriction by explicitly defining an Import-Package version:

Import-Package: com.liferay.portal.kernel.exception;version="[8.4,13)",*

This format applies a specific version range on a given package and ensures that if your module is deployed at some 7.4 version that has package version 15.x, your bundle will not resolve and lead to an unresolved requirement exception.

Note the asterisk at the end of the directive, this is necessary so any other imported packages can be pulled in as necessary for the code.

If you go this route, the only guarantee you have is that your module will resolve by OSGi, there are no guarantees that your module would continue to work using package version 9.x or 10.x or whatever. You would still be responsible for updating your code at that point.

Conclusion

So depending upon which camp you're in, you now know what is coming and you can prepare for it accordingly.

As to which camp I'm in, I find myself more on the side of a single build, tested artifact used everywhere until it no longer works, so I'm going with the bnd.bnd changes.

That said, my position is based on my being a lazy developer that is not involved in all of the regular, active environment management responsibilities, so my position is not going to be good for everyone.

If you ask me what the right camp is, or what camp Liferay recommends, both answers are solidly in the rebuild your artifacts targeting a release camp. Liferay and Support are much more concerned with the guarantees stemming from rebuilt artifacts targeting the specific release they are deployed to.

But, whatever camp you're in, hopefully you find the option here to make that camp happy...