NoSuchBeanDefinitionException / UnsatisfiedDependencyException 测试时 Spring 使用 Java 模块启动应用程序

NoSuchBeanDefinitionException / UnsatisfiedDependencyException when testing Spring Boot app with Java modules

在测试配置了 Java 9 个模块的多模式 Spring 引导应用程序时,如何减轻 NoSuchBeanDefinitionException 和相关 UnsatisfiedDependencyException 的发生?

module-info.java 个文件添加到我的多模块项目后,我的 Spring 引导应用程序测试开始失败:

测试失败

contextLoads  Time elapsed: 0 s  <<< ERROR!
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: 
   Error creating bean with name 'second': 
   Unsatisfied dependency expressed through constructor parameter 0; 
   nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: 
   No qualifying bean of type 'com.example.first.First' available: 
   expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
   No qualifying bean of type 'com.example.first.First' available: 
   expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

文件夹结构

parent
  |
  + pom.xml
  |
  +-- first
  |     + pom.xml
  |     + src/main/java/module-info.java
  |     + src/main/java/com.example.first/First.java
  |
  +-- second
  |     + pom.xml
  |     + src/main/java/module-info.java
  |     + src/main/java/com.example.second/ApplicationConfig.java
  |     + src/main/java/com.example.second/Second.java
  |     + src/test/java/com.example.second/SecondTest.java

父模块

仅包含父 pom 文件的 Maven 父模块。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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>2.6.2</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>parent</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>first</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>second</artifactId>
                <version>${project.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <modules>
        <module>first</module>
        <module>second</module>
    </modules>
</project>

第一个模块

一个简单的 Java 模块,它只包含一个 Java class 并且确实有任何 Spring 依赖项。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>parent</artifactId>
        <version>0.1.0-SNAPSHOT</version>
    </parent>

    <artifactId>first</artifactId>
</project>

First.java

package com.example.first;

public class First {
}

第一个模块-info.java

module com.example.first {
    exports com.example.first;

    opens com.example.first;
}

第二个模块

除了所需的 Spring 依赖项之外,还依赖于第一个模块的应用程序模块。它负责配置 Spring bean、运行 应用程序等

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>parent</artifactId>
        <version>0.1.0-SNAPSHOT</version>
    </parent>

    <artifactId>second</artifactId>

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

    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>first</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Second.java

package com.example.second;

import com.example.first.First;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Second {

    private final First first;

    public Second(First first) {
        this.first = first;
    }

    public static void main(String[] args) {
        SpringApplication.run(ApplicationConfig.class, args);
        System.out.println("started");
    }
}

ApplicationConfig.java

package com.example.second;

import com.example.first.First;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfig {

    @Bean
    First first() {
        return new First();
    }
}

SecondTest.java

package com.example.second;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SecondTest {

    @Test
    void contextLoads() {
    }
}

观察

根据@xerx593 评论中的反馈,我实施了一个解决方法,我将 @SpringBootApplication class 提取到单独的 Application class 并相应地更新了测试:

  |
  +-- second
  |     + pom.xml
  |     + src/main/java/module-info.java
  |     + src/main/java/com.example.second/Application.java
  |     + src/main/java/com.example.second/ApplicationConfig.java
  |     + src/main/java/com.example.second/Second.java
  |     + src/test/java/com.example.second/ApplicationTest.java

Application.java

package com.example.second;

import java.util.Arrays;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;


@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

ApplicationConfig.java

package com.example.second;

import com.example.first.First;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfig {

    @Bean
    First first() {
        return new First();
    }
}

Second.java

package com.example.second;

import com.example.first.First;
import org.springframework.stereotype.Service;

@Service
public class Second {

    private final First first;

    public Second(First first) {
        this.first = first;
    }
}

ApplicationTest.java

package com.example.second;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes = Application.class)
class ApplicationTest {

    @Test
    void contextLoads() {
    }
}