Replacing Liferay Components

Sometimes it takes a little bit more to replace a Liferay component...

So just a quick one today about an issue that I helped a coworker with.

A client had reported that after they restarted Liferay, they had to redeploy their custom module that had a replacement @Component for one of Liferay's components, otherwise the custom component would not work.

I replied back, "Oh, they just need to blocklist the Liferay component so their custom component is the only option."

So the client tried the blocklist and everything worked just as it should, but it doesn't really get to why blocklisting might be necessary.

Component Replacement Examples

Say, for example, that you wanted to replace Liferay's com.liferay.blogs.web.internal.portlet.action.UploadCoverImageMVCActionCommand with a custom version that you've added auditing to so you know who is uploading blog cover images, and your component class is com.example.blogs.AuditingUploadCoverImageMVCActionCommand.

Your class definition is going to basically be:

package com.example.blogs;

import com.liferay.blogs.constants.BlogsPortletKeys;
import com.liferay.portal.kernel.portlet.bridges.mvc.BaseMVCActionCommand;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCActionCommand;
...

@Component(
  property = {
    "javax.portlet.name=" + BlogsPortletKeys.BLOGS,
    "javax.portlet.name=" + BlogsPortletKeys.BLOGS_ADMIN,
    "mvc.command.name=/blogs/upload_cover_image",
    "service.ranking:Integer=100"
  },
  service = MVCActionCommand.class
)
public class AuditingUploadCoverImageMVCActionCommand extends BaseMVCActionCommand {
  ...
}

Now OSGi will tell you that the addition of the service.ranking property is supposed to make your custom component take priority over any other matching component with a lower service ranking.

Sometimes, though, this doesn't really happen, especially at restart. At redeployment, OSGi is forced to reconsider things because that is basically part of what redeployment is for.

For example, in this scenario Liferay uses ServiceTracker implementations to find all MVCActionCommand implementations for a portlet so when it is processing an action phase, it can find the right command object to hand off to.

Although your component matches the Liferay component, the service ranking may not impact the service tracker and both your custom component and Liferay's component could be in the list and would be used to process the request. Although I believe the real implementation uses a ServiceTrackerMap on the mvc.command.name, and as we all know with maps, the last one added to the map that matches a key wins.

Now, in another case, let's assume that you need a custom implementation of DLURLHelper because, for whatever reason, Liferay's com.liferay.document.library.internal.helper.DLURLHelperImpl isn't doing what you need to. So you create your own implementation, com.example.urls.CustomDLURLHelper like so:

package com.example.urls;

import com.liferay.document.library.util.DLURLHelper;
...

@Component(
  property = {
    "service.ranking:Integer=100"
  },
  service = DLURLHelper.class
)
public class CustomDLURLHelper implements DLURLHelper {
  ...
}

So you now have your own component, and per OSGi, you're using a higher service ranking and therefore your implementation should take precedence over Liferay's implementation.

When you [re]deploy your module, your component should replace Liferay's component references.

But sometimes this doesn't happen, i.e. when you restart the container.

OSGi Reference Policies and Component Overrides

Getting the wrong component stuck in a reference is a result of Liferay's general use of @Reference along with the ReferencePolicy and ReferencePolicyOption attribute default values. I wrote about them in my blog Liferay/OSGi Annotations.

Basically, when you see code like:

@Reference
private DLURLHelper _dlURLHelper;

What you're actually seeing (after inserting the defaults) is:

@Reference(policy = ReferencePolicy.STATIC, 
    policyOption = ReferencePolicyOption.RELUCTANT)
private DLURLHelper _dlURLHelper;

From the blog, let's review what both of these mean:

Policy can be either ReferencePolicy.STATIC (the default) or ReferencePolicy.DYNAMIC.  The meanings of these are:

  • STATIC - The component will only be started when there is an assigned reference, and will not be notified of alternative services as they become available.
  • DYNAMIC - The component will start when there is reference(s) or not, and the component will accept new references as they become available.

The policy controls what happens after your component starts when new reference options become available.  For STATIC, new reference options are ignored and DYNAMIC your component is willing to change.

The policyOption attribute can be either ReferencePolicyOption.RELUCTANT (the default) or ReferencePolicyOption.GREEDY.  The meanings of these are:

  • RELUCTANT - For single reference cardinality, new reference potentials that become available will be ignored.  For multiple reference cardinality, new reference potentials will be bound.
  • GREEDY - As new reference potentials become available, the component will bind to them.

The default values translate into once a reference has been resolved (meaning an available component was injected), it is not likely to change.

During deployment though, this can actually force a change, but during restart of the tomcat it is often first come, first served.

Blocklisting to the Rescue

So, what is the easiest way to resolve this issue?

Well you just blocklist the Liferay original components to prevent them from starting.

This way, when your component starts, it is the only game in town! Well, it's the only component that can satisfy the references, so Liferay will only be able to resolve any @References on the service by your component.

To blocklist a component, go to the Control Panel and then to System Settings, select Module Container under the Platform section, then find the Component Blocklist item.

In a new Liferay environment, this will be empty. When it is empty, you can just list the fully-qualified class name in the text box, otherwise you may need to hit the + button to add a new entry.

When I change the blocklisted components, I do like to perform a restart just to make sure the blocklisted component is not at all available or used as an @Reference value.

Downsides to Blocklisting

The biggest downside is, of course, that you can't @Reference Liferay's component in your own component implementation in order to "pass through" to Liferay's component, which is something we often want to do when we don't to have to build out the whole implementation ourselves (i.e. the DLURLHelperImpl that Liferay provides has a lot of functionality, if I only wanted to override a single method, I'd want to @Reference the Liferay class in and then proxy the remaining calls over to it).

Conclusion

Generally blocklisting should not be necessary as long as the service rankings are set correctly. Of course you should test this out during a deployment, a redeployment, and a container restart.

If you find that your component isn't taking over all the time, then blocklisting can be a way to ensure your component is the one that gets referenced.

But if you blocklist a component, you will not be able to use it either, so only go down the blocklisting road if you really need to.