Blogs

Blogs

Exposing Spring Services into OSGi

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

2
Blogs