MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Spring Boot中的自定义Starter开发

2024-06-273.6k 阅读

Spring Boot中的自定义Starter开发

Spring Boot 以其快速开发、自动配置等特性,极大地简化了基于 Spring 框架的应用开发。其中,Starter 作为 Spring Boot 的核心概念之一,允许开发者通过引入特定的 Starter 依赖,便捷地集成各种功能。当现有的官方 Starter 无法满足项目需求时,自定义 Starter 就成为了解决问题的有效手段。接下来,我们将深入探讨如何在 Spring Boot 中开发自定义 Starter。

1. 自定义 Starter 的结构

自定义 Starter 通常包含两个主要部分:自动配置模块(Auto - Configuration Module)和 Starter 模块。

  • 自动配置模块:该模块负责编写自动配置类,这些类会根据项目的依赖和配置属性,自动配置 Spring 应用上下文。它通常包含对各种组件的初始化、配置属性的绑定等逻辑。
  • Starter 模块:这个模块主要是一个依赖聚合器,它将自动配置模块以及相关的依赖整合在一起,方便开发者在项目中引入。通过引入这个 Starter 模块,开发者就能够快速地启用自定义的功能。

2. 创建自动配置模块

我们以开发一个简单的 "Hello World" 自定义 Starter 为例。首先创建一个 Maven 项目作为自动配置模块,假设项目名为 hello - world - autoconfigure

pom.xml 文件中添加必要的依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring - boot - autoconfigure</artifactId>
        <version>2.6.3</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring - boot - configuration - processor</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

这里,spring - boot - autoconfigure 是 Spring Boot 自动配置的核心依赖,spring - boot - configuration - processor 用于生成配置元数据,方便 IDE 提供配置提示。

接下来,定义配置属性类。创建 HelloWorldProperties 类:

package com.example.hello;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "hello.world")
public class HelloWorldProperties {
    private String message = "Default Hello World";

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

@ConfigurationProperties 注解将配置文件中以 hello.world 为前缀的属性绑定到该类的字段上。

然后,编写自动配置类 HelloWorldAutoConfiguration

package com.example.hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnClass(HelloWorldService.class)
@EnableConfigurationProperties(HelloWorldProperties.class)
public class HelloWorldAutoConfiguration {

    @Autowired
    private HelloWorldProperties helloWorldProperties;

    @Bean
    @ConditionalOnProperty(name = "hello.world.enabled", havingValue = "true")
    public HelloWorldService helloWorldService() {
        return new HelloWorldService(helloWorldProperties.getMessage());
    }
}

@Configuration 表明这是一个配置类。@ConditionalOnClass 表示只有当 HelloWorldService 类在类路径中存在时,该配置类才会生效。@EnableConfigurationProperties 启用 HelloWorldProperties 的绑定。@ConditionalOnProperty 表示只有当配置属性 hello.world.enabledtrue 时,helloWorldService 这个 Bean 才会被创建。

再创建 HelloWorldService 类:

package com.example.hello;

public class HelloWorldService {
    private String message;

    public HelloWorldService(String message) {
        this.message = message;
    }

    public String sayHello() {
        return message;
    }
}

最后,在 resources/META - INF/spring.factories 文件中注册自动配置类:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.hello.HelloWorldAutoConfiguration

这一步是 Spring Boot 自动配置机制的关键,通过在 spring.factories 文件中指定自动配置类,Spring Boot 在启动时会自动加载这些配置类。

3. 创建 Starter 模块

创建另一个 Maven 项目作为 Starter 模块,假设项目名为 hello - world - starter。在 pom.xml 文件中添加依赖:

<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>hello - world - autoconfigure</artifactId>
        <version>1.0.0</version>
    </dependency>
</dependencies>

这里将自动配置模块作为依赖引入。Starter 模块本身通常不需要编写额外的 Java 代码,它主要的作用就是聚合依赖,方便其他项目引入。

4. 在 Spring Boot 项目中使用自定义 Starter

创建一个新的 Spring Boot 项目,在 pom.xml 文件中添加自定义 Starter 的依赖:

<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>hello - world - starter</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring - boot - starter - web</artifactId>
    </dependency>
</dependencies>

这里同时引入了 spring - boot - starter - web 依赖,以便我们可以创建一个简单的 Web 应用来测试自定义 Starter。

application.properties 文件中添加配置:

hello.world.enabled=true
hello.world.message=Custom Hello from Starter

这里启用了自定义 Starter 的功能,并设置了自定义的消息。

创建一个控制器类 HelloWorldController

package com.example.demo;

import com.example.hello.HelloWorldService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloWorldController {

    @Autowired
    private HelloWorldService helloWorldService;

    @GetMapping("/hello")
    public String hello() {
        return helloWorldService.sayHello();
    }
}

在这个控制器中,注入 HelloWorldService 并通过 /hello 接口返回问候消息。

启动 Spring Boot 应用,访问 http://localhost:8080/hello,就可以看到返回的自定义问候消息 "Custom Hello from Starter"。

5. 深入理解自动配置原理

Spring Boot 的自动配置是基于条件化配置(Conditional Configuration)的机制。@Conditional 及其派生注解(如 @ConditionalOnClass@ConditionalOnProperty 等)是实现这一机制的核心。

当 Spring Boot 应用启动时,它会扫描 META - INF/spring.factories 文件中定义的所有自动配置类。对于每个自动配置类,Spring Boot 会根据类路径下是否存在某些类、配置属性的值等条件来决定是否应用该配置类。

例如,在我们的 HelloWorldAutoConfiguration 中,@ConditionalOnClass(HelloWorldService.class) 确保只有当 HelloWorldService 类在类路径中时,这个配置类才会被考虑。@ConditionalOnProperty(name = "hello.world.enabled", havingValue = "true") 进一步限制只有当配置属性 hello.world.enabledtrue 时,helloWorldService 这个 Bean 才会被创建。

这种条件化配置机制使得 Spring Boot 能够根据项目的实际情况,灵活地加载和配置所需的组件,避免了不必要的配置和资源浪费。

6. 配置属性的优先级

在 Spring Boot 中,配置属性有多个来源,并且它们的优先级是不同的。从高到低的优先级顺序如下:

