Fixing Module Package Access Modifiers

If you're a Java Architect or Senior Developer, you know just how important the Java access modifiers are.

Each choice about what to make public, protected, private or package protected is important from an architectural perspective. If you make the wrong choices you can expose too much of your implementation or not enough, you can give subclasses unlimited ability to change internal functionality or little access at all.

If you've ever worked on a library or framework project, either public or a company internal project, you know that these decisions are even more important. With these types of projects, if you make the wrong decision about access modifiers you can end up with irate users or an unused library.

So there exists two different sets of rules, one for app developers and one for lib developers.  App developers are going to use a lot of private to hide stuff and public to expose; they'll define pojos w/ getters but no setters and perhaps a package private constructor to initialize final fields. Methods are typically public or private and protected only comes into play if there are known plans to subclass.

Library developers swing the other way, allowing for every class to potentially be subclassed, extensive use of protected over private, no use of package protected, etc. Basically implementation details will be protected from those using the class yet exposed for subclasses to extend or override.

Rules for lib developers are not hard and fast, some libraries certainly do a better job than others for exposing necessary access points.

I'm sure most of us have been there... Using a class in someone's jar where they declare, for example, a private field with a public getter but no setter, resulting in a class that is difficult to extend and customize. Then we have to break out our Reflection skills to access the field, change the access and update the value. Obviously we don't want to do these things, but we get forced into it because the library developer used application developer rules when defining the access modifiers.

OSGi Access Modifiers

OSGi bundles has its own set of "access modifiers".  We've seen those in the bnd.bnd files, at a package level you can choose to export packages or mark them as private.

Choices you make along these lines affect what you can extend/override in other bundles. If you mark a package as private, the classes are not available to another bundle to leverage and use.

Just like app vs lib developer access modifier rules, there is a similar distinction for OSGi application bundle developer rules and OSGi library bundle developer rules.  For the app bundle developer, packages are exported if they can be used by other modules, otherwise they are private to protect them. For lib bundle developers, you're going to export pretty much every package because you can never know how your library module will be used.

What I Think is Wrong

Probably my biggest complaint with the Liferay 7 CE / Liferay DXP modules is that I believe the developers were creating modules as though they are app bundle developers when, in fact, they should have been library bundle developers.

For example, the Liferay chat portlet... The Liferay chat portlet does not export a single package; every package in the module is private. As an application portlet bundle developer, this is probably exactly the decision I would make to protect my code, it won't need to be extended or overridden, as the developer if that comes up in the future I can just do it.

But the Liferay developers, they should not have built it this way in my opinion. Me, I may have a need to make some significant changes to the chat portlet, not just for JSP changes but perhaps also some logic. From that point of view, the Liferay chat portlet is a library bundle, a "base" bundle that I want to be able to extend or override. The com.liferay.chat.web.portlet.ChatPortlet is not full Liferay MVC, so all business logic is tied up in that class. If I want to customize the chat portlet, I need to copy the class and make my change and hope that Liferay doesn't update the portlet.

In order to complete my customization, I might need to change a method in the ChatPortlet itself. Sure, with OSGi I can replace the OOTB portlet class with my own, but I really want to be able to do something like:

@Component(...)
public class MyChatPortlet extends ChatPortlet {...}

This would allow me to replace the OOTB portlet using a higher service ranking for mine, yet I can keep as much of the original logic as-is without taking over responsibility for maintaining the full class myself.

For another concrete example, take the Liferay Login portlet.  This portlet is full-on Liferay MVC so, if I want to override the create account action, I just need to register an instance of MVCActionCommand with the right mvc.command.name and a higher service ranking. But again, since most of the packages in the Liferay Login portlet are private, I cannot do something like:

@Component(...)
public class CustomCreateAcountMVCActionCommand extends CreateAccountMVCActionCommand {...}

If my requirement is just to do some additional work in the addUser() method, I don't want to copy the whole Liferay class just to be able to tweak one method.  What happens when the next release comes out? I'd have to copy the whole class in and release again. At least by extending I only have to worry about keeping in sync the stuff I change, everything else is extended.

Can We Fix It?

As Bob the Builder says, "Yes We Can!", and it turns out it is really, really easy!

Let's say we want to tackle being able to extend Liferay's CreateAccountMVCActionCommand class. Our requirement is that we need to log whenever an account is being created. I know, pretty lame, but the point here is to extend a class which Liferay didn't plan on our extending - once we're over that hump, any additional requirements will be easy to tackle.

So let's get started. The first thing we need is a Fragment bundle. That's right, you read correctly, a Fragment bundle.

blade create -t fragment -h com.liferay.login.web -H 1.0.0 open-liferay-web

That gets us started. We need to open the bnd.bnd file and we're going to be doing two basic things:

  1. Copy in most of the stuff from the original bnd.bnd file. The only change we want to make is with the exported packages, so we want to keep everything else.
  2. Change the exported package line (or add one) to include the packages we want to export, and we'll also change to a version range.

I've gone ahead and done this, and here's what I ended up with:

Bundle-Name: Open Liferay Login Web
Bundle-SymbolicName: open.login.web
Bundle-Version: 1.1.19
Fragment-Host: com.liferay.login.web;bundle-version="[1.0.0,2.0.0)"

Export-Package: com.liferay.login.web.constants,\
	com.liferay.login.web.internal.portlet.action
Import-Package:\
	javax.net.ssl,\
	\
	*
Liferay-Releng-Module-Group-Description:
Liferay-Releng-Module-Group-Title:

