Blogs
Want to share some Spring services from your Spring Portlet Wars? This blog entry will show you how...
Introduction
I've been working with a client to upgrade their Spring portlet wars to Liferay 7.4. And the update is kind of big as we're upgrading Spring, changing to PortletMVC4Spring (needed for Spring 5), ...
They have a large number of Spring portlet wars, but in reviewing the code I noticed that each of the wars had duplicate Spring services defined into the Spring context.
Of course, everything worked because the different portlet wars have their own context, but this is quite wasteful. It takes longer for the portlet wars to start because they all have to instantiate the same services over and over. It takes more memory since each portlet war has a duplicate. And it uses more of the Metaspace to load separate, duplicate classes into each of the portlet war class loaders.
So this got me thinking... If these services were OSGi services, then I could have one shared service for all of the portlet wars, and many of these issues would fall away...
But I wasn't sure it could be done until I tried it...
The Challenges
After reviewing all the code, there were a number of challenges that needed to be addressed.
Proxies
So I was fortunate in that most of the Spring services had separate interfaces and implementations, so this was easier to deal with.
For the ones that were not separated like this, I just extracted an interface and then built a proxy class.
Intellij made this process very easy... The Refactor menu lets me extract an interface off of the original class. Then I could start my proxy service implementation such as:
@Component("myService") public class MyServiceImpl extends AbstractSelfRegisteringService implements MyService { @Autowired private com.example.internal.LegacyService _legacyService;
In Intellij, I could then use the Code -> Delegate Methods,
select the _legacyService
member, then select all of the
methods from the legacy service to expose. Intellij would instantiate
all of the proxy methods, and in no time at all I had created an
interface for the service and a proxy implementation, and I didn't
even have to modify LegacyService in any way.
Exporting Packages
As we all know with regular OSGi module jars, any code that needs to be shared must be exported.
This is quite easy to do also. Just open up the
liferay-plugin-package.properties
file in the portlet war
that will expose the services and add an Export-Package
declaration, just as you would in the bnd.bnd
files.
Export-Package: \ com.example.blah.model, \ com.example.blah.exception, \ com.example.blah.service
This will export the packages so any classes/interfaces they contain will be visible to other modules in the OSGi container.
Compiling
So there were two issues that I faced with using a war file.
First, a war file really isn't like a jar file, the classes go into
the /WEB-INF/classes
directory and that makes it
impossible for another jar/war project to mark it as a dependency.
This I solved by building a jar from the war file for dependency usage. In your Maven pom, you just need to add a line of configuration to the war plugin:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> <configuration> <filteringDeploymentDescriptors>true</filteringDeploymentDescriptors> <attachClasses>true</attachClasses> </configuration> </plugin>
In Gradle, we just need to tweak the build.gradle file with the following:
assemble { dependsOn jar } jar { classifier = 'classes' }
Both of these will create the normal war file,
myproject-1.0.war
, but it will also generate a separate
jar, myproject-1.0-classes.jar
, that can be used by other
modules/wars for compiling.
Because of the use of the classifier, when we depend on the war, we would do it in Maven like:
<dependency> <groupId>com.example</groupId> <artifactId>myproject</artifactId> <version>1.0</version> <classifier>classes</classifier> <scope>provided</scope> </dependency>
And in Gradle like:
dependencies { compileOnly group: 'com.example', name: 'myproject', version: '1.0', classifier: 'classes' }
Other Spring War Usage
So Spring portlet wars kind of are real OSGi bundles (from the
war -> wab conversion), but not really. There is no support for
Declarative Services (DS) or Service Component Runtime
(SCR), so you can't simply use @Component
and
@Reference
to access an OSGi service.
Instead, you need to use a ServiceTracker
.
Because I was going to be accessing the services in a bunch of Spring portlet wars, I chose to create static util files for all of my interfaces. That way I could have the shared service tracker use and not replicate that code all over the place.
So right next to my service interface, I'd place my static util class:
public class MyServiceUtil { public static int getMyIntValueFromService(final String param1) { return getService().getMyIntValueFromService(param1); } public static MyService getService() { return _serviceTracker.getService(); } private static ServiceTracker<MyService, MyService> _serviceTracker; static { _serviceTracker = ServiceTrackerFactory.open( FrameworkUtil.getBundle(MyService.class), MyService.class); } }
So the other Spring portlet wars can just invoke
MyServiceUtil.getMyIntValueFromService(param);
and
they'll be using the shared service w/o having to do anything with
service trackers themselves.
One thing to keep in mind with the static utils, you can call them even when the service has not been registered. When this happens, it typically means your service didn't start or register. This could stem from a Spring issue, a code issue, etc.
Service Registration
So I've covered the challenges, but I only hinted at the service registration itself...
I'm doing that using a base class:
public abstract class AbstractSelfRegisteringService implements SelfRegisteringService { private ServiceRegistration _serviceRegistration; private String beanName; @Override public void setServiceRegistration(ServiceRegistration serviceRegistration) { _serviceRegistration = serviceRegistration; } @Override public String getBeanName() { return beanName; } @Override public void setBeanName(String beanName) { this.beanName = beanName; } }
The SelfRegisteringService
interface is:
public interface SelfRegisteringService extends InitializingBean, BeanNameAware { String getBeanName(); void setServiceRegistration(final ServiceRegistration serviceRegistration); default void afterPropertiesSet() throws Exception { registerSelf(); } default void registerSelf() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { setServiceRegistration(registerService(getBeanName(), this)); } default ServiceRegistration registerService(final String beanName, final Object service) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Bundle bundle = FrameworkUtil.getBundle(service.getClass()); BundleContext bundleContext = bundle.getBundleContext(); Class<?> clazz = service.getClass(); if (ProxyUtil.isProxyClass(clazz)) { InvocationHandler invocationHandler = ProxyUtil.getInvocationHandler(service); Class<?> invocationHandlerClass = invocationHandler.getClass(); Method method = invocationHandlerClass.getMethod("getTarget"); Object target = method.invoke(invocationHandler); clazz = target.getClass(); } OSGiBeanProperties osgiBeanProperties = AnnotationLocator.locate(clazz, OSGiBeanProperties.class); Set<String> names = OSGiBeanProperties.Service.interfaceNames(service, osgiBeanProperties, PropsValues.MODULE_FRAMEWORK_SERVICES_IGNORED_INTERFACES); if (names.isEmpty()) { return null; } HashMapDictionary<String, Object> properties = HashMapDictionaryBuilder.<String, Object>put("bean.id", beanName).put( "origin.bundle.symbolic.name", () -> { return bundle.getSymbolicName(); } ).build(); if (osgiBeanProperties != null) { properties.putAll(OSGiBeanProperties.Convert.toMap(osgiBeanProperties)); } return bundleContext.registerService(names.toArray(new String[0]), service, properties); } }
This automates the service registration into OSGi using the bean
name of the service. If you use the Spring @Service
annotation for your service, the bean name is the name of the class w/
the first character converted to lower case. Since my implementations
are all like MyServiceImpl
, I chose to use
Spring's @Component
so I could designate the bean name as
"myService".
Since MyServiceImpl
inherits from this base class,
when the bean is created it will automatically register itself into
OSGi and other module jars can @Reference
them in and
other Spring portlet wars can use the static utils.
Conclusion (and Problems)
So yeah, this is basically it. Using this technique, all of my Spring-based services can be instantiated and wired up in one Spring portlet war and they can be exposed for others to reuse.
There is an underlying problem though and you may see this come up in your logs - startup order.
When the server is started up, module jars are typically started first and wars are started last. So this means your jars will not be available until the Spring portlet war has finished starting. And since order isn't guaranteed when starting wars, the one registering the services could be the last one to start.
But once the Spring portlet war is deployed and the services are registered, OSGi should resolve everything and the environment should be ready to go.
Additionally I should say that this is not the only way to expose Spring services. My good friend Neil Griffin told me that I could do the same kind of thing using CDI (maybe it would be easier that this method). I haven't played much with that, so maybe that's a future blog post (if he doesn't beat me to it).