Liferay DXP lifecycle events: chaos vs. order

Historically there have been a number of extension points in Liferay that enable developers to hook into portal events and add their own custom/additional behaviour. In Liferay DXP this is still the case and the list below shows when certain events are fired and in which order. You’ll notice that a number of events are mentioned multiple times because they’re shown in the context of a specific action instead of just as a straightforward list of all the available event types:

  • Once during a server start/stop cycle:
    • global.startup.events
    • application.startup.events (1 for every portal instance ID)
    • application.shutdown.events (1 for every portal instance ID)
    • global.shutdown.events
  • For every login:
    • servlet.service.events.pre
    • servlet.session.create.events
    • servlet.service.events.post
    • login.events.pre
    • login.events.post
  • For every logout:
    • servlet.service.events.pre
    • logout.events.pre
    • servlet.session.destroy.events
    • logout.events.post
    • servlet.service.events.post
  • For every every HTTP request:
    • servlet.service.events.pre
    • servlet.service.events.post
  • For every page that is updated:
    • servlet.service.events.pre
    • layout.configuration.action.update
    • servlet.service.events.post
  • For every page that is added/updated:
    • servlet.service.events.pre
    • layout.configuration.action.delete
    • servlet.service.events.post

Even with this many extension points there are a lot of cases where you would want to add more than 1 additional custom behaviour to an event. Because developers like to create small, dedicated modules, instead of putting everything together in 1 big module, it is also important to be able to have these custom event extensions, each in their own class/module, run in a certain specific order. Otherwise the overall behaviour would be pretty random and erratic.

In Liferay 6.2 this was pretty simple. You just had a number of properties in your portal-ext.properties that contained a comma separated list of implementation classes that would be run in the order they were present in this list, e.g.:

login.events.post=com.liferay.portal.events.ChannelLoginPostAction,com.liferay.portal.events.DefaultLandingPageAction,com.liferay.portal.events.LoginPostAction

This was a very simple and easy to understand way of doing this. When I tried to do the same in Liferay DXP it quickly became clear that with the switch to OSGI it now didn’t work this way anymore. A single custom event is now a standalone OSGI component, e.g.: https://github.com/liferay/liferay-blade-samples/tree/master/maven/blade.lifecycle.loginpreaction, and I couldn’t find any place where you could still define a list like I did before. So from the example it wasn’t exactly clear how the order of the components could be influenced.

Initial testing showed that the order seemed to be determined by when the bundle was started. I tested this by making 2 small bundles that each contained a dummy implementation of the same lifecycle event, but each outputted a different message. After installing and starting both bundles I got the messages I expected in the order the bundles were started. Then I stopped and started the bundle that was started first and triggered the event again and the order of the messages was the other way around.

Because I really don’t want this kind of behaviour, but something more deterministic, I was hoping that there was some sort of solution for this. This was when I remembered something about service ranking. In order to override a default Liferay service/component you just need to implement the correct interface and provide your implementation (component) with a service.ranking value thats larger that the one present on the original Liferay implementation. This same property can also be seen in the the Blade sample that shows how to add/override JSPs:

...

@Component(
    immediate = true,
    property = {
        "context.id=BladeCustomJspBag", "context.name=Test Custom JSP Bag",
        "service.ranking:Integer=100"
    }
)
public class BladeCustomJspBag implements CustomJspBag {
...
}

The documentation of the cases above always talks about using the service.ranking property to find a single implementation for something. The highest ranked implementation wins. So now I was wondering if all these implementations are just kept in a sorted list somewhere? If this were to be the case then you’d expect Liferay to retrieve all the deployed implementations for a certain lifecycle event and use this sorting to be able to run them in the correct order. So I quickly created a bunch of custom event implementations for the same event and gave them different service.ranking values, created a bundle, installed & started it. To my surprise I got the behaviour I wanted! It seems the higher the service.ranking value on my custom event implementation, the earlier in the chain it was executed.

A quick debug and code inspection session did indeed show that EventsProcessorUtil uses ServiceTrackers to retrieve all the registered implementations for a lifecycle event and gets back a sorted list. This list is retrieved from a ListServiceTrackerBucket that internally keeps a sorted list of ServiceReferences. The ServiceReference implementation is Comparable and the comparison is based on the value of the service ranking and in the case the ranking is identical on the service ID.

The example code can be found on my Github: https://github.com/planetsizebrain/event-order-hook

博客
This is great. Thanks for explaining the implementation
This could solve my issues with application.startup.events but which order are they started lowest first or highest?
Also you talked about running things when starting your modules (I guess via GoGo shell) what action are you using for that? I really don't want to take 10 minute to restart my test system to try and fix issues I think are due to my service starting before anyone else (like I cant get PortaUtil.getDefaultCompanyId().