Spring Boot 的 Starter 机制是其生态系统中强大的一部分,允许开发者创建可重用的模块,简化依赖管理和配置。本文将通过一个实际案例,详细讲解如何创建一个 Spring Boot Starter,用于提供字符串操作工具。我们将实现一个 spring-boot-string-utils-starter,并探讨一个常见问题:为什么需要 Starter,而不仅仅是一个简单的 JAR 包?通过对比两者的优劣,我们将揭示 Starter 的真正价值。


1. 项目背景与目标

我们将创建一个名为 spring-boot-string-utils-starter 的 Spring Boot Starter,提供以下字符串操作功能:

  • 反转字符串 (reverse)
  • 转换为大写 (toUpperCase)
  • 转换为小写 (toLowerCase)
  • 拼接字符串 (concatenate)

这个 Starter 将通过 Spring Boot 的自动配置机制,让用户在引入依赖后无需额外配置即可使用这些功能。同时,我们将回答以下问题:

  1. spring.factories 可以指定多个文件吗?
  2. 每个文件必须是 @Configuration 吗?
  3. 为什么需要 Starter,而不仅仅是一个简单的 JAR 包?

2. 项目结构

项目的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring-boot-string-utils-starter/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── business/
│ │ │ └── springbootstringutilsstarter/
│ │ │ ├── StringUtilsService.java
│ │ │ └── StringUtilsAutoConfiguration.java
│ │ └── resources/
│ │ └── META-INF/
│ │ └── spring.factories
│ └── test/
└── target/
  • pom.xml:Maven 配置文件,定义依赖和构建规则。
  • StringUtilsService.java:核心服务类,包含字符串操作方法。
  • StringUtilsAutoConfiguration.java:自动配置类,负责将服务类注入 Spring 容器。
  • spring.factories:Spring Boot 自动配置的入口文件,位于 src/main/resources/META-INF/

3. 实现步骤

3.1 创建 Maven 项目

首先,我们需要一个 Maven 项目,pom.xml 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?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>
<groupId>com.business</groupId>
<artifactId>spring-boot-string-utils-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-string-utils-starter</name>
<description>spring-boot-string-utils-starter</description>
<packaging>jar</packaging>

<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.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>

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

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

关键点:

  • Spring Boot 版本:使用 2.6.13,确保兼容 Java 8。
  • 依赖:引入 spring-boot-starter 作为基础依赖,提供 Spring Boot 的核心功能。
  • 构建插件:配置 spring-boot-maven-plugin,但设置 skip=true,因为 Starter 不需要生成可执行 JAR(Starter 只是依赖库,不是可运行应用)。
  • 编码:确保项目使用 UTF-8 编码,防止编码问题。

3.2 实现字符串工具服务

创建 StringUtilsService.java,包含核心字符串操作方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.business.springbootstringutilsstarter;

public class StringUtilsService {

public String reverse(String input) {
if (input == null) {
return null;
}
return new StringBuilder(input).reverse().toString();
}

public String toUpperCase(String input) {
if (input == null) {
return null;
}
return input.toUpperCase();
}

public String toLowerCase(String input) {
if (input == null) {
return null;
}
return input.toLowerCase();
}

public String concatenate(String str1, String str2) {
if (str1 == null || str2 == null) {
return null;
}
return str1 + str2;
}
}

关键点:

  • 空值检查:每个方法都包含对输入参数的空值检查,增强健壮性。
  • 方法设计:提供基础的字符串操作,满足常见需求。

3.3 创建自动配置类

创建 StringUtilsAutoConfiguration.java,将 StringUtilsService 注入 Spring 容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.business.springbootstringutilsstarter;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class StringUtilsAutoConfiguration {

@Bean
public StringUtilsService stringUtilsService() {
return new StringUtilsService();
}
}

