Maven - managing spring, springboot, starter parent versions in a multi module project

Refereces

  1. https://docs.spring.io/spring-boot/docs/1.2.3.RELEASE/reference/html/using-boot-build-systems.html

spring-boot-starter-parent

For small springboot projects, spring-boot-starter-parent is added as a parent in the pom.xml file.

e.g.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.5</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.example</groupId>
  <artifactId>demo</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>demo</name>
  <description>Demo project for Spring Boot</description>
  <properties>
    <java.version>17</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

But this does not work well in an enterprise application that is using multi-module structure. In big enterprise applications, the smaller modules will have to use the parent project for inheritance. So, it is not possible to specify <artifactId>spring-boot-starter-parent</artifactId> as the parent. But, it will be nice to specify the versions (of spring dependencies) in one place instead of specifying the version numbers in every sub-module.

How to do this?

spring-boot-dependencies in parent pom

If we don’t make use of the parent POM, we can still benefit from dependency management by adding the spring-boot-dependencies artifact with scope=import:

<dependencyManagement>
     <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.1.5</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

In the sub-modules, we can start simply start adding Spring dependencies and making use of Spring Boot features:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

What happens if we don’t manage spring-boot-dependencies in parent pom?

If we do not manage the version numbers of spring dependencies in the parent pom, and instead, specify a version number in each of the sub-module poms, it will lead to a mess. All the sub-modules need to be on the same version of spring dependencies. If not, we will run into errors.

Another reason is, certain versions of Spring work with only certain versions of jdk. e.g. Java 8 requires Spring framework 4. It is easy to manage these compatibility issues if all of the version numbers are maintained in a single place.

BeanDefinitionStoreException Failed to read candidate component class

ERROR DispatcherServlet - Context initialization failed
org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [C:\workspace-sts\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\wtpwebapps\Webtest1\WEB-INF\classes\com\springweb\controller\SetHomePageController.class]; nested exception is java.lang.IncompatibleClassChangeError: class org.springframework.core.type.classreading.ClassMetadataReadingVisitor has interface org.springframework.asm.ClassVisitor as super class

Compatibility between the versions of spring-boot, spring framework and spring cloud

In the parent pom, enforce the compatibility

<dependencyManagement>
     <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.7.18</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <!-- How would we get this version?
        Go to https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies/2.7.18 and search for org.springframework.spring-framework-bom version number -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-framework-bom</artifactId>
            <version>5.3.34</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <!-- How would we get this version?
             Go to https://spring.io/projects/spring-cloud and search for Spring Cloud release train Spring Boot compatibility.
             We should find something like this: 2021.0.x aka Jubilee - 2.6.x, 2.7.x (Starting with 2021.0.3) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.9</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

    </dependencies>
</dependencyManagement>

Add the spring-boot-maven-plugin explicitly

Without the parent POM, we no longer benefit from plugin management. This means we need to add the spring-boot-maven-plugin explicitly:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Overriding Dependency Versions

If we want to use a different version for a certain dependency than the one managed by Boot, we need to declare it in the dependencyManagement section in the parent pom.xml, before spring-boot-dependencies is declared:

Remember: just declaring the version for the dependency outside the dependencyManagement tag will no longer work.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>3.1.5</version>
        </dependency>
    </dependencies>
    // ...
</dependencyManagement>

How to exclude specific dependencies from spring-boot-starter-parent

https://stackoverflow.com/questions/51239227/how-to-exclude-specific-dependencies-from-spring-boot-starter-parent

To summarize the questions, I want to use spring-boot-starter-parent and spring-boot-starter-data-jpa but not the managed transitive dependencies from spring-boot-dependencies.

How can I do it?

Instead of messing around with <excludes> and then try to figure out what you need to include again (after figuring out what you excluded). Just override the version as explained here in the Spring Boot Reference Guide.

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-dependency-versions

Assuming you are using the spring-boot-starter-parent as the parent you can just add a <selenium.version> to your <properties> section to specify which version you want.

<properties>
  <selenium.version>2.53.0</selenium.version>
</properties>

This will make Spring Boot use the version you want.

TODO

How to manage spring’s irregularity in version dependencies?

https://stackoverflow.com/a/5571735/1504291