一、为什么要自定义 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。
三、定义配置属性
`java
@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
// getter / setter ...
public enum StorageType { DB, FILE, KAFKA }
}
`
四、编写业务注解
`java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpsLog {
/* 操作模块 /
String module();
/* 操作类型 /
String action();
/* 操作描述(支持 SpEL 表达式) /
String desc() default "";
}
`
五、AOP 切面实现
`java
@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);
}
}
`
六、自动配置
`java
@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();
}
}
`
几个关键注解
| 声明为自动配置类(替代旧版 @Configuration) | 类路径存在指定类时才生效 | 用户未自定义时才创建默认 Bean | 配置属性满足条件才生效 | 启用配置属性绑定 七、自动配置注册
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件内容:
`
com.example.opslog.OpsLogAutoConfiguration
`
就这么一行。Spring Boot 启动时会自动扫描并加载。
八、使用方集成
`xml
`
`yaml
`application.yml
ops:
log:
storage: kafka # 存入 Kafka
record-result: true # 记录响应
mask-fields: [password, idCard, phone] # 额外脱敏字段
`java
@RestController
public class UserController {
@OpsLog(module = "用户管理", action = "创建用户", desc = "'创建用户:' + #user.name")
@PostMapping("/users")
public Result
一行注解,零配置即可启用操作日志。
九、发布与版本管理
建议将 Starter 发布到公司私有 Maven 仓库(Nexus / Artifactory),配合语义化版本:
- 1.x.x:基础功能稳定,向后兼容
- MINOR 版本新增配置项时,保持默认值兼容旧行为
- MAJOR 版本破坏性变更前,提前邮件通知下游
十、总结
自定义 Starter 的核心思路:约定优于配置 + 条件装配。一个好的 Starter 应该:
- ✅ 引入即可用,合理默认值覆盖 80% 场景
- ✅ 配置项清晰,支持覆盖默认行为
- ✅ 允许用户完全接管(@ConditionalOnMissingBean`)
- ✅ 文档完善,最好附带一个 sample 模块
把你团队的通用能力都变成 Starter 吧,下一次开新项目你只需要写业务代码。