关键点:

  • 注解

    • @Configuration:标记这是一个 Spring 配置类。
    • @Bean:定义一个 Bean,Spring Boot 会自动将其注入容器。
  • 作用StringUtilsService 会被 Spring 管理,用户可以通过 @Autowired 直接使用。


3.4 配置 spring.factories

src/main/resources/META-INF/spring.factories 中,指定自动配置类:

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\
com.business.springbootstringutilsstarter.StringUtilsAutoConfiguration

关键点:

  • 路径META-INF/spring.factories 是 Spring Boot 的标准路径,用于声明自动配置类。

  • 键值对

    • org.springframework.boot.autoconfigure.EnableAutoConfiguration 是 Spring Boot 自动配置的入口。
    • 值是配置类的全限定名,支持指定多个类(用逗号分隔)。

4. 解答问题:spring.factories 的使用

在开发过程中,可能会遇到以下问题:

4.1 spring.factories 可以指定多个文件吗?

是的,spring.factories 支持指定多个类。它的格式是一个键值对,值可以用逗号分隔指定多个类名。

示例:添加另一个配置类

创建一个新配置类 AnotherConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.business.springbootstringutilsstarter;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;

@Configuration
public class AnotherConfiguration {

@Bean
public String helloBean() {
return "Hello from AnotherConfiguration!";
}
}

更新 spring.factories

1
2
3
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\
com.business.springbootstringutilsstarter.StringUtilsAutoConfiguration,\\
com.business.springbootstringutilsstarter.AnotherConfiguration

工作原理:

  • Spring Boot 在启动时会读取 spring.factories 文件。
  • 对于 EnableAutoConfiguration 键,Spring Boot 会加载所有列出的类(StringUtilsAutoConfigurationAnotherConfiguration)。
  • 结果是 StringUtilsServicehelloBean 都会注入到 Spring 容器中。

价值:

  • 支持模块化设计,允许 Starter 包含多个配置类,满足复杂需求。

4.2 每个文件必须是 @Configuration 吗?

不一定,是否需要 @Configuration 取决于 spring.factories 中使用的键。

情况 1:使用 EnableAutoConfiguration

  • 如果键是 org.springframework.boot.autoconfigure.EnableAutoConfiguration,列出的类通常应该使用 @Configuration
  • 原因是 Spring Boot 期望这些类定义 Bean 或执行配置逻辑。如果没有 @Configuration,Spring 不会将其视为配置类,方法也不会被识别为 @Bean 定义。

情况 2:其他键

  • spring.factories 支持其他键,例如:

    • org.springframework.context.ApplicationListener:指定事件监听器。
    • org.springframework.beans.factory.config.BeanPostProcessor:指定 Bean 后置处理器。
  • 对于这些键,类不需要是 @Configuration,只需要实现对应的接口即可。

示例:添加一个监听器

创建一个监听器 MyListener.java

1
2
3
4
5
6
7
8
9
10
11
package com.business.springbootstringutilsstarter;

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;

public class MyListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("Context refreshed!");
}
}

更新 spring.factories

1
2
3
4
5
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\
com.business.springbootstringutilsstarter.StringUtilsAutoConfiguration

org.springframework.context.ApplicationListener=\\
com.business.springbootstringutilsstarter.MyListener

分析:

  • MyListener 不需要 @Configuration,因为它是一个事件监听器,只需实现 ApplicationListener 接口。
  • Spring Boot 会自动注册 MyListener 为事件监听器,在容器刷新时触发 onApplicationEvent 方法。

价值:

  • 提供灵活性,允许 Starter 注册不同类型的组件(如配置类、监听器、后置处理器等)。

5. 使用 Starter

5.1 构建和发布

运行以下命令构建项目:

1
mvn clean install
  • 这会生成 spring-boot-string-utils-starter-0.0.1-SNAPSHOT.jar,可以发布到 Maven 仓库或本地使用。

  • 构建后的 JAR 包结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    spring-boot-string-utils-starter-0.0.1-SNAPSHOT.jar
    ├── META-INF/
    │ └── spring.factories
    ├── com/
    │ └── business/
    │ └── springbootstringutilsstarter/
    │ ├── StringUtilsService.class
    │ └── StringUtilsAutoConfiguration.class