  • 命令行参数:通过命令行传递的配置属性优先级最高。例如,java -jar myapp.jar --hello.world.message=FromCommandLine,这个配置会覆盖其他任何来源的 hello.world.message 属性值。
  • 来自 SPRING_APPLICATION_JSON 的属性:可以通过环境变量 SPRING_APPLICATION_JSON 来设置 JSON 格式的配置属性。例如,export SPRING_APPLICATION_JSON='{"hello.world.message":"FromSpringApplicationJson"}'
  • 操作系统环境变量:系统的环境变量也可以作为配置属性的来源。例如,export HELLO_WORLD_MESSAGE=FromEnvVar,在 Spring Boot 中可以通过 hello.world.message 来获取这个值(注意,环境变量名通常使用大写字母和下划线分隔,而配置属性名使用小写字母和点分隔)。
  • JVM 系统属性:通过 -D 参数设置的 JVM 系统属性也可以作为配置属性。例如,java -Dhello.world.message=FromJvmSystemProperty -jar myapp.jar
  • 应用配置文件:包括 application.propertiesapplication.yml 等。这些配置文件中的属性优先级相对较低,但却是最常用的配置方式。
  • 默认属性:在配置属性类中设置的默认值是优先级最低的。例如,在 HelloWorldProperties 类中设置的 private String message = "Default Hello World"

理解配置属性的优先级对于调试和正确配置应用非常重要,特别是在复杂的生产环境中,可能会有多种配置来源同时存在。

7. 自定义 Starter 的最佳实践

  • 保持 Starter 的单一职责:每个 Starter 应该专注于实现一个特定的功能,这样可以提高 Starter 的可复用性和维护性。例如,如果要开发一个数据库相关的 Starter,应该只关注数据库连接、事务管理等与数据库直接相关的功能,而不要混入其他不相关的业务逻辑。
  • 合理使用条件化配置:在自动配置类中,要谨慎使用 @Conditional 及其派生注解。确保配置类只有在真正需要的时候才会生效,避免不必要的配置加载。例如,如果某个功能依赖于特定的数据库驱动,应该使用 @ConditionalOnClass 来检查该驱动是否在类路径中。
  • 提供清晰的文档:为自定义 Starter 编写详细的文档,包括如何引入依赖、配置属性的说明、使用示例等。这将帮助其他开发者快速上手并正确使用 Starter。文档可以采用 Markdown 格式,放在项目的 README.md 文件中,也可以发布在专门的文档平台上。
  • 版本管理:对 Starter 的版本进行严格管理,遵循语义化版本控制(SemVer)规范。当 Starter 的功能发生不兼容的变化时,要及时更新主版本号;当添加新功能且保持向后兼容时,更新次版本号;当修复 bug 时,更新补丁版本号。这样可以让使用者清楚了解 Starter 的变化情况,便于进行版本升级。

8. 处理依赖传递

在自定义 Starter 中,要注意处理好依赖传递。当引入自动配置模块和其他相关依赖时,要确保这些依赖不会带来不必要的冲突。

例如,如果自动配置模块依赖于某个特定版本的库,而使用自定义 Starter 的项目也依赖于相同库的不同版本,可能会导致类加载冲突等问题。为了避免这种情况,可以考虑以下方法:

