Boot Spring Context within each Java Module

On 21st of september 2017 Java 9 was released including the much anticipated Java Platform Module System (aka Jigsaw). On 25th of september 2018 Java 11 made this available in a Long Term Support release.

At the moment of writing, the spring framework only supports java modules by reserving their own automatic module names spring.core, spring.context, spring.beans, … . Nothing is known about how spring plans to align application contexts with the module boundaries.

Aligning spring contexts with module boundaries

One approach could be to start an application context covering all modules on the module-path.

One spring application context covering all modules
One spring application context covering all modules does not provide the desired modularity.

There are some problems with this approach:

  • it would use spring beans cross-module which by-passes the service loader mechanism of the java module system
  • it implies less encapsulation
  • it implies that modules participating in this application context depend on such a context being present
  • it implies possible name clashes between spring bean names

Another more preferred approach is to start an application context isolated within a single module, and repeat this for every module requiring a spring application context.

Separate spring context within each module
A separate spring application context within each individual module with injection of services using the java module system service loader mechanism.

We want to achieve the following:

  • encapsulate the spring application context within the module
  • go through the java module system service loader for cross-module services.
  • inject used services from other modules just like other spring beans
  • easily provide a bean as service from a module

A small framework to achieve spring context modularity

Leveraging some parts of the module system and some clever annotations we can boot spring contexts within java modules. The code for the io.github.tomdw.java.modules.context.boot module enabling this can be found on https://github.com/tomdw/java-modules-context-boot.

The maven dependency to use is (Note: it is available on maven central).

<dependency>
  <groupId>io.github.tomdw.java.modules.spring</groupId>
  <artifactId>java-modules-context-boot</artifactId>
</dependency>

@ModuleContext and ModuleContextBooter.boot()

In Java 9+ it is possible to use annotation on modules themselves in the module-info.java file. We can exploit this as follows:

import io.github.tomdw.java.modules.context.boot.api.ModuleContext;
import io.github.tomdw.java.modules.spring.samples.basicapplication.internal.MainApplicationConfiguration;

@ModuleContext(mainConfigurationClass = MainApplicationConfiguration.class)
module io.github.tomdw.java.modules.spring.samples.basicapplication.application {
    requires io.github.tomdw.java.modules.context.boot;

    opens io.github.tomdw.java.modules.spring.samples.basicapplication.internal to spring.beans, spring.core, spring.context;
}

Additionally, the code below boots the spring context initialisation from any place within your application:

public static void main(String... commandLineArguments) {
    ModuleContextBooter.boot();
}

The effect is that every resolved module that has a @ModuleContext annotation will be extended by starting a new spring application context using the given mainConfigurationClass. Such a class is a default spring configuration class, for example the following is using basic component scanning to find the necessary spring beans:

@Configuration
@ComponentScan
public class SpeakerConfiguration {
}

Note that this does not require you to export the packages containing the spring configuration classes in any way. You only need to open the package specifically to the spring modules for deep reflection using the ‘opens’ keyword in the module-info. As a result, the spring application context details remain encapsulated within each module.

Injecting ServiceLoader Services between module application contexts

In the previous paragraph we described how to bootstrap spring application contexts within each module. A modular application has no use for multiple modules if there is no way for them to interact. Services exposed by modules allow to integrate different modules in the java module system. A module will provide the service using the provides keyword, for example:

@ModuleContext(mainConfigurationClass = SpeakerConfiguration.class)
module io.github.tomdw.java.modules.spring.samples.basicapplication.speaker {
    exports io.github.tomdw.java.modules.spring.samples.basicapplication.speaker.api;
    requires io.github.tomdw.java.modules.context.boot;

    provides SpeakerService with DefaultSpeakerService;

    opens io.github.tomdw.java.modules.spring.samples.basicapplication.speaker.internal to spring.beans, spring.core, spring.context;

}

Other modules indicate using the services through the uses keyword, for example:

@ModuleContext(mainConfigurationClass = MainApplicationConfiguration.class)
module io.github.tomdw.java.modules.spring.samples.basicapplication.application {
    requires io.github.tomdw.java.modules.spring.samples.basicapplication.speaker;
    requires io.github.tomdw.java.modules.context.boot;

    opens io.github.tomdw.java.modules.spring.samples.basicapplication.internal to spring.beans, spring.core, spring.context;

    uses SpeakerService;
}

For the ‘SpeakerService’ in the example, its interface needs to be in an exported package and its implementation DefaultSpeakerService needs to have a default constructor or a static provider method returning an instance of the service.

In the using module we would then call the ServiceLoader.load API to retrieve instances of the service. A new instance will be created through the constructor or provider method each time the ServiceLoader is asked for an instance. This might result in multiple instances of the same service.

We want:

  • to use a spring bean from the application context of the providing module as service instance
  • only 1 singleton instance of that service to be used everywhere
  • to use the service by simply injecting it as a bean in the application context of the using module

Provide spring beans from the module’s application context as services

We use the static provider method and make sure to returns a bean of the desired type from the module’s spring application context:

@Named
public class DefaultSpeakerService implements SpeakerService {

    public static DefaultSpeakerService provider() {
        return ModuleServiceProvider.provide(DefaultSpeakerService.class);
    }

    ...
}

The ModuleServiceProvider.provide  method will make sure to use the application context of the provided implementation class to return the correct singleton bean.

Inject a service instance into another module’s application context

We use an additional @ModuleServiceReference annotation to indicate that the bean to inject is to be looked up through the ServiceLoader API. This way it is very simple to inject cross-module services:

@Named
public class MessageGenerator {

    @ModuleServiceReference
    private SpeakerService speakerService;

    ...
}

Every spring context is enriched with a processor to retrieve the necessary services through the ServiceLoader API and make them available for injection in the spring context of that module.

Conclusion

Re-visiting our schema of before we now have a simple modular way to boot spring applications.

Separate spring context within each module

Complete encapsulation of spring contexts within modules is achieved together with easy integration of these modules by injecting cross-module services as normal spring beans.

Hopefully a similar approach will get integrated in the spring framework in the near future? Otherwise we are limited to only creating monolithic spring applications.

5 thoughts on “Boot Spring Context within each Java Module

  1. Hello Tom! Great article!

    I’m also updating Spring app to use Java modules and need to provide all Services implemented under the same package to other modules. It’s quite cumbersome writing “provides” for each of them at module-info.
    Have you seen any way to provide all of them at once?

    Thanks in advance.

    Liked by 2 people

    1. I have not seen such an approach yet. Maybe it works when you provide a ‘List’ as type but I’m afraid this does not work either. The class you specify behind the ‘with’ can also be a MyServiceFactory with a static provider method and this one could return a list. Although I’m not sure how the generic types in the list get picked up when retrieving the services.

      Would be good if the services mechanism had more first class support for this.

      Liked by 1 person

      1. Great Tom! Sorry for late reply and thanks for your thoughts! We still haven’t reached a good solution for this problem, but factories may work for us, I’ll give it a try 🙂

        Thanks again!

        Liked by 1 person

Leave a reply to Tom De Wolf Cancel reply