5.2 在其他项目中使用

在另一个 Spring Boot 项目中引入依赖:

1
2
3
4
5
<dependency>
<groupId>com.business</groupId>
<artifactId>spring-boot-string-utils-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.demo;

import com.business.springbootstringutilsstarter.StringUtilsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyService {

private final StringUtilsService stringUtilsService;

@Autowired
public MyService(StringUtilsService stringUtilsService) {
this.stringUtilsService = stringUtilsService;
}

public void processString(String input) {
System.out.println("Reversed: " + stringUtilsService.reverse(input));
System.out.println("Upper: " + stringUtilsService.toUpperCase(input));
System.out.println("Lower: " + stringUtilsService.toLowerCase(input));
System.out.println("Concatenated: " + stringUtilsService.concatenate(input, " World"));
}
}

测试代码:

创建一个简单的 Spring Boot 应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.demo;

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

@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
MyService myService = context.getBean(MyService.class);
myService.processString("Hello");
}
}

输出:

1
2
3
4
Reversed: olleH
Upper: HELLO
Lower: hello
Concatenated: Hello World

关键点:

  • 自动注入StringUtilsService 由 Spring 容器管理,用户通过 @Autowired 直接使用。
  • 零配置:用户无需手动定义 Bean,Starter 自动完成配置。

6. 探讨问题:为什么需要 Starter,而不仅仅是一个简单的 JAR 包?

一个常见的疑问是:既然可以创建一个简单的 JAR 包(包含 StringUtilsService),直接引入并使用,为什么还需要 Spring Boot Starter?以下通过对比两者的实现和优劣,详细分析 Starter 的价值。

6.1 简单 JAR 包的实现

假设我们创建一个简单的 JAR 包,包含 StringUtilsService

1
2
3
4
5
6
7
8
9
10
11
package com.example.utils;

public class StringUtilsService {
public String reverse(String input) {
return input == null ? null : new StringBuilder(input).reverse().toString();
}

public String toUpperCase(String input) {
return input == null ? null : input.toUpperCase();
}
}
  • 构建:将这个类打包成 JAR(string-utils.jar)。

  • 使用:在另一个项目中引入依赖,然后手动实例化:

    1
    2
    StringUtilsService utils = new StringUtilsService();
    String result = utils.reverse("hello");
  • 替代方案:如果想在 Spring 环境中使用,可以手动定义为 Bean:

    1
    2
    3
    4
    5
    6
    7
    @Configuration
    public class AppConfig {
    @Bean
    public StringUtilsService stringUtilsService() {
    return new StringUtilsService();
    }
    }

6.2 Spring Boot Starter 的实现

我们的 spring-boot-string-utils-starter 包含:

  • StringUtilsService:工具类。
  • StringUtilsAutoConfiguration:自动配置类。
  • spring.factories:自动配置入口。

使用时,只需引入依赖,Spring Boot 自动注入 StringUtilsService

1
2
@Autowired
private StringUtilsService stringUtilsService;

6.3 对比分析:Starter 的核心优势

6.3.1 自动配置(Zero Configuration)

  • 简单 JAR 包:需要用户手动实例化(new StringUtilsService())或手动定义为 Spring Bean。
  • Spring Boot Starter:通过 spring.factories@Configuration,Spring Boot 在启动时自动将 StringUtilsService 注入容器,用户无需任何配置即可通过 @Autowired 使用。

价值

  • 减少样板代码,符合 Spring Boot 的“约定优于配置”理念。
  • 提升开发效率,用户无需关心 Bean 的创建和管理。

6.3.2 条件加载(Conditional Loading)

  • 简单 JAR 包:一旦引入,工具类总是可用,无法根据环境动态加载或禁用。
  • Spring Boot Starter:支持条件注解(如 @ConditionalOnProperty@ConditionalOnClass),可以根据配置或环境动态加载 Bean。