  • 使用 optional 依赖:对于一些非必需的依赖,可以将其声明为 optional。这样在引入 Starter 时,使用者可以根据自己的需求决定是否引入这些依赖。例如,如果某个功能依赖于特定的数据库驱动,而该驱动对于某些使用者可能是不需要的,可以将数据库驱动依赖声明为 optional
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql - connector - java</artifactId>
    <version>8.0.28</version>
    <optional>true</optional>
</dependency>
  • 管理依赖版本:在 Starter 的 pom.xml 文件中,尽量通过 dependencyManagement 来管理依赖的版本,确保引入的依赖版本与 Starter 的兼容性。同时,也要考虑与常见 Spring Boot 版本的兼容性。例如:
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring - boot - dependencies</artifactId>
            <version>2.6.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

通过这种方式,可以继承 Spring Boot 官方推荐的依赖版本,减少版本冲突的可能性。

9. 测试自定义 Starter

对自定义 Starter 进行充分的测试是确保其质量和稳定性的关键。可以使用 JUnit、Mockito 等测试框架来编写单元测试和集成测试。

  • 单元测试:针对自动配置模块中的配置属性类和自动配置类进行单元测试。例如,测试 HelloWorldProperties 类的属性绑定是否正确:
package com.example.hello;

import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import static org.assertj.core.api.Assertions.assertThat;

public class HelloWorldPropertiesTest {

    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();

    @Test
    public void testHelloWorldProperties() {
        contextRunner.withPropertyValues("hello.world.message=Test Message")
              .run(context -> {
                    HelloWorldProperties properties = context.getBean(HelloWorldProperties.class);
                    assertThat(properties.getMessage()).isEqualTo("Test Message");
                });
    }
}

这里使用 ApplicationContextRunner 来模拟 Spring 应用上下文的启动,并验证配置属性是否正确绑定。

  • 集成测试:编写集成测试来验证整个 Starter 在 Spring Boot 应用中的功能是否正常。例如,创建一个简单的 Spring Boot 测试项目,引入自定义 Starter,编写测试用例来验证 HelloWorldService 是否正确注入并返回预期的结果:
package com.example.demo;

import com.example.hello.HelloWorldService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class HelloWorldStarterIntegrationTest {

    @Autowired
    private HelloWorldService helloWorldService;

    @Test
    public void testHelloWorldService() {
        String message = helloWorldService.sayHello();
        assertThat(message).isEqualTo("Custom Hello from Starter");
    }
}

通过这种方式,可以全面地测试自定义 Starter 在实际应用场景中的表现。

10. 与其他框架集成

在实际项目中,自定义 Starter 可能需要与其他框架进行集成。例如,与 Spring Cloud 集成,为微服务项目提供特定的功能。

假设我们要开发一个自定义 Starter,为 Spring Cloud 微服务提供统一的日志格式配置。首先,在自动配置模块中添加 Spring Cloud 相关的依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring - cloud - commons</artifactId>
        <version>3.1.0</version>
    </dependency>
    <!-- 其他相关依赖 -->
</dependencies>

然后,编写自动配置类来配置日志格式。例如:

package com.example.log;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnClass(RollingFileAppender.class)
public class CustomLogAutoConfiguration {

    @Value("${custom.log.path:./logs/custom.log}")
    private String logPath;

    @Bean
    @ConditionalOnProperty(name = "custom.log.enabled", havingValue = "true")
    public RollingFileAppender rollingFileAppender() {
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        RollingFileAppender appender = new RollingFileAppender();
        appender.setContext(context);
        appender.setFile(logPath);

        PatternLayoutEncoder encoder = new PatternLayoutEncoder();
        encoder.setContext(context);
        encoder.setPattern("%d{yyyy - MM - dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n");
        encoder.start();

        TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy();
        rollingPolicy.setContext(context);
        rollingPolicy.setFileNamePattern(logPath + ".%d{yyyy - MM - dd}");
        rollingPolicy.setMaxHistory(30);
        rollingPolicy.start();

        appender.setEncoder(encoder);
        appender.setRollingPolicy(rollingPolicy);
        appender.start();
        return appender;
    }
}

在这个例子中,通过 @ConditionalOnClass 确保只有当 RollingFileAppender 类在类路径中时,该配置类才会生效。@Value 注解用于获取配置文件中的日志路径属性。

resources/META - INF/spring.factories 文件中注册自动配置类:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.log.CustomLogAutoConfiguration

然后,在 Starter 模块中聚合相关依赖,在使用的 Spring Cloud 项目中引入自定义 Starter,并在 application.properties 文件中配置:

custom.log.enabled=true
custom.log.path=/var/log/myapp/custom.log

这样就可以在 Spring Cloud 微服务项目中使用自定义的日志格式配置了。

通过以上步骤,我们详细介绍了 Spring Boot 中自定义 Starter 的开发过程,包括结构设计、创建模块、使用、原理理解、最佳实践、依赖处理、测试以及与其他框架集成等方面。掌握这些知识,开发者可以根据项目需求灵活定制 Spring Boot 的功能,提高开发效率和代码的可维护性。