Spring Boot 自定义 Starter 详解
一、Spring Boot Starter 基础概念
1.1 什么是 Spring Boot Starter
Spring Boot Starter 是 Spring Boot 的一个核心概念,它是一种特殊的依赖描述符,包含了一组可以集成到应用中的依赖项。简单来说,Starter 就是一组预配置的依赖项,使开发者能够快速地将特定功能添加到 Spring Boot 应用中,而无需手动管理每个单独的依赖及其版本。
1.2 Starter 的核心价值
-
简化依赖管理:
- 一个 Starter 通常会引入多个必要的依赖,这些依赖已经过版本兼容性测试
- 开发者只需引入一个 Starter,而不是手动管理多个独立依赖及其版本关系
-
自动配置:
- Starter 不仅仅提供依赖,还通常包含自动配置类
- 能根据应用环境自动配置 Bean,减少手动配置的工作量
-
约定大于配置:
- 遵循"约定大于配置"的设计理念,提供合理的默认配置
- 开发者只需关注与默认行为不同的部分
二、Spring Boot Starter 工作原理
2.1 依赖管理
Maven/Gradle 的依赖管理是 Starter 的基础。每个 Starter 都定义了一组协调工作的依赖:
<!-- Spring Boot Web Starter 示例 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
这个简单的依赖会引入:
- spring-web 和 spring-webmvc 核心库
- 内嵌 Tomcat 服务器
- JSON 处理库
- 日志库
- 其他 Web 应用所需的各种组件
2.2 自动配置机制
Starter 的强大之处在于它们的自动配置能力,主要通过以下方式实现:
-
@EnableAutoConfiguration 注解:
- Spring Boot 应用通过 @SpringBootApplication(其中包含 @EnableAutoConfiguration)启用自动配置
- 该注解触发自动配置的加载过程
-
spring.factories 文件:
- 每个 Starter 通常在 META-INF/spring.factories 文件中声明其自动配置类
- Spring Boot 在启动时会扫描 classpath 下所有的 spring.factories 文件
- 加载其中声明的自动配置类
-
条件化配置:
- 自动配置类使用 @Conditional 系列注解,只在满足特定条件时才创建 Bean
- 例如 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等
2.3 属性绑定
- Starter 使用 @ConfigurationProperties 注解绑定配置属性
- 开发者可以在 application.properties/yml 中覆盖默认配置
三、常见的 Spring Boot Starter
Spring Boot 提供了多种官方 Starter,以下是一些常用的:
Starter 名称 | 作用 |
---|---|
spring-boot-starter | 核心 Starter,包含自动配置、日志和 YAML 支持 |
spring-boot-starter-web | 构建 Web 应用,包括 RESTful 应用程序 |
spring-boot-starter-data-jpa | 使用 Spring Data JPA 与数据库交互 |
spring-boot-starter-security | 添加 Spring Security 支持 |
spring-boot-starter-test | 测试支持,包括 JUnit、Hamcrest 和 Mockito |
spring-boot-starter-jdbc | 使用 JDBC 访问数据库 |
spring-boot-starter-actuator | 提供生产就绪功能,帮助监控和管理应用 |
spring-boot-starter-thymeleaf | 使用 Thymeleaf 视图构建 MVC 应用 |
spring-boot-starter-cache | 提供缓存支持 |
spring-boot-starter-validation | 提供 Java Bean 校验功能 |
四、Starter 的命名规范
Spring Boot 官方 Starter 遵循特定的命名模式:
- 官方 Starter:
spring-boot-starter-*
,如 spring-boot-starter-web - 第三方 Starter:
*-spring-boot-starter
,如 mybatis-spring-boot-starter
这种命名约定使开发者可以轻松区分官方和社区提供的 Starter。
五、自定义 Spring Boot Starter 开发步骤
创建自定义 Starter 通常需要以下步骤:
5.1 创建配置属性类
@ConfigurationProperties(prefix = "acme.service")
public class AcmeServiceProperties {
private boolean enabled = true;
private String prefix = "默认前缀";
private String suffix = "默认后缀";
// getter 和 setter 方法
}
5.2 创建自动配置类
@Configuration
@ConditionalOnClass(AcmeService.class)
@EnableConfigurationProperties(AcmeServiceProperties.class)
public class AcmeServiceAutoConfiguration {
@Autowired
private AcmeServiceProperties properties;
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "acme.service", value = "enabled", havingValue = "true", matchIfMissing = true)
public AcmeService acmeService() {
return new AcmeServiceImpl(properties.getPrefix(), properties.getSuffix());
}
}
5.3 注册自动配置类
在 META-INF/spring.factories
文件中添加:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.acme.AcmeServiceAutoConfiguration
5.4 Maven 依赖结构
通常,自定义 Starter 项目分为两个模块:
- 自动配置模块:包含自动配置代码
- Starter 模块:包含必要的依赖,依赖于自动配置模块
<!-- Starter 模块的 pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot 自动配置核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- 用于生成配置元数据,提供IDE自动提示功能 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 其他必要依赖 -->
</dependencies>
六、Starter 最佳实践
-
提供明确的文档:
- 详细说明 Starter 的功能
- 列出所有可配置的属性及其默认值
- 提供使用示例
-
遵循命名约定:
- 第三方 Starter 使用
xxx-spring-boot-starter
命名
- 第三方 Starter 使用
-
设计合理的默认值:
- 默认配置应适合大多数使用场景
- 同时允许灵活定制
-
合理使用条件注解:
- 避免与其他配置冲突
- 允许用户覆盖默认行为
七、Starter 的优势总结
- 大幅减少配置工作:通过自动配置减少样板代码
- 提供经过验证的依赖组合:解决依赖冲突和版本不兼容问题
- 简化技术集成:使不同技术栈的整合变得更加容易
- 增强可维护性:依赖更新时只需更新 Starter 版本
- 加速开发过程:开发者可以专注于业务逻辑而非基础设施配置
八、自定义日志切面 Starter 实战案例
本部分将介绍如何创建一个自定义的日志切面 Starter,用于在 Spring Boot 应用中自动记录方法调用日志,包括注解方式和非注解方式两种实现。
代码地址
8.1 整体方案设计
我们将创建一个名为 log-aspect-spring-boot-starter
的自定义 Starter,它能够:
- 自动记录方法的调用信息(入参、返回值、执行时间等)
- 支持通过注解指定需要记录日志的方法
- 支持全局配置,无需注解也能记录日志
- 允许自定义日志格式和输出方式
8.2 项目结构
log-aspect-spring-boot-starter/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── logaspect/
│ │ ├── annotation/
│ │ │ └── LogRecord.java
│ │ ├── aspect/
│ │ │ ├── AnnotationLogAspect.java
│ │ │ └── GlobalLogAspect.java
│ │ ├── config/
│ │ │ ├── LogAspectAutoConfiguration.java
│ │ │ └── LogAspectProperties.java
│ │ └── service/
│ │ ├── LogRecordService.java
│ │ └── DefaultLogRecordServiceImpl.java
│ └── resources/
│ └── META-INF/
│ ├── spring.factories
│ └── additional-spring-configuration-metadata.json
├── pom.xml
└── README.md
8.3 核心代码实现
8.3.1 创建日志记录注解
首先,我们定义一个注解,用于标记需要记录日志的方法:
package com.example.logaspect.annotation;
import java.lang.annotation.*;
/**
* 日志记录注解,用于标记需要记录日志的方法
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogRecord {
/**
* 日志描述信息
*/
String description() default "";
/**
* 是否记录入参
*/
boolean recordParams() default true;
/**
* 是否记录返回值
*/
boolean recordResult() default true;
/**
* 是否记录执行时间
*/
boolean recordExecutionTime() default true;
/**
* 日志级别
*/
LogLevel level() default LogLevel.INFO;
/**
* 日志级别枚举
*/
enum LogLevel {
DEBUG, INFO, WARN, ERROR
}
}
8.3.2 创建日志记录服务接口
定义日志记录的服务接口:
package com.example.logaspect.service;
import com.example.logaspect.annotation.LogRecord.LogLevel;
/**
* 日志记录服务接口
*/
public interface LogRecordService {
/**
* 记录方法开始执行
*
* @param methodSignature 方法签名
* @param description 描述信息
* @param args 方法参数
* @param level 日志级别
*/
void recordMethodStart(String methodSignature, String description, Object[] args, LogLevel level);
/**
* 记录方法执行完成
*
* @param methodSignature 方法签名
* @param description 描述信息
* @param args 方法参数
* @param result 返回结果
* @param executionTime 执行时间(毫秒)
* @param level 日志级别
*/
void recordMethodEnd(String methodSignature, String description, Object[] args,
Object result, long executionTime, LogLevel level);
/**
* 记录方法执行异常
*
* @param methodSignature 方法签名
* @param description 描述信息
* @param args 方法参数
* @param throwable 异常信息
* @param executionTime 执行时间(毫秒)
* @param level 日志级别
*/
void recordMethodException(String methodSignature, String description, Object[] args,
Throwable throwable, long executionTime, LogLevel level);
}
8.3.3 创建默认的日志记录服务实现
package com.example.logaspect.service;
import com.example.logaspect.annotation.LogRecord.LogLevel;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
/**
* 默认日志记录服务实现
*/
public class DefaultLogRecordServiceImpl implements LogRecordService {
private static final Logger log = LoggerFactory.getLogger(DefaultLogRecordServiceImpl.class);
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void recordMethodStart(String methodSignature, String description, Object[] args, LogLevel level) {
String message = String.format("开始执行方法: %s", methodSignature);
if (StringUtils.hasText(description)) {
message += String.format(" | 描述: %s", description);
}
if (args != null && args.length > 0) {
try {
message += String.format(" | 入参: %s", objectMapper.writeValueAsString(args));
} catch (Exception e) {
message += String.format(" | 入参: %s (序列化失败)", args);
}
}
logByLevel(message, level);
}
@Override
public void recordMethodEnd(String methodSignature, String description, Object[] args,
Object result, long executionTime, LogLevel level) {
String message = String.format("方法执行完成: %s | 耗时: %d毫秒", methodSignature, executionTime);
if (StringUtils.hasText(description)) {
message += String.format(" | 描述: %s", description);
}
if (result != null) {
try {
message += String.format(" | 返回值: %s", objectMapper.writeValueAsString(result));
} catch (Exception e) {
message += String.format(" | 返回值: %s (序列化失败)", result);
}
}
logByLevel(message, level);
}
@Override
public void recordMethodException(String methodSignature, String description, Object[] args,
Throwable throwable, long executionTime, LogLevel level) {
String message = String.format("方法执行异常: %s | 耗时: %d毫秒 | 异常: %s",
methodSignature, executionTime, throwable.getMessage());
if (StringUtils.hasText(description)) {
message += String.format(" | 描述: %s", description);
}
logByLevel(message, LogLevel.ERROR);
}
private void logByLevel(String message, LogLevel level) {
switch (level) {
case DEBUG:
log.debug(message);
break;
case INFO:
log.info(message);
break;
case WARN:
log.warn(message);
break;
case ERROR:
log.error(message);
break;
default:
log.info(message);
break;
}
}
}
8.3.4 创建配置属性类
package com.example.logaspect.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* 日志切面配置属性
*/
@ConfigurationProperties(prefix = "logging.aspect")
public class LogAspectProperties {
/**
* 是否启用日志切面
*/
private boolean enabled = true;
/**
* 是否启用基于注解的日志切面
*/
private boolean enableAnnotationAspect = true;
/**
* 是否启用全局日志切面(无需注解)
*/
private boolean enableGlobalAspect = false;
/**
* 全局切面的切入点表达式
*/
private String globalPointcut = "execution(* com.example..*.*(..))";
/**
* 是否记录方法入参
*/
private boolean recordParams = true;
/**
* 是否记录方法返回值
*/
private boolean recordResult = true;
/**
* 是否记录方法执行时间
*/
private boolean recordExecutionTime = true;
/**
* 排除的包路径,这些包下的方法不会被记录日志
*/
private List<String> excludePackages = new ArrayList<>();
// getters and setters
// ...
}
8.3.5 创建基于注解的日志切面
package com.example.logaspect.aspect;
import com.example.logaspect.annotation.LogRecord;
import com.example.logaspect.annotation.LogRecord.LogLevel;
import com.example.logaspect.config.LogAspectProperties;
import com.example.logaspect.service.LogRecordService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import java.lang.reflect.Method;
/**
* 基于注解的日志切面
*/
@Aspect
@Order(1)
public class AnnotationLogAspect {
private final LogRecordService logRecordService;
private final LogAspectProperties properties;
public AnnotationLogAspect(LogRecordService logRecordService, LogAspectProperties properties) {
this.logRecordService = logRecordService;
this.properties = properties;
}
@Around("@annotation(com.example.logaspect.annotation.LogRecord)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 实现逻辑
// ...
}
}
8.3.6 创建全局日志切面(非注解方式)
package com.example.logaspect.aspect;
import com.example.logaspect.annotation.LogRecord.LogLevel;
import com.example.logaspect.config.LogAspectProperties;
import com.example.logaspect.service.LogRecordService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import java.lang.reflect.Method;
/**
* 全局日志切面(无需注解)
*/
@Aspect
@Order(2)
public class GlobalLogAspect {
private final LogRecordService logRecordService;
private final LogAspectProperties properties;
public GlobalLogAspect(LogRecordService logRecordService, LogAspectProperties properties) {
this.logRecordService = logRecordService;
this.properties = properties;
}
@Pointcut("execution(* *.*(..))")
public void defaultPointcut() {}
@Around("defaultPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 实现逻辑
// ...
}
}
8.3.7 创建自动配置类
package com.example.logaspect.config;
import com.example.logaspect.aspect.AnnotationLogAspect;
import com.example.logaspect.aspect.GlobalLogAspect;
import com.example.logaspect.service.DefaultLogRecordServiceImpl;
import com.example.logaspect.service.LogRecordService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 日志切面自动配置类
*/
@Configuration
@EnableConfigurationProperties(LogAspectProperties.class)
@ConditionalOnProperty(prefix = "logging.aspect", name = "enabled", havingValue = "true", matchIfMissing = true)
public class LogAspectAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public LogRecordService logRecordService() {
return new DefaultLogRecordServiceImpl();
}
@Bean
@ConditionalOnProperty(prefix = "logging.aspect", name = "enable-annotation-aspect", havingValue = "true", matchIfMissing = true)
public AnnotationLogAspect annotationLogAspect(LogRecordService logRecordService, LogAspectProperties properties) {
return new AnnotationLogAspect(logRecordService, properties);
}
@Bean
@ConditionalOnProperty(prefix = "logging.aspect", name = "enable-global-aspect", havingValue = "true", matchIfMissing = false)
public GlobalLogAspect globalLogAspect(LogRecordService logRecordService, LogAspectProperties properties) {
return new GlobalLogAspect(logRecordService, properties);
}
}
8.3.8 创建 spring.factories 文件(springboot 2.x)
在 src/main/resources/META-INF/spring.factories
中添加:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.logaspect.config.LogAspectAutoConfiguration
8.3.9 创建 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件(springboot 3.x)
在 Spring Boot 3.0 及以上版本中,spring.factories
方式已被弃用,取而代之的是使用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件进行自动配置类的注册。
在 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中添加:
com.example.logaspect.config.LogAspectAutoConfiguration
这个文件的格式很简单,每行一个配置类的全限定名,不需要键值对形式。这种方式加载更高效,减少了对不必要配置的解析,是 Spring Boot 3.x 推荐的自动配置注册方式。
如果你需要同时兼容 Spring Boot 2.x 和 3.x 版本,可以同时提供这两个文件,不会产生冲突:
META-INF/spring.factories
- 用于 Spring Boot 2.xMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
- 用于 Spring Boot 3.x
示例项目结构:
src/main/resources/
├── META-INF/
│ ├── spring.factories # Spring Boot 2.x
│ ├── spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports # Spring Boot 3.x
│ └── additional-spring-configuration-metadata.json
对于新项目,强烈建议直接使用 Spring Boot 3.x 的新方式,不仅加载效率更高,而且对于模块化和类路径扫描也有更好的支持。
8.4 如何使用日志切面 Starter
8.4.1 添加依赖
在需要使用日志切面的 Spring Boot 项目中添加依赖:
<dependency>
<groupId>com.example</groupId>
<artifactId>log-aspect-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
8.4.2 注解方式使用
在需要记录日志的方法上添加 @LogRecord
注解:
import com.example.logaspect.annotation.LogRecord;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
@LogRecord(description = "获取用户信息")
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// 方法实现
return userService.getUserById(id);
}
@LogRecord(
description = "创建用户",
level = LogRecord.LogLevel.DEBUG,
recordResult = false
)
@PostMapping
public User createUser(@RequestBody User user) {
// 方法实现
return userService.createUser(user);
}
}
8.4.3 非注解方式使用(全局切面)
在 application.properties
或 application.yml
中启用全局日志切面:
# 启用日志切面
logging.aspect.enabled=true
# 启用全局日志切面
logging.aspect.enable-global-aspect=true
# 设置全局切入点表达式,这里记录 service 包下所有方法的日志
logging.aspect.global-pointcut=execution(* com.example.service..*.*(..))
# 排除不需要记录日志的包
logging.aspect.exclude-packages[0]=com.example.common
logging.aspect.exclude-packages[1]=com.example.util
# 配置日志记录选项
logging.aspect.record-params=true
logging.aspect.record-result=true
logging.aspect.record-execution-time=true
8.5 日志输出示例
注解方式日志输出
2023-08-10 10:15:23.456 INFO --- [http-nio-8080-exec-1] com.example.DefaultLogRecordServiceImpl : 开始执行方法: com.example.controller.UserController.getUser | 描述: 获取用户信息 | 入参: [1]
2023-08-10 10:15:23.678 INFO --- [http-nio-8080-exec-1] com.example.DefaultLogRecordServiceImpl : 方法执行完成: com.example.controller.UserController.getUser | 耗时: 222毫秒 | 描述: 获取用户信息 | 返回值: {"id":1,"name":"张三","age":30,"email":"zhangsan@example.com"}
全局方式日志输出
2023-08-10 10:16:45.123 INFO --- [http-nio-8080-exec-2] com.example.DefaultLogRecordServiceImpl : 开始执行方法: com.example.service.impl.UserServiceImpl.getUserById | 入参: [2]
2023-08-10 10:16:45.234 INFO --- [http-nio-8080-exec-2] com.example.DefaultLogRecordServiceImpl : 方法执行完成: com.example.service.impl.UserServiceImpl.getUserById | 耗时: 111毫秒 | 返回值: {"id":2,"name":"李四","age":25,"email":"lisi@example.com"}
九、bug 解决
9.1 日志切面无法生效
如果日志切面无法生效,请检查以下几点:
- 确保日志切面类被正确扫描到
- 注意 springboot 版本,springboot 3.x 和 2.x 的配置方式有所不同(见 8.3)
9.2 log.debug 无输出内容
- springboot 中的 slf4j 框架默认的日志级别为 info,debug 日志级别低于 info
- 需要配置日志级别为 debug
logging.level.root=debug