示例: 在 StringUtilsAutoConfiguration 中添加条件:

1
2
3
4
5
6
7
8
@Configuration
@ConditionalOnProperty(prefix = "string.utils", name = "enabled", havingValue = "true", matchIfMissing = true)
public class StringUtilsAutoConfiguration {
@Bean
public StringUtilsService stringUtilsService() {
return new StringUtilsService();
}
}

用户可以通过 application.properties 控制:

1
string.utils.enabled=false

如果 enabled=falseStringUtilsService 不会加载。

价值

  • 提供灵活性,适合复杂场景(比如只在特定环境下启用功能)。
  • 避免不必要的 Bean 加载,优化性能。

6.3.3 与 Spring 生态的深度整合

  • 简单 JAR 包:只是一个独立的工具库,无法直接利用 Spring 的功能(如依赖注入、事件监听、AOP 等)。

  • Spring Boot Starter:深度整合 Spring 生态,可以:

    • 使用依赖注入(注入其他 Bean)。
    • 发布和监听 Spring 事件。
    • 利用 Spring Boot 的配置机制(@ConfigurationProperties)。

示例: 假设 StringUtilsService 需要依赖另一个 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class StringUtilsAutoConfiguration {
private final SomeOtherService otherService;

public StringUtilsAutoConfiguration(SomeOtherService otherService) {
this.otherService = otherService;
}

@Bean
public StringUtilsService stringUtilsService() {
return new StringUtilsService(otherService);
}
}

Starter 可以轻松注入其他 Bean,而简单 JAR 包需要用户手动管理依赖。

价值

  • 充分利用 Spring 的 IoC 容器和生态系统,提升开发效率。
  • 支持复杂业务逻辑,适合企业级应用。

6.3.4 配置属性支持

  • 简单 JAR 包:无法直接支持通过 application.properties 自定义行为。
  • Spring Boot Starter:可以使用 @ConfigurationProperties 提供可配置的属性。

示例: 添加一个配置类:

1
2
3
4
5
6
7
8
9
10
11
12
@ConfigurationProperties(prefix = "string.utils")
public class StringUtilsProperties {
private String defaultPrefix = "";

public String getDefaultPrefix() {
return defaultPrefix;
}

public void setDefaultPrefix(String defaultPrefix) {
this.defaultPrefix = defaultPrefix;
}
}

StringUtilsService 中使用:

1
2
3
4
5
6
7
8
9
10
11
public class StringUtilsService {
private final String defaultPrefix;

public StringUtilsService(StringUtilsProperties properties) {
this.defaultPrefix = properties.getDefaultPrefix();
}

public String concatenateWithPrefix(String input) {
return defaultPrefix + input;
}
}

StringUtilsAutoConfiguration 中启用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableConfigurationProperties(StringUtilsProperties.class)
public class StringUtilsAutoConfiguration {
private final StringUtilsProperties properties;

public StringUtilsAutoConfiguration(StringUtilsProperties properties) {
this.properties = properties;
}

@Bean
public StringUtilsService stringUtilsService() {
return new StringUtilsService(properties);
}
}

用户可以在 application.properties 中配置:

1
string.utils.default-prefix=Hello_

调用 stringUtilsService.concatenateWithPrefix("World") 将返回 Hello_World

价值

  • 提供用户友好的配置方式,增强 Starter 的灵活性和可扩展性。
  • 支持动态调整行为,满足不同场景的需求。

6.3.5 模块化与生态一致性

  • 简单 JAR 包:只是一个独立的库,可能不符合 Spring Boot 的使用习惯。
  • Spring Boot Starter:遵循 Spring Boot 的 Starter 命名和设计规范(spring-boot-starter-*),与其他 Starter 无缝协作。

价值

  • 提供一致的开发体验,降低学习成本。
  • 便于在 Spring Boot 生态中共享和复用。

