D3nsk
D3nsk

Reputation: 131

Spring boot test multi module maven application

I have a multi-module maven application which uses Spring boot:

- spring boot parent
    - myproject parent (both parent and module pom)
        - module1
        - module2
        - module-it (integration tests)

In my module-it, I add the other modules as dependencies.

When I build my project with maven, I get "Build Success":

mvn clean install

So far so good.
Yet I would like each of my modules to be an executable jar at the end of the build. With the above settings, the manifest is not defined and the jar is not executable. To fix this issue, I've added the following in my module1 and module2 pom files:

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

With this setting, my jar file is executable but I cannot build anymore. Classes that I use in my module-it are not found.

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile (default-testCompile) on project module-it: Compilation failure: Compilation failure:
[ERROR] /home/user/<path-to-project>/testing/module-it/src/test/java/com/mycompany/testing/greeting/GreetingControllerIT.java:[20,17] cannot find symbol
[ERROR] symbol:   class GreetingController
[ERROR] location: class com.mycompany.testing.greeting.GreetingControllerIT
[ERROR] /home/user/<path-to-project>/testing/module-it/src/test/java/com/mycompany/testing/hello/HelloControllerIT.java:[20,17] cannot find symbol
[ERROR] symbol:   class HelloController
[ERROR] location: class com.mycompany.testing.hello.HelloControllerIT
[ERROR] /home/user/<path-to-project>/testing/module-it/src/test/java/com/mycompany/testing/greeting/GreetingControllerIT.java:[16,27] cannot find symbol
[ERROR] symbol: class GreetingController
[ERROR] /home/user/<path-to-project>/testing/module-it/src/test/java/com/mycompany/testing/hello/HelloControllerIT.java:[16,27] cannot find symbol
[ERROR] symbol: class HelloController

Can you please help me understand why spring-boot-maven-plugin makes my build fail and how I can solve the issue?

Thanks in advance for your help.

Upvotes: 7

Views: 5060

Answers (4)

Anthony Raymond
Anthony Raymond

Reputation: 7872

Late answer to an old question but i've just been working on this for the past hours. Even though the previous answers were helpfull they don't explain why this problem is happening.

For others that may comes accross this issue here is a detailed explnation why it's not working

In a Nutshell

Usualy Maven package your application as a regular .jar with all compiled class being in a well known location in the .jar file. So it's pretty straighforward for a compiler to import the .jar as a library and to load the available .class.

But the spring-boot-maven-plugin is actually modifying the .jar structure to leverage spring-boot logic when you start the .jar application. In short, the .class are not available to be imported as a "library" from the resulting .jar, because the spring class have took the well known location for itself.

Detailed explanation

Let's explore the problem with an example

Project structure

Let's imagine a project with multiple maven modules like so

my-app/        -- The parent project
├─ pom.xml 
├─ application/
│  ├─ pom.xml
├─ integration-tests/
│  ├─ pom.xml

Given the following pom.xml files:

my-app/pom.xml:

<project [...]>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.0</version>
        <relativePath/>
    </parent>

    <groupId>com.me</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>application</module>
        <module>integration-tests</module>
    </modules>
</project>

my-app/application/pom.xml:

<project [...]>
    <parent>
        <groupId>com.me</groupId>
        <artifactId>my-app</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>application</artifactId>
    <packaging>jar</packaging>

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

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

</project>

my-app/integration-tests/pom.xml:

<project [...]>
    <parent>
        <groupId>com.me</groupId>
        <artifactId>my-app</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>integration-tests</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>com.me</groupId>
            <artifactId>application</artifactId>
        </dependency>
    </dependencies>

</project>

Why is it not building

Let's try to package our app

/my-app$ mvn package

Of course it will miserably fail with an error cannot find symbol, but why is it so?

Let's take a look at our architecture after the failed build:

my-app/        -- The parent project
├─ pom.xml 
├─ application/
│  ├─ pom.xml
│  ├─ target/
│  │  ├─ application-1.0.0-SNAPSHOT.jar
│  │  ├─ application-1.0.0-SNAPSHOT.jar.original
├─ integration-tests/
│  ├─ pom.xml
│  ├─ target/

The spring-boot-maven-plugin has done several things to the application module output:

  • renamed the compiled application-1.0.0-SNAPSHOT.jar to application-1.0.0-SNAPSHOT.jar.original
  • created it's own .jar with the name of application-1.0.0-SNAPSHOT.jar

Let's explore the structure of the application-1.0.0-SNAPSHOT.jar:

BOOT-INF/
├─ classes/
│  ├─ com/
│  │  ├─ me/    -- The compiled .class of your project reside here
META-INF/
org/
├─ springframework/
│  ├─ boot/     -- contains the spring boot loader classes

As you can see the .class files at the root of your .jar are the spring boot loader classes, not our own .class that are relegated to the BOOT-INF/classes/ folder. This is not conventional, and when the .jar is imported as a dependency it won't search here for class to import.

Because of that, when maven try to package the integration-tests module, it fails because the class present in the application-1.0.0-SNAPSHOT.jar are actually a bunch of spring class instead of the one you are trying to import from application module.

If you were to look at the structure of the application-1.0.0-SNAPSHOT.jar.origial it would be something like so:

META-INF/
com/
├─ me/          -- The compiled .class of your project reside here

Solution

Getting rid of spring-boot-maven-plugin is not an acceptable solution; Of course your project will be buildable, but the resulting .jar won't be a spring boot standalone running .jar.

Instead you can instruct the spring-boot-maven-plugin to not replace the original jar and to build to spring boot jar with another name.

To do so you'll need to configure the spring-boot-maven-plugin in the application module:

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <configuration>
                            <classifier>exec</classifier>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

Now when you build your project you'll have something like that:

my-app/        -- The parent project
├─ pom.xml 
├─ application/
│  ├─ pom.xml
│  ├─ target/
│  │  ├─ application-1.0.0-SNAPSHOT.jar -- the original untouched .jar
│  │  ├─ application-1.0.0-SNAPSHOT-exec.jar -- the spring boot executable .jar
├─ integration-tests/
│  ├─ pom.xml
│  ├─ target/
│  │  ├─ integration-tests-1.0.0-SNAPSHOT.jar

Upvotes: 2

D3nsk
D3nsk

Reputation: 131

To solve this issue, we can add a classifier as described in the documentation custom repackage classifier

The plugin then becomes:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
            <configuration>
                <classifier>exec</classifier>
            </configuration>
        </execution>
    </executions>
</plugin>

Upvotes: 4

Nikolai
Nikolai

Reputation: 1

At least for the spring-boot-starter-parent:2.6.0 pom the configuration for the spring-boot-maven-plugin contains an <id>repackage</id> for the execution of the repackage goal

So I had to add the line <id>repackage</id> too.

Full configuration:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
        <execution>
            <id>repackage</id>
            <goals>
                <goal>repackage</goal>
            </goals>
            <configuration>
                <classifier>exec</classifier>
            </configuration>
        </execution>
    </executions>
</plugin>

Upvotes: 0

Evgeniy Kolmogorov
Evgeniy Kolmogorov

Reputation: 21

Also you could set repackage goal parameter attach to false:

        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                    <configuration>
                        <attach>false</attach>
                    </configuration>
                </execution>
            </executions>
        </plugin>

Upvotes: 2

Related Questions