打造一个 Starter
Spring Boot 的 Starter 机制是其生态系统中强大的一部分,允许开发者创建可重用的模块,简化依赖管理和配置。本文将通过一个实际案例,详细讲解如何创建一个 Spring Boot Starter,用于提供字符串操作工具。
我们将实现一个 spring-boot-utils-starter,并探讨一个常见问题:为什么需要 Starter,而不仅仅是一个简单的 JAR 包?通过对比两者的优劣,我们将揭示 Starter 的真正价值。
项目背景与目标
我们将创建一个名为 spring-boot-utils-starter 的 Spring Boot Starter,提供以下字符串操作功能:
- 反转字符串 (reverse)
- 转换为大写 (toUpperCase)
- 转换为小写 (toLowerCase)
- 拼接字符串 (concatenate)
这个 Starter 将通过 Spring Boot 的自动配置机制,让用户在引入依赖后无需额外配置即可使用这些功能。同时,我们将回答以下问题:
spring.factories可以指定多个文件吗?- 每个文件必须是
@Configuration吗? - 为什么需要 Starter,而不仅仅是一个简单的 JAR 包?
项目结构
项目的目录结构如下:
spring-boot-utils-starter/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── business/
│ │ │ └── springbootstringutilsstarter/
│ │ │ ├── StringUtilsService.java
│ │ │ └── StringUtilsAutoConfiguration.java
│ │ └── resources/
│ │ └── META-INF/
│ │ └── spring.factories
│ └── test/
└── target/2
3
4
5
6
7
8
9
10
11
12
13
14
15
pom.xml:Maven 配置文件,定义依赖和构建规则。StringUtilsService.java:核心服务类,包含字符串操作方法。StringUtilsAutoConfiguration.java:自动配置类,负责将服务类注入 Spring 容器。spring.factories:Spring Boot 自动配置的入口文件,位于src/main/resources/META-INF/。
实现步骤
创建 Maven 项目
首先,我们需要一个 Maven 项目,pom.xml 文件如下:
<?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>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
关键点:
- Spring Boot 版本:使用 2.6.13,确保兼容 8。
- 依赖:引入
spring-boot-starter作为基础依赖,提供 Spring Boot 的核心功能。 - 构建插件:配置
spring-boot-maven-plugin,但设置skip=true,因为 Starter 不需要生成可执行 JAR(Starter 只是依赖库,不是可运行应用)。 - 编码:确保项目使用
UTF-8编码,防止编码问题。
实现字符串工具服务
创建 StringUtilsService.java,包含核心字符串操作方法:
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;
}
}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
关键点:
- 空值检查:每个方法都包含对输入参数的空值检查,增强健壮性。
- 方法设计:提供基础的字符串操作,满足常见需求。
创建自动配置类
创建 StringUtilsAutoConfiguration.java,将 StringUtilsService 注入 Spring 容器:
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();
}
}2
3
4
5
6
7
8
9
10
11
12
13
关键点:
注解:
@Configuration:标记这是一个 Spring 配置类。@Bean:定义一个 Bean,Spring Boot 会自动将其注入容器。
作用:StringUtilsService 会被 Spring 管理,用户可以通过
@Autowired直接使用。
配置 spring.factories
在 src/main/resources/META-INF/spring.factories 中,指定自动配置类:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\
com.business.springbootstringutilsstarter.StringUtilsAutoConfiguration2
关键点:
路径:
META-INF/spring.factories是 Spring Boot 的标准路径,用于声明自动配置类。键值对:
键
org.springframework.boot.autoconfigure.EnableAutoConfiguration是 Spring Boot 自动配置的入口。值是配置类的全限定名,支持指定多个类(用逗号分隔)。
解答问题:spring.factories 的使用
在开发过程中,可能会遇到以下问题:
spring.factories 可以指定多个文件吗?
是的,spring.factories 支持指定多个类。它的格式是一个键值对,值可以用逗号分隔指定多个类名。
示例:添加另一个配置类
创建一个新配置类 AnotherConfiguration.java:
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!";
}
}2
3
4
5
6
7
8
9
10
11
12
13
更新 spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.business.springbootstringutilsstarter.StringUtilsAutoConfiguration,com.business.springbootstringutilsstarter.AnotherConfiguration工作原理:
- Spring Boot 在启动时会读取
spring.factories文件。 - 对于 EnableAutoConfiguration 键,Spring Boot 会加载所有列出的类(StringUtilsAutoConfiguration 和 AnotherConfiguration)。
- 结果是 StringUtilsService 和 helloBean 都会注入到 Spring 容器中。
价值:
- 支持模块化设计,允许 Starter 包含多个配置类,满足复杂需求。
每个文件必须是 @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:
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!");
}
}2
3
4
5
6
7
8
9
10
11
更新 spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\
com.business.springbootstringutilsstarter.StringUtilsAutoConfiguration
org.springframework.context.ApplicationListener=\\
com.business.springbootstringutilsstarter.MyListener2
3
4
5
分析:
- MyListener 不需要 @Configuration,因为它是一个事件监听器,只需实现 ApplicationListener 接口。
- Spring Boot 会自动注册 MyListener 为事件监听器,在容器刷新时触发 onApplicationEvent 方法。
价值:
- 提供灵活性,允许 Starter 注册不同类型的组件(如配置类、监听器、后置处理器等)。
使用 Starter
构建和发布
运行以下命令构建项目:
mvn clean install这会生成 spring-boot-string-utils-starter-0.0.1-SNAPSHOT.jar,可以发布到 Maven 仓库或本地使用。
构建后的 JAR 包结构如下:
spring-boot-string-utils-starter-0.0.1-SNAPSHOT.jar
├── META-INF/
│ └── spring.factories
├── com/
│ └── business/
│ └── springbootstringutilsstarter/
│ ├── StringUtilsService.class
│ └── StringUtilsAutoConfiguration.class2
3
4
5
6
7
8
在其他项目中使用
在另一个 Spring Boot 项目中引入依赖:
<dependency>
<groupId>com.business</groupId>
<artifactId>spring-boot-string-utils-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>2
3
4
5
使用示例:
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"));
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
测试代码
创建一个简单的 Spring Boot 应用:
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");
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
输出:
Reversed: olleH
Upper: HELLO
Lower: hello
Concatenated: Hello World2
3
4
关键点:
- 自动注入:StringUtilsService 由 Spring 容器管理,用户通过 @Autowired 直接使用。
- 零配置:用户无需手动定义 Bean,Starter 自动完成配置。
探讨问题:为什么需要 Starter,而不仅仅是一个简单的 JAR 包?
一个常见的疑问是:既然可以创建一个简单的 JAR 包(包含 StringUtilsService),直接引入并使用,为什么还需要 Spring Boot Starter?以下通过对比两者的实现和优劣,详细分析 Starter 的价值。
简单 JAR 包的实现
假设我们创建一个简单的 JAR 包,包含 StringUtilsService:
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();
}
}2
3
4
5
6
7
8
9
10
11
构建:将这个类打包成 JAR(string-utils.jar)。
使用:在另一个项目中引入依赖,然后手动实例化:
StringUtilsService utils = new StringUtilsService();
String result = utils.reverse("hello");2
替代方案:如果想在 Spring 环境中使用,可以手动定义为 Bean:
@Configuration
public class AppConfig {
@Bean
public StringUtilsService stringUtilsService() {
return new StringUtilsService();
}
}2
3
4
5
6
7
Spring Boot Starter 的实现
我们的 spring-boot-utils-starter 包含:
- StringUtilsService:工具类。
- StringUtilsAutoConfiguration:自动配置类。
- spring.factories:自动配置入口。
使用时,只需引入依赖,Spring Boot 自动注入 StringUtilsService:
@Autowired
private StringUtilsService stringUtilsService;2
对比分析:Starter 的核心优势
自动配置(Zero Configuration)
- 简单 JAR 包:需要用户手动实例化(
new StringUtilsService())或手动定义为 Spring Bean。 - Spring Boot Starter:通过
spring.factories和 @Configuration,Spring Boot 在启动时自动将 StringUtilsService 注入容器,用户无需任何配置即可通过 @Autowired 使用。
价值:
- 减少样板代码,符合 Spring Boot 的“约定优于配置”理念。
- 提升开发效率,用户无需关心 Bean 的创建和管理。
条件加载(Conditional Loading)
- 简单 JAR 包:一旦引入,工具类总是可用,无法根据环境动态加载或禁用。
- Spring Boot Starter:支持条件注解(如
@ConditionalOnProperty、@ConditionalOnClass),可以根据配置或环境动态加载 Bean。
示例: 在 StringUtilsAutoConfiguration 中添加条件:
@Configuration
@ConditionalOnProperty(prefix = "string.utils", name = "enabled", havingValue = "true", matchIfMissing = true)
public class StringUtilsAutoConfiguration {
@Bean
public StringUtilsService stringUtilsService() {
return new StringUtilsService();
}
}2
3
4
5
6
7
8
用户可以通过 application.properties 控制:
string.utils.enabled=false如果 enabled=false,StringUtilsService 不会加载。
价值:
- 提供灵活性,适合复杂场景(比如只在特定环境下启用功能)。
- 避免不必要的 Bean 加载,优化性能。
与 Spring 生态的深度整合
简单 JAR 包:只是一个独立的工具库,无法直接利用 Spring 的功能(如依赖注入、事件监听、AOP 等)。
Spring Boot Starter:深度整合 Spring 生态,可以:
- 使用依赖注入(注入其他 Bean)。
- 发布和监听 Spring 事件。
- 利用 Spring Boot 的配置机制(@ConfigurationProperties)。
示例: 假设 StringUtilsService 需要依赖另一个 Bean:
@Configuration
public class StringUtilsAutoConfiguration {
private final SomeOtherService otherService;
public StringUtilsAutoConfiguration(SomeOtherService otherService) {
this.otherService = otherService;
}
@Bean
public StringUtilsService stringUtilsService() {
return new StringUtilsService(otherService);
}
}2
3
4
5
6
7
8
9
10
11
12
13
Starter 可以轻松注入其他 Bean,而简单 JAR 包需要用户手动管理依赖。
价值:
充分利用 Spring 的 IoC 容器和生态系统,提升开发效率。支持复杂业务逻辑,适合企业级应用。
配置属性支持
- 简单 JAR 包:无法直接支持通过
application.properties自定义行为。 - Spring Boot Starter:可以使用
@ConfigurationProperties提供可配置的属性。
示例: 添加一个配置类:
@ConfigurationProperties(prefix = "string.utils")
public class StringUtilsProperties {
private String defaultPrefix = "";
public String getDefaultPrefix() {
return defaultPrefix;
}
public void setDefaultPrefix(String defaultPrefix) {
this.defaultPrefix = defaultPrefix;
}
}2
3
4
5
6
7
8
9
10
11
12
在 StringUtilsService 中使用:
public class StringUtilsService {
private final String defaultPrefix;
public StringUtilsService(StringUtilsProperties properties) {
this.defaultPrefix = properties.getDefaultPrefix();
}
public String concatenateWithPrefix(String input) {
return defaultPrefix + input;
}
}2
3
4
5
6
7
8
9
10
11
在 StringUtilsAutoConfiguration 中启用:
@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);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
用户可以在 application.properties 中配置:
string.utils.default-prefix=Hello_调用 stringUtilsService.concatenateWithPrefix("World") 将返回 Hello_World。
价值:
- 提供用户友好的配置方式,增强 Starter 的灵活性和可扩展性。
- 支持动态调整行为,满足不同场景的需求。
模块化与生态一致性
- 简单 JAR 包:只是一个独立的库,可能不符合 Spring Boot 的使用习惯。
- Spring Boot Starter:遵循 Spring Boot 的 Starter 命名和设计规范(spring-boot-starter-*),与其他 Starter 无缝协作。
价值:
- 提供一致的开发体验,降低学习成本。
- 便于在 Spring Boot 生态中共享和复用。
生命周期管理
- 简单 JAR 包:工具类的生命周期完全由用户管理,可能导致资源泄漏或不必要的实例化。
- Spring Boot Starter:Bean 的生命周期由 Spring 容器管理,支持单例、原型等作用域,还可以利用
@PostConstruct和@PreDestroy进行初始化和销毁。
示例:
public class StringUtilsService {
@PostConstruct
public void init() {
System.out.println("StringUtilsService initialized");
}
@PreDestroy
public void destroy() {
System.out.println("StringUtilsService destroyed");
}
}2
3
4
5
6
7
8
9
10
11
Spring 容器会自动调用 init 和 destroy 方法。
价值:
- 提供更安全和可控的资源管理。
- 避免手动管理带来的潜在问题。
适用场景对比
使用简单 JAR 包的场景
- 功能非常简单:比如一个纯静态工具类(StringUtils),不需要依赖注入或配置。
- 不依赖 Spring 生态:工具类完全独立,不需要 Spring 的功能。
- 轻量级需求:项目规模小,不需要复杂的配置或条件加载。
示例: Apache Commons Lang 的 StringUtils 是一个典型的简单 JAR 包,提供静态方法(如 StringUtils.isBlank()),无需 Spring 环境即可使用。
使用 Spring Boot Starter 的场景
- 需要自动配置:希望用户引入依赖后无需手动配置即可使用。
- 需要条件加载:根据环境或配置动态启用/禁用功能。
- 需要 Spring 生态支持:依赖注入、事件监听、配置属性等。
- 模块化设计:为 Spring Boot 项目提供可复用的模块。
示例: spring-boot-starter-web 是一个典型的 Starter,它自动配置了 Web 相关的 Bean(如 DispatcherServlet),并支持条件加载和配置属性。
回到本案例
在 spring-boot-utils-starter 中,虽然功能简单,但通过 Starter 实现有以下好处:
- 自动注入:用户无需手动创建 StringUtilsService 实例,直接通过 @Autowired 使用。
- 可扩展性:未来可以添加条件加载、配置属性等功能。
- Spring 生态整合:可以轻松与其他 Spring Bean 协作。
- 一致性:遵循 Spring Boot 的开发习惯,提供更好的用户体验。
如果使用简单 JAR 包:
- 用户需要手动实例化 StringUtilsService 或自己定义为 Bean。
- 无法支持配置属性或条件加载。
- 无法利用 Spring 的生命周期管理。
结论:
- 如果你的目标是创建一个轻量级、独立的工具库,且不依赖 Spring 生态,简单 JAR 包是一个更轻量级的选择。
- 如果目标是为 Spring Boot 项目提供可复用的模块,支持自动配置、条件加载和 Spring 生态整合,Starter 是更好的选择。
总结
通过这个案例,我们创建了一个简单的 Spring Boot Starter,实现了字符串操作功能,并回答了三个关键问题:
spring.factories 可以指定多个文件吗?
可以,使用逗号分隔多个类名,支持模块化设计。
每个文件必须是 @Configuration 吗?
不一定,取决于键的用途。对于 EnableAutoConfiguration,建议使用 @Configuration;对于其他用途(如监听器),则不需要。
为什么需要 Starter,而不仅仅是一个简单的 JAR 包?
Starter 提供了自动配置、条件加载、Spring 生态整合、配置属性支持、模块化设计和生命周期管理等优势,适合需要深度整合 Spring Boot 生态的场景。
这个 Starter 是一个基础实现,你可以根据需求扩展:
- 添加更多字符串操作方法。
- 使用 @ConfigurationProperties 支持自定义配置。
- 添加条件注解(如 @ConditionalOnProperty)控制 Bean 的加载。