6.3.6 生命周期管理

  • 简单 JAR 包:工具类的生命周期完全由用户管理,可能导致资源泄漏或不必要的实例化。
  • Spring Boot Starter:Bean 的生命周期由 Spring 容器管理,支持单例、原型等作用域,还可以利用 @PostConstruct@PreDestroy 进行初始化和销毁。

示例

1
2
3
4
5
6
7
8
9
10
11
public class StringUtilsService {
@PostConstruct
public void init() {
System.out.println("StringUtilsService initialized");
}

@PreDestroy
public void destroy() {
System.out.println("StringUtilsService destroyed");
}
}

Spring 容器会自动调用 initdestroy 方法。

价值

  • 提供更安全和可控的资源管理。
  • 避免手动管理带来的潜在问题。

6.4 适用场景对比

6.4.1 使用简单 JAR 包的场景

  • 功能非常简单:比如一个纯静态工具类(StringUtils),不需要依赖注入或配置。
  • 不依赖 Spring 生态:工具类完全独立,不需要 Spring 的功能。
  • 轻量级需求:项目规模小,不需要复杂的配置或条件加载。

示例: Apache Commons Lang 的 StringUtils 是一个典型的简单 JAR 包,提供静态方法(如 StringUtils.isBlank()),无需 Spring 环境即可使用。

6.4.2 使用 Spring Boot Starter 的场景

  • 需要自动配置:希望用户引入依赖后无需手动配置即可使用。
  • 需要条件加载:根据环境或配置动态启用/禁用功能。
  • 需要 Spring 生态支持:依赖注入、事件监听、配置属性等。
  • 模块化设计:为 Spring Boot 项目提供可复用的模块。

示例spring-boot-starter-web 是一个典型的 Starter,它自动配置了 Web 相关的 Bean(如 DispatcherServlet),并支持条件加载和配置属性。

6.5 回到本案例

spring-boot-string-utils-starter 中,虽然功能简单,但通过 Starter 实现有以下好处:

  1. 自动注入:用户无需手动创建 StringUtilsService 实例,直接通过 @Autowired 使用。
  2. 可扩展性:未来可以添加条件加载、配置属性等功能。
  3. Spring 生态整合:可以轻松与其他 Spring Bean 协作。
  4. 一致性:遵循 Spring Boot 的开发习惯,提供更好的用户体验。

如果使用简单 JAR 包:

  • 用户需要手动实例化 StringUtilsService 或自己定义为 Bean。
  • 无法支持配置属性或条件加载。
  • 无法利用 Spring 的生命周期管理。

结论

  • 如果你的目标是创建一个轻量级、独立的工具库,且不依赖 Spring 生态,简单 JAR 包是一个更轻量级的选择。
  • 如果目标是为 Spring Boot 项目提供可复用的模块,支持自动配置、条件加载和 Spring 生态整合,Starter 是更好的选择。

7. 总结

通过这个案例,我们创建了一个简单的 Spring Boot Starter,实现了字符串操作功能,并回答了三个关键问题:

  1. spring.factories 可以指定多个文件吗?

    • 可以,使用逗号分隔多个类名,支持模块化设计。
  2. 每个文件必须是 @Configuration 吗?

    • 不一定,取决于键的用途。对于 EnableAutoConfiguration,建议使用 @Configuration;对于其他用途(如监听器),则不需要。
  3. 为什么需要 Starter,而不仅仅是一个简单的 JAR 包?

    • Starter 提供了自动配置、条件加载、Spring 生态整合、配置属性支持、模块化设计和生命周期管理等优势,适合需要深度整合 Spring Boot 生态的场景。

这个 Starter 是一个基础实现,你可以根据需求扩展:

  • 添加更多字符串操作方法。
  • 使用 @ConfigurationProperties 支持自定义配置。
  • 添加条件注解(如 @ConditionalOnProperty)控制 Bean 的加载。