So you can see that I satisfied #1 above, I've kept the import packages and the Liferay-Releng guys.

For #2, my export package statement was updated so now we're going to be exporting the com.liferay.login.web.internal.portlet.action package. This will allow us to subclass Liferay's action command by making it visible.

I also tweaked the Fragment-Host version. Instead of using a single version, I've changed it to a version range. Why? Because this fragment bundle doesn't care what version is actually deployed, we're just planning on exporting the package regardless of version.

And that's it! See, I said it was easy. You don't really need any other files, you're basically just going to be building and deploying a jar w/ the overriding OSGi manifest information.

Testing

Testing is also kind of easy. We know we want to extend the CreateAccountMVCActionCommand, so we just create a bundle and specify the contents. I did that already, too, and here's what I got:

@Component(
	property = {
		"javax.portlet.name=" + LoginPortletKeys.FAST_LOGIN,
		"javax.portlet.name=" + LoginPortletKeys.LOGIN,
		"mvc.command.name=/login/create_account",
		"service.ranking:Integer=100"
	},
	service = MVCActionCommand.class
)
public class CustomCreateAccountMVCActionCommand extends CreateAccountMVCActionCommand {

	@Override
	protected void addUser(ActionRequest actionRequest, ActionResponse actionResponse) throws Exception {
		_log.info("About to create a new account.");

		super.addUser(actionRequest, actionResponse);
	}

	@Reference(unbind = "-")
	protected void setLayoutLocalService(
		LayoutLocalService layoutLocalService) {
		super.setLayoutLocalService(layoutLocalService);
	}

	@Reference(unbind = "-")
	protected void setUserLocalService(UserLocalService userLocalService) {
		super.setUserLocalService(userLocalService);
	}

	@Reference(unbind = "-")
	protected void setUserService(UserService userService) {
		super.setUserService(userService);
	}

	@Reference(unbind = "-")
	protected void setAuthenticatedSessionManager(AuthenticatedSessionManager sessionMgr) {
		update("_authenticatedSessionManager", sessionMgr);
	}
	@Reference(unbind = "-")
	protected void setListTypeLocalService(ListTypeLocalService listTypeLocalService) {
		update("_listTypeLocalService", listTypeLocalService);
	}
	@Reference(unbind = "-")
	protected void setPortal(Portal portal) {
		update("_portal", portal);
	}

	protected void update(final String fieldName, final Object value) {
		try {
			Field f = getClass().getSuperclass().getDeclaredField(fieldName);

			f.setAccessible(true);

			f.set(this, value);
		} catch (IllegalAccessException e) {
			_log.error("Error updating " + fieldName, e);
		} catch (NoSuchFieldException e) {
			_log.error("Error updating " + fieldName, e);
		}
	}

	private static final Log _log = LogFactoryUtil.getLog(CustomCreateAccountMVCActionCommand.class);
}

Oh, crap. What is all of this junk?

Well, first let's get the necessary stuff out of the way. Our @Component reference has the necessary properties and service ranking so OSGi will use our action command class, which we are now extending Liferay's CreateAccountMVCActionCommand. We also have the overriding addUser() method to log when we are about to create an account, so we have satisfied our requirement.

The rest of the class, well that is necessary to inject the right OSGi references into the super class that it expects. Some of these are easy, such as the layout service and the two user services. The others are hard, the authenticated session manager, list type service and the portal instance.

Remember I started this blog saying that the rules for a library developer are different than an app developer, and when you have a bad library class you're left to using Reflection to update a super class? Yep, here's an example. Now, I can't really fault Liferay here for this because they created the module as though they were an app module developer, so the fact that they used app developer rules here is no surprise. Fortunately though I could use Reflection to get to the super field and update it appropriately.

Conclusion

So, when we build and deploy these two modules and create a new account, we find we have been successful:

13:30:31,360 INFO  [http-nio-8080-exec-6][CustomCreateAccountMVCActionCommand:39] About to create a new account.

Through a simple (really simple) fragment bundle we were able to export a package that Liferay did not export. From there, we can extend classes from that package to introduce our own modifications without having to copy everything from the original.

It's important to note the hurdles we had to bypass for the OSGi stuff, especially the Reflection usage to update the super class.

If you're going to go down this path, you will be doing things like this. There's no way around it, not all @Reference usage in Liferay classes are tied to methods; when they are, great, but when they're not you'll have to peel them open yourself.

Hope this helps you on your Liferay 7 CE / Liferay DXP developer journey!

 

Blogs
Great post! I especially like the reference to Bob the builder lol. Question though David, can this same approach also be used in non-liferay custom portlets? By this I mean, for example, let's say I write a custom registration portlet. Can you then use this same technique a to expose some of my portlets packages to I alter the logic? Im assuming yes? Is the akin to plugins that extend plugins from the old system?
Sure, this is just OSGi magic, has nothing to do with liferay bundles vs your own. For customers of SIs, for example, where the SI has delivered something that is similarly private, this technique can be used to open their packages too.
[...] The other problem is, that the Elasticsearch adapter module is only exposing the com.liferay.search.elasticsearch.settings subpackage. So, if you need to reference any package inside the adapter in... [...] Read More

Doesn't give you compile time error in custom module where you override original class. When you write extends original class but that class may not be expose with existing module. We are creating fragment for that even though it's giving me compile time dependancy error.

 

https://community.liferay.com/forums/-/message_boards/message/111525356

Thanks a lot for writing this Dave! This is exactly the scenario I was running into. I found I needed to export some internal packages in a Liferay module and this is just the walkthrough I needed.