一、为什么要自定义 Starter
在一个中型 Java 团队中,你大概率见过这样的现象:
- 每个微服务都拷贝一份
RestTemplateConfig - 日志 AOP 切面在 N 个模块中重复出现
- 统一异常处理代码在不同项目中各有差异
自定义 Starter 的目的:将通用能力收敛为一个依赖,引入即用,零代码集成。
二、命名与模块结构
命名规范
Spring 官方建议:
- 官方 Starter:
spring-boot-starter-xxx(如spring-boot-starter-web) - 自定义 Starter:
xxx-spring-boot-starter(如mybatis-spring-boot-starter)
模块结构
ops-log-spring-boot-starter/
├── pom.xml
├── src/main/java/com/example/opslog/
│ ├── OpsLogAutoConfiguration.java // 自动配置类
│ ├── OpsLogProperties.java // 配置属性
│ ├── annotation/
│ │ └── OpsLog.java // 注解
│ ├── aspect/
│ │ └── OpsLogAspect.java // AOP 切面
│ └── model/
│ └── OperationLog.java // 日志实体
└── src/main/resources/
└── META-INF/
└── spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports
注意:Spring Boot 3.x 使用
AutoConfiguration.imports文件,替代了旧版的spring.factories。
三、定义配置属性
@ConfigurationProperties(prefix = "ops.log")
public class OpsLogProperties {
/** 是否启用操作日志 */
private boolean enabled = true;
/** 日志存储方式:db / file / kafka */
private StorageType storage = StorageType.DB;
/** 是否记录请求参数 */
private boolean recordParams = true;
/** 是否记录响应结果 */
private boolean recordResult = false;
/** 参数脱敏字段列表 */
private List<String> maskFields = List.of("password", "token", "secret");
// getter / setter ...
public enum StorageType { DB, FILE, KAFKA }
}
四、编写业务注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpsLog {
/** 操作模块 */
String module();
/** 操作类型 */
String action();
/** 操作描述(支持 SpEL 表达式) */
String desc() default "";
}
五、AOP 切面实现
@Aspect
@Component
@ConditionalOnProperty(prefix = "ops.log", name = "enabled", havingValue = "true", matchIfMissing = true)
public class OpsLogAspect {
@Around("@annotation(opsLog)")
public Object around(ProceedingJoinPoint joinPoint, OpsLog opsLog) throws Throwable {
long start = System.currentTimeMillis();
Object result = null;
Throwable error = null;
try {
result = joinPoint.proceed();
return result;
} catch (Throwable t) {
error = t;
throw t;
} finally {
long cost = System.currentTimeMillis() - start;
// 异步记录日志,不阻塞主流程
CompletableFuture.runAsync(() -> saveLog(opsLog, joinPoint, result, error, cost));
}
}
private void saveLog(OpsLog opsLog, ProceedingJoinPoint jp,
Object result, Throwable error, long cost) {
OperationLog log = OperationLog.builder()
.module(opsLog.module())
.action(opsLog.action())
.method(jp.getSignature().toShortString())
.cost(cost)
.success(error == null)
.build();
// 发送到存储层(DB / Kafka / 文件)
logPublisher.publish(log);
}
}
六、自动配置
@AutoConfiguration
@EnableConfigurationProperties(OpsLogProperties.class)
@ConditionalOnClass({Aspect.class})
@ConditionalOnProperty(prefix = "ops.log", name = "enabled", havingValue = "true", matchIfMissing = true)
public class OpsLogAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public OpsLogAspect opsLogAspect(OpsLogProperties properties) {
return new OpsLogAspect(properties);
}
// 根据 storage 类型注入不同的日志发布器
@Bean
@ConditionalOnProperty(prefix = "ops.log", name = "storage", havingValue = "db")
public LogPublisher dbLogPublisher() {
return new DbLogPublisher();
}
@Bean
@ConditionalOnProperty(prefix = "ops.log", name = "storage", havingValue = "kafka")
public LogPublisher kafkaLogPublisher() {
return new KafkaLogPublisher();
}
}
几个关键注解
| 注解 | 作用 |
|---|---|
@AutoConfiguration |
声明为自动配置类(替代旧版 @Configuration) |
@ConditionalOnClass |
类路径存在指定类时才生效 |
@ConditionalOnMissingBean |
用户未自定义时才创建默认 Bean |
@ConditionalOnProperty |
配置属性满足条件才生效 |
@EnableConfigurationProperties |
启用配置属性绑定 |
七、自动配置注册
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件内容:
com.example.opslog.OpsLogAutoConfiguration
就这么一行。Spring Boot 启动时会自动扫描并加载。
八、使用方集成
<!-- pom.xml -->
<dependency>
<groupId>com.example</groupId>
<artifactId>ops-log-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
# application.yml
ops:
log:
storage: kafka # 存入 Kafka
record-result: true # 记录响应
mask-fields: [password, idCard, phone] # 额外脱敏字段
@RestController
public class UserController {
@OpsLog(module = "用户管理", action = "创建用户", desc = "'创建用户:' + #user.name")
@PostMapping("/users")
public Result<User> createUser(@RequestBody User user) {
return userService.create(user);
}
}
一行注解,零配置即可启用操作日志。
九、发布与版本管理
建议将 Starter 发布到公司私有 Maven 仓库(Nexus / Artifactory),配合语义化版本:
- 1.x.x:基础功能稳定,向后兼容
- MINOR 版本新增配置项时,保持默认值兼容旧行为
- MAJOR 版本破坏性变更前,提前邮件通知下游
十、总结
自定义 Starter 的核心思路:约定优于配置 + 条件装配。一个好的 Starter 应该:
- ✅ 引入即可用,合理默认值覆盖 80% 场景
- ✅ 配置项清晰,支持覆盖默认行为
- ✅ 允许用户完全接管(
@ConditionalOnMissingBean) - ✅ 文档完善,最好附带一个 sample 模块
把你团队的通用能力都变成 Starter 吧,下一次开新项目你只需要写业务代码。
