Multiple Spring Boot applications in the same project

Multiple Spring Boot applications in the same project

I frequently use the Spring Boot framework in my demos. The latest one is no different. It shows how to achieve CQRS using two different code paths:

My use case is a banking application that offers a REST layer allowing clients to call any parts. Demoing the query part is easy enough with curl as the URL is not complex:

curl localhost:8080/balance/123          // 1
  1. Query the balance of the account 123

On the other hand, creating a new operation, e.g., a credit, requires passing data to curl. While it's feasible to do that, the payload's structure itself is complex as it's part JSON. Hence, it's a risk to use curl to demo the command part in front of a live audience. I tend to avoid unnecessary risks, so I thought about a few alternatives.

My first idea was to prepare the command in advance, to copy-paste during the demo. I wanted to have a couple of different operations, so I'd have to:

  • Either prepare a single command, paste it and change it before running it
  • Or prepare all the different commands

To me, both were too awkward.

Another option was to create another @SpringBootApplication annotated class in the same project:

@SpringBootApplication
public class GeneratorApplication {

    @Bean
    public CommandLineRunner run() {
        var template = new RestTemplate();
        return args -> {
            var operation = generateRandomOperation();            // 1
            LongStream.range(0, Long.parseLong(args[0]))          // 2
                .forEach(
                    operation -> template.postForObject(          // 3
                        "http://localhost:8080/operation",
                        operation,
                        Object.class));
        };
    }

    public static void main(String[] args) {
        new SpringApplicationBuilder(GeneratorApplication.class)
                .run(args);
    }
}
  1. Generate a random Operation, somehow
  2. Get the number of calls from the argument
  3. Call the URL of the main web application

When I launched this application after the other web one, it failed. There are two reasons for that:

  1. Both applications share the same Maven POM. As the spring-boot-starter-web is on the classpath, the generator application tries to launch Tomcat. It fails because the first application did bind the default port.
  2. Spring Boot relies on component scanning by default. Hence, the web application scans the generator application and its declared beans and creates them. It's possible to redefine some of the beans this way. However, the webapp also creates the CommandLineRunner bean above. It thus "posts to itself" while its server is not ready yet.

The most straightforward solution is to move each application's classes in their own dedicated Maven module. You need to create a POM in each module with only the necessary dependencies. Plus, I need to use a couple of classes in the runner from the webapp. While I could duplicate them in the other module, it's extra work and complexity.

To prevent classpath scanning, we move each application class into its package. Note that it doesn't work when packages have a parent-child relationship: they must be siblings.

To create beans of specific classes, we need to rely on particular annotations depending on their nature:

  • For JPA entities, @EntityScan, pointing to the package to scan
  • For JPA repositories, @EnableJpaRepositories, pointing to the package to scan
  • For other classes, @Import points to the classes to generate bean from

The final step is to prevent the generator application from launching the webserver. You can configure it when launching the application.

@SpringBootApplication
@EnableJpaRepositories("org.hazelcast.cqrs")                      // 1
@EntityScan("org.hazelcast.cqrs")                                 // 2
public class GeneratorApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(GeneratorApplication.class)
                .web(WebApplicationType.NONE)                     // 3
                .run(args);
    }

    // Command-line runner
}
  1. Scan for JPA repositories
  2. Scan for JPA entities
  3. Prevent the web server from launching

It works as expected, and we can finally reap the benefits of the setup.

For real-world applications, you'll probably create a self-executing JAR. You'll need to develop a way to configure the main class during the build, one for each application. I think it's better not to do it and keep this as a demo hack.

To go further:

Originally published at A Java Geek on December 5th, 2021