Blogs
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.
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 ;-)
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.
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...