Spring Boot 的 Starter 机制是其生态系统中最重要的设计之一,它让依赖管理和自动配置变得异常简单。本文将深入解析 Starter 的工作原理,并手把手教你如何开发一个自定义 Starter。
Starter 是 Spring Boot 提供的一种约定优于配置的依赖描述符,它将一组相关的依赖、配置和自动配置类打包在一起,让开发者只需引入一个依赖即可快速启用某项功能。
spring-boot-starter-* 命名,如 spring-boot-starter-web*-spring-boot-starter 命名,如 mybatis-spring-boot-starterSpring Boot 通过 META-INF/spring.factories 文件(Spring Boot 2.7+ 推荐使用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)来加载自动配置类:
// META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.autoconfigure.MyAutoConfiguration
使用 @Conditional 系列注解实现按需加载:
@Configuration
@ConditionalOnClass(MyService.class)
@ConditionalOnProperty(prefix = "my.starter", name = "enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 只有用户未定义时才创建
public MyService myService(MyProperties properties) {
return new MyService(properties);
}
}
通过 @ConfigurationProperties 将外部配置绑定到 Java 对象:
@Data
@ConfigurationProperties(prefix = "my.starter")
public class MyProperties {
private boolean enabled = true;
private String name = "default";
private int timeout = 5000;
private List<String> interceptors = new ArrayList<>();
@NestedConfigurationProperty
private CacheProperties cache = new CacheProperties();
@Data
public static class CacheProperties {
private boolean enabled = false;
private Duration ttl = Duration.ofMinutes(30);
}
}
下面我们创建一个实用的分布式日志追踪 Starter,自动为每个请求生成 TraceID。
my-trace-spring-boot-starter/
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/example/trace/
│ ├── MyTraceAutoConfiguration.java
│ ├── TraceProperties.java
│ ├── TraceContext.java
│ ├── TraceIdGenerator.java
│ └── TraceFilter.java
└── resources/
└── META-INF/
└── spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports
<?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
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>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>my-trace-spring-boot-starter</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- 必须:自动配置支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- 必须:配置处理器,生成 metadata -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 可选:Web 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<optional>true</optional>
</dependency>
<!-- 可选:WebFlux 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
@Data
@ConfigurationProperties(prefix = "my.trace")
public class TraceProperties {
/** 是否启用追踪 */
private boolean enabled = true;
/** TraceID HTTP Header 名称 */
private String headerName = "X-Trace-Id";
/** 是否将 TraceID 写入响应头 */
private boolean responseHeader = true;
/** TraceID 长度 */
private int idLength = 16;
/** 需要忽略的 URL 路径 */
private List<String> excludePaths = Arrays.asList("/health", "/actuator/**");
}
public class TraceContext {
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();
public static void set(String traceId) {
TRACE_ID.set(traceId);
START_TIME.set(System.currentTimeMillis());
}
public static String get() {
return TRACE_ID.get();
}
public static long getDuration() {
Long start = START_TIME.get();
return start != null ? System.currentTimeMillis() - start : 0;
}
public static void clear() {
TRACE_ID.remove();
START_TIME.remove();
}
// MDC 集成,方便日志使用
public static void putToMdc() {
MDC.put("traceId", get());
}
}
@Component
public class TraceIdGenerator {
private final SecureRandom random = new SecureRandom();
public String generate(int length) {
String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
// 支持从上游服务传递的 TraceID
public String generateOrReuse(String existingId, int length) {
return StringUtils.hasText(existingId) ? existingId : generate(length);
}
}
@Order(Ordered.HIGHEST_PRECEDENCE + 100)
public class TraceFilter extends OncePerRequestFilter {
private final TraceProperties properties;
private final TraceIdGenerator generator;
public TraceFilter(TraceProperties properties, TraceIdGenerator generator) {
this.properties = properties;
this.generator = generator;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 检查是否排除
if (isExcluded(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
try {
// 从请求头获取或生成 TraceID
String existingTraceId = request.getHeader(properties.getHeaderName());
String traceId = generator.generateOrReuse(existingTraceId, properties.getIdLength());
// 设置上下文
TraceContext.set(traceId);
TraceContext.putToMdc();
// 可选:写入响应头
if (properties.isResponseHeader()) {
response.setHeader(properties.getHeaderName(), traceId);
}
// 包装请求,支持后续 Filter/Servlet 获取 TraceID
chain.doFilter(new TraceIdRequestWrapper(request, traceId), response);
} finally {
// 清理,防止线程池复用导致污染
TraceContext.clear();
MDC.clear();
}
}
private boolean isExcluded(String uri) {
return properties.getExcludePaths().stream()
.anyMatch(pattern -> new AntPathMatcher().match(pattern, uri));
}
}
@Configuration
@EnableConfigurationProperties(TraceProperties.class)
@ConditionalOnClass(Filter.class)
@ConditionalOnProperty(prefix = "my.trace", name = "enabled", havingValue = "true", matchIfMissing = true)
public class MyTraceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public TraceIdGenerator traceIdGenerator() {
return new TraceIdGenerator();
}
@Bean
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnMissingBean(name = "traceFilter")
public FilterRegistrationBean<TraceFilter> traceFilter(
TraceProperties properties,
TraceIdGenerator generator) {
FilterRegistrationBean<TraceFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new TraceFilter(properties, generator));
bean.addUrlPatterns("/*");
bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 100);
return bean;
}
// WebFlux 支持
@Bean
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnMissingBean
public TraceWebFilter traceWebFilter(TraceProperties properties,
TraceIdGenerator generator) {
return new TraceWebFilter(properties, generator);
}
}
创建 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
com.example.trace.MyTraceAutoConfiguration
mvn clean install
<dependency>
<groupId>com.example</groupId>
<artifactId>my-trace-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
my:
trace:
enabled: true
header-name: X-Request-Id
response-header: true
id-length: 20
exclude-paths:
- /health
- /metrics
<!-- logback-spring.xml -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
输出示例:
2024-01-15 09:23:45 [http-nio-8080-exec-1] [aB3xK9mP2nQ7vZ4wE5r] INFO c.e.c.OrderController - 订单创建成功,订单号:12345
// 默认启用,但提供关闭开关
@ConditionalOnProperty(prefix = "my.starter", name = "enabled",
havingValue = "true", matchIfMissing = true)
@Bean
@ConditionalOnMissingBean // 用户可覆盖
public MyService myService() { }
@Bean
@ConditionalOnBean(DataSource.class) // 依赖其他组件
public MyRepository myRepository(DataSource ds) { }
添加 additional-spring-configuration-metadata.json:
{
"hints": [
{
"name": "my.trace.id-length",
"values": [
{"value": 16, "description": "标准长度"},
{"value": 32, "description": "高安全场景"}
]
}
]
}
@Component
@ConditionalOnClass(HealthIndicator.class)
public class MyServiceHealthIndicator implements HealthIndicator {
@Override
public Health health() {
if (myService.isHealthy()) {
return Health.up()
.withDetail("connections", myService.getActiveConnections())
.build();
}
return Health.down().withDetail("error", "连接失败").build();
}
}
启动时添加 --debug 参数,查看哪些自动配置生效:
java -jar myapp.jar --debug
Spring Boot Starter 机制的核心是自动配置 + 约定优于配置。开发自定义 Starter 的关键步骤:
*-spring-boot-starter 命名@Conditional* 系列注解实现按需加载@ConfigurationProperties 提供类型安全配置@ConditionalOnMissingBean 让用户可自定义掌握 Starter 开发后,你可以将团队内部的公共组件封装成 Starter,大幅提升开发效率和代码复用率。
参考链接: