前言:Spring Boot 的时代意义与价值
在当今快速迭代的软件开发环境中,开发效率和应用性能成为企业选择技术栈的关键因素。Spring Boot 作为 Spring 生态系统的革命性产品,彻底改变了 Java 企业级应用的开发方式。
为什么 Spring Boot 能够成为 Java 开发的事实标准?
传统 Spring 开发的痛点:
- 复杂的 XML 配置,配置文件的体积往往超过业务代码
- 依赖管理困难,版本冲突频发
- 应用部署繁琐,需要外部 Web 容器
- 开发环境搭建耗时,新成员上手成本高
- 测试困难,环境依赖复杂
Spring Boot 的解决方案:
- 零 XML 配置,基于注解和约定
- 起步依赖(Starters)自动管理依赖版本
- 内嵌 Web 容器,打包即可运行
- 自动配置,开箱即用
- 强大的生产就绪功能
一、Spring Boot 架构深度解析
1.1 Spring Boot 整体架构设计
1.2 核心组件协同工作流程
1.3 Spring Boot 启动过程源码级分析
// SpringApplication.run() 方法执行流程
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
// 1. 启动计时器
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 2. 创建应用上下文
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
// 3. 获取SpringApplicationRunListeners
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
// 4. 准备环境配置
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 5. 打印Banner
Banner printedBanner = printBanner(environment);
// 6. 创建应用上下文
context = createApplicationContext();
// 7. 准备上下文
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 8. 刷新上下文(核心步骤)
refreshContext(context);
// 9. 后置处理
afterRefresh(context, applicationArguments);
// 10. 启动完成
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
} catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
} catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
}
二、配置文件系统深度解析
2.1 YAML 配置语言的完整语法规范
YAML(YAML Ain’t Markup Language)是一种人性化的数据序列化标准,特别适合配置文件。
2.1.1 基础数据结构
# 标量类型(Scalars)
string: "Hello World"
integer: 42
float: 3.14159
boolean: true
null_value: null
date: 2023-12-20
datetime: 2023-12-20T10:30:00Z
# 集合类型(Collections)
list:
- "Apple"
- "Banana"
- "Cherry"
map:
key1: "value1"
key2: "value2"
nested:
subkey: "subvalue"
# 复杂嵌套结构
server:
port: 8080
servlet:
context-path: /api
tomcat:
max-threads: 200
connection-timeout: 5000
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
2.1.2 高级 YAML 特性
# 锚点和别名(避免重复)
defaults: &database-defaults
pool-size: 10
timeout: 30000
test-on-borrow: true
primary-db:
<<: *database-defaults
url: jdbc:mysql://primary/db
replica-db:
<<: *database-defaults
url: jdbc:mysql://replica/db
# 多文档支持(--- 分隔符)
---
spring:
profiles: development
datasource:
url: jdbc:h2:mem:testdb
---
spring:
profiles: production
datasource:
url: jdbc:mysql://prod-server/db
2.2 配置属性绑定的完整解决方案
2.2.1 复杂配置属性绑定
@Configuration
@ConfigurationProperties(prefix = "app.system")
@Validated
@Data
@ToString
public class SystemConfigProperties {
// 基础类型绑定
@NotBlank
private String name;
@Min(1)
@Max(65535)
private int port;
// 集合类型绑定
private List<String> whitelist = new ArrayList<>();
private Map<String, String> metadata = new HashMap<>();
// 嵌套对象绑定
private Database database = new Database();
private Cache cache = new Cache();
private Security security = new Security();
// 枚举类型绑定
private Environment environment = Environment.DEVELOPMENT;
// 持续时间绑定(Spring Boot 特殊支持)
@DurationUnit(ChronoUnit.SECONDS)
private Duration timeout = Duration.ofSeconds(30);
@Data
public static class Database {
private String url;
private String username;
private String password;
private Pool pool = new Pool();
@Data
public static class Pool {
@Min(1)
private int maxSize = 20;
private Duration maxWait = Duration.ofSeconds(30);
}
}
@Data
public static class Cache {
private CacheType type = CacheType.REDIS;
private Duration ttl = Duration.ofHours(1);
private Redis redis = new Redis();
public enum CacheType {
REDIS, MEMORY, EHCACHE
}
@Data
public static class Redis {
private String host = "localhost";
private int port = 6379;
private int database = 0;
}
}
@Data
public static class Security {
private boolean enabled = true;
private Jwt jwt = new Jwt();
private Cors cors = new Cors();
@Data
public static class Jwt {
private String secret;
private Duration expiration = Duration.ofHours(2);
}
@Data
public static class Cors {
private List<String> allowedOrigins = Arrays.asList("*");
private List<String> allowedMethods = Arrays.asList("*");
private List<String> allowedHeaders = Arrays.asList("*");
}
}
public enum Environment {
DEVELOPMENT, TESTING, STAGING, PRODUCTION
}
}
对应的 YAML 配置:
app:
system:
name: "订单管理系统"
port: 8080
environment: PRODUCTION
timeout: 60s
whitelist:
- "192.168.1.100"
- "192.168.1.101"
metadata:
version: "1.0.0"
owner: "架构组"
database:
url: "jdbc:mysql://localhost:3306/order_db"
username: "app_user"
password: "encrypted_password"
pool:
max-size: 50
max-wait: 60s
cache:
type: REDIS
ttl: 2h
redis:
host: "redis-cluster.example.com"
port: 6379
database: 1
security:
enabled: true
jwt:
secret: "jwt-signing-key-2023"
expiration: 4h
cors:
allowed-origins:
- "https://example.com"
- "https://api.example.com"
allowed-methods:
- "GET"
- "POST"
- "PUT"
- "DELETE"
2.2.2 配置属性验证的最佳实践
@Component
@ConfigurationProperties(prefix = "app.validation")
@Validated
@Data
public class ValidationProperties {
@NotNull
@Pattern(regexp = "^[a-zA-Z0-9_-]{3,50}$")
private String applicationCode;
@Email
@NotBlank
private String adminEmail;
@Valid
private RateLimit rateLimit = new RateLimit();
@Valid
private FileUpload fileUpload = new FileUpload();
@Data
public static class RateLimit {
@Min(1)
private int requestsPerMinute = 100;
@Min(1)
private int burstCapacity = 50;
@AssertTrue(message = "突发容量必须小于等于每分钟请求数")
public boolean isBurstCapacityValid() {
return burstCapacity <= requestsPerMinute;
}
}
@Data
public static class FileUpload {
@Min(1024)
@Max(10485760) // 10MB
private long maxFileSize = 5242880; // 5MB
@Min(1)
@Max(20)
private int maxFilesPerRequest = 5;
private List<String> allowedExtensions = Arrays.asList(
"jpg", "jpeg", "png", "pdf", "doc", "docx"
);
@AssertTrue(message = "必须至少允许一种文件扩展名")
public boolean isExtensionsValid() {
return allowedExtensions != null && !allowedExtensions.isEmpty();
}
}
}
2.3 配置文件加载机制的完整流程
graph TB
A[配置加载开始] --> B[加载默认属性]
B --> C[加载application.yml/properties]
C --> D[加载profile特定配置]
D --> E[加载OS环境变量]
E --> F[加载Java系统属性]
F --> G[加载命令行参数]
G --> H[配置属性合并]
H --> I[属性值转换]
I --> J[属性验证]
J --> K[绑定到@ConfigurationProperties]
K --> L[配置完成]
M[PropertySource顺序] --> N[1. 命令行参数]
N --> O[2. SPRING_APPLICATION_JSON]
O --> P[3. ServletConfig参数]
P --> Q[4. ServletContext参数]
Q --> R[5. JNDI属性]
R --> S[6. Java系统属性]
S --> T[7. OS环境变量]
T --> U[8. random.*属性]
U --> V[9. Profile特定配置文件]
V --> W[10. 应用配置文件]
W --> X[11. @PropertySource注解]
X --> Y[12. 默认属性]
三、请求参数校验的完整体系
3.1 校验注解的完整分类与使用场景
3.1.1 JSR-303 标准注解详解
| 分类 | 注解 | 适用类型 | 说明 | 示例 |
|---|---|---|---|---|
| 空值检查 | @NotNull | 任意 | 不能为null | @NotNull String name |
@NotEmpty | 字符串/集合 | 不能为空 | @NotEmpty List items | |
@NotBlank | 字符串 | 非空且非空白 | @NotBlank String username | |
| 布尔检查 | @AssertTrue | Boolean | 必须为true | @AssertTrue Boolean active |
@AssertFalse | Boolean | 必须为false | @AssertFalse Boolean deleted | |
| 数值检查 | @Min | 数值 | 最小值 | @Min(18) Integer age |
@Max | 数值 | 最大值 | @Max(100) Integer age | |
@DecimalMin | BigDecimal | 小数最小值 | @DecimalMin("0.0") | |
@DecimalMax | BigDecimal | 小数最大值 | @DecimalMax("100.0") | |
@Digits | 数值 | 数字位数 | @Digits(integer=3, fraction=2) | |
@Positive | 数值 | 正数 | @Positive Integer count | |
@PositiveOrZero | 数值 | 正数或零 | @PositiveOrZero Integer count | |
@Negative | 数值 | 负数 | @Negative Integer balance | |
@NegativeOrZero | 数值 | 负数或零 | @NegativeOrZero Integer balance | |
| 字符串检查 | @Size | 字符串/集合 | 大小范围 | @Size(min=2, max=50) |
@Pattern | 字符串 | 正则表达式 | @Pattern(regexp="^[a-z]+$") | |
| 日期检查 | @Past | 日期 | 过去日期 | @Past Date birthDate |
@PastOrPresent | 日期 | 过去或现在 | @PastOrPresent Date createTime | |
@Future | 日期 | 将来日期 | @Future Date expireDate | |
@FutureOrPresent | 日期 | 将来或现在 | @FutureOrPresent Date startDate |
3.1.2 Hibernate Validator 扩展注解
| 分类 | 注解 | 说明 | 示例 |
|---|---|---|---|
| 字符串验证 | @Length | 字符串长度 | @Length(min=5, max=20) |
@Email | 邮箱格式 | @Email String email | |
@URL | URL格式 | @URL String website | |
@SafeHtml | 安全HTML | @SafeHtml String content | |
| 数值验证 | @Range | 数值范围 | @Range(min=1, max=100) |
@Mod10Check | 模10校验 | @Mod10Check String code | |
@Mod11Check | 模11校验 | @Mod11Check String code | |
| 专业验证 | @CreditCardNumber | 信用卡号 | @CreditCardNumber String card |
@ISBN | ISBN号 | @ISBN String isbn | |
@EAN | EAN码 | @EAN String ean | |
@CNPJ | 巴西CNPJ | @CNPJ String cnpj | |
@CPF | 巴西CPF | @CPF String cpf |
3.2 复杂业务场景的校验实战
3.2.1 用户注册完整校验示例
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserRegistrationDTO {
// 基本信息校验
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 128, message = "密码长度必须在8-128个字符之间")
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]+$",
message = "密码必须包含至少一个大写字母、一个小写字母、一个数字和一个特殊字符"
)
private String password;
// 联系信息校验
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
// 个人资料校验
@NotBlank(message = "真实姓名不能为空")
@Size(min = 2, max = 10, message = "真实姓名长度必须在2-10个字符之间")
private String realName;
@Min(value = 18, message = "年龄必须大于等于18岁")
@Max(value = 100, message = "年龄必须小于等于100岁")
private Integer age;
@Pattern(regexp = "^(男|女|其他)$", message = "性别只能是男、女或其他")
private String gender;
@Past(message = "出生日期必须是过去的时间")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate birthDate;
// 地址信息校验(嵌套校验)
@Valid
@NotNull(message = "地址信息不能为空")
private AddressDTO address;
// 业务规则校验
@AssertTrue(message = "必须同意用户协议")
private Boolean agreedToTerms;
// 自定义业务规则校验方法
@AssertTrue(message = "年龄和出生日期不匹配")
public boolean isAgeMatchesBirthDate() {
if (birthDate == null || age == null) {
return true;
}
return Period.between(birthDate, LocalDate.now()).getYears() == age;
}
// 嵌套地址DTO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class AddressDTO {
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
@NotBlank(message = "区县不能为空")
private String district;
@NotBlank(message = "详细地址不能为空")
@Size(max = 200, message = "详细地址不能超过200个字符")
private String detail;
@Pattern(regexp = "^\\d{6}$", message = "邮政编码格式不正确")
private String postalCode;
}
}
3.2.2 商品订单复杂校验示例
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderCreateDTO {
// 订单基础信息
@NotBlank(message = "订单标题不能为空")
@Size(max = 100, message = "订单标题不能超过100个字符")
private String title;
@NotNull(message = "订单类型不能为空")
private OrderType type;
// 时间相关校验
@Future(message = "预约时间必须是将来时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime appointmentTime;
// 金额相关校验
@NotNull(message = "订单金额不能为空")
@DecimalMin(value = "0.01", message = "订单金额必须大于0")
@Digits(integer = 10, fraction = 2, message = "订单金额格式不正确")
private BigDecimal amount;
@DecimalMin(value = "0.00", message = "折扣金额不能为负数")
@Digits(integer = 10, fraction = 2, message = "折扣金额格式不正确")
private BigDecimal discountAmount;
// 集合校验
@Valid
@NotEmpty(message = "订单商品不能为空")
@Size(max = 50, message = "单次订单最多包含50个商品")
private List<OrderItemDTO> items;
// 支付信息校验
@Valid
@NotNull(message = "支付信息不能为空")
private PaymentInfoDTO paymentInfo;
// 自定义业务校验
@AssertTrue(message = "折扣金额不能超过订单金额")
public boolean isDiscountValid() {
if (amount == null || discountAmount == null) {
return true;
}
return discountAmount.compareTo(amount) <= 0;
}
@AssertTrue(message = "预约订单必须指定预约时间")
public boolean isAppointmentTimeValid() {
if (type == OrderType.APPOINTMENT) {
return appointmentTime != null;
}
return true;
}
// 嵌套DTO定义
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class OrderItemDTO {
@NotBlank(message = "商品ID不能为空")
private String productId;
@NotBlank(message = "商品名称不能为空")
@Size(max = 50, message = "商品名称不能超过50个字符")
private String productName;
@NotNull(message = "商品数量不能为空")
@Min(value = 1, message = "商品数量必须大于0")
@Max(value = 999, message = "单商品数量不能超过999")
private Integer quantity;
@NotNull(message = "商品单价不能为空")
@DecimalMin(value = "0.01", message = "商品单价必须大于0")
@Digits(integer = 10, fraction = 2, message = "商品单价格式不正确")
private BigDecimal unitPrice;
// 业务校验:单价 * 数量 = 总价
@AssertTrue(message = "商品总价计算不正确")
public boolean isTotalPriceValid() {
if (unitPrice == null || quantity == null) {
return true;
}
BigDecimal expectedTotal = unitPrice.multiply(BigDecimal.valueOf(quantity));
// 这里假设有totalPrice字段,实际项目中根据情况调整
return true;
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PaymentInfoDTO {
@NotNull(message = "支付方式不能为空")
private PaymentMethod paymentMethod;
// 条件校验:信用卡支付需要卡号
@NotBlank(message = "信用卡号不能为空", groups = CreditCardValidation.class)
private String cardNumber;
// 条件校验组接口
public interface CreditCardValidation {}
// 条件校验方法
@AssertTrue(message = "信用卡支付必须提供卡号", groups = CreditCardValidation.class)
public boolean isCardNumberRequired() {
if (paymentMethod == PaymentMethod.CREDIT_CARD) {
return cardNumber != null && !cardNumber.trim().isEmpty();
}
return true;
}
}
public enum OrderType {
NORMAL, APPOINTMENT, URGENT
}
public enum PaymentMethod {
ALIPAY, WECHAT, CREDIT_CARD, BANK_TRANSFER
}
}
3.3 自定义校验注解的完整实现
3.3.1 手机号校验注解
/**
* 手机号校验注解
* 支持中国大陆手机号格式
*/
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 是否允许为空
boolean nullable() default false;
// 运营商限制
Operator[] operators() default {Operator.CHINA_MOBILE, Operator.CHINA_UNICOM, Operator.CHINA_TELECOM};
public enum Operator {
CHINA_MOBILE, // 中国移动
CHINA_UNICOM, // 中国联通
CHINA_TELECOM // 中国电信
}
}
3.3.2 手机号校验器实现
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
private boolean nullable;
private Phone.Operator[] operators;
// 运营商号段定义
private static final Set<String> CHINA_MOBILE_PREFIXES = Set.of(
"134", "135", "136", "137", "138", "139", "147", "148", "150",
"151", "152", "157", "158", "159", "165", "172", "178", "182",
"183", "184", "187", "188", "195", "197", "198"
);
private static final Set<String> CHINA_UNICOM_PREFIXES = Set.of(
"130", "131", "132", "140", "145", "146", "155", "156", "166",
"167", "171", "175", "176", "185", "186", "196"
);
private static final Set<String> CHINA_TELECOM_PREFIXES = Set.of(
"133", "1349", "141", "149", "153", "162", "173", "174", "177",
"180", "181", "189", "190", "191", "193", "199"
);
@Override
public void initialize(Phone constraintAnnotation) {
this.nullable = constraintAnnotation.nullable();
this.operators = constraintAnnotation.operators();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 处理空值
if (value == null || value.trim().isEmpty()) {
return nullable;
}
// 基础格式校验
if (!PHONE_PATTERN.matcher(value).matches()) {
return false;
}
// 运营商校验
if (operators.length > 0) {
String prefix = value.substring(0, 3);
String prefix4 = value.substring(0, 4);
boolean validOperator = false;
for (Phone.Operator operator : operators) {
switch (operator) {
case CHINA_MOBILE:
if (CHINA_MOBILE_PREFIXES.contains(prefix)) {
validOperator = true;
}
break;
case CHINA_UNICOM:
if (CHINA_UNICOM_PREFIXES.contains(prefix)) {
validOperator = true;
}
break;
case CHINA_TELECOM:
if (CHINA_TELECOM_PREFIXES.contains(prefix) ||
CHINA_TELECOM_PREFIXES.contains(prefix4)) {
validOperator = true;
}
break;
}
if (validOperator) break;
}
if (!validOperator) {
return false;
}
}
return true;
}
}
3.3.3 身份证号校验注解
/**
* 身份证号校验注解
* 支持15位和18位身份证号格式和校验码验证
*/
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdCardValidator.class)
public @interface IdCard {
String message() default "身份证号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 是否进行严格校验(校验码验证)
boolean strict() default true;
}
3.3.4 身份证号校验器实现
public class IdCardValidator implements ConstraintValidator<IdCard, String> {
private static final Pattern ID_CARD_PATTERN =
Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$|^[1-9]\\d{5}\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{2}[0-9Xx]$");
private static final int[] WEIGHT = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
private static final char[] VERIFY_CODE = {'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'};
private boolean strict;
@Override
public void initialize(IdCard constraintAnnotation) {
this.strict = constraintAnnotation.strict();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.trim().isEmpty()) {
return false;
}
// 基础格式校验
if (!ID_CARD_PATTERN.matcher(value).matches()) {
return false;
}
// 严格模式下的校验码验证(仅对18位身份证)
if (strict && value.length() == 18) {
return verifyCheckCode(value);
}
return true;
}
private boolean verifyCheckCode(String idCard) {
if (idCard == null || idCard.length() != 18) {
return false;
}
char[] chars = idCard.toCharArray();
int sum = 0;
for (int i = 0; i < 17; i++) {
sum += (chars[i] - '0') * WEIGHT[i];
}
char expectedCode = VERIFY_CODE[sum % 11];
return Character.toUpperCase(chars[17]) == expectedCode;
}
}
3.4 分组校验和条件校验
3.4.1 校验分组定义
public class ValidationGroups {
// 创建时的校验规则
public interface Create {}
// 更新时的校验规则
public interface Update {}
// 删除时的校验规则
public interface Delete {}
// 查询时的校验规则
public interface Query {}
}
// 使用分组校验
@Data
public class UserDTO {
@NotNull(groups = {ValidationGroups.Update.class, ValidationGroups.Delete.class})
private Long id;
@NotBlank(groups = ValidationGroups.Create.class)
@Size(min = 3, max = 20, groups = {ValidationGroups.Create.class, ValidationGroups.Update.class})
private String username;
@NotBlank(groups = ValidationGroups.Create.class)
@Email(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class})
private String email;
}
// Controller中使用
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
@PostMapping
public ResponseEntity<UserDTO> createUser(@Validated(ValidationGroups.Create.class) @RequestBody UserDTO user) {
// 创建用户逻辑
return ResponseEntity.ok(user);
}
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Validated(ValidationGroups.Update.class) @RequestBody UserDTO user) {
// 更新用户逻辑
return ResponseEntity.ok(user);
}
}
四、全局异常处理的完整体系
4.1 异常处理架构设计
4.2 完整的异常处理实现
4.2.1 错误码枚举定义
public enum ErrorCode {
// 系统错误
SUCCESS(0, "成功"),
SYSTEM_ERROR(10001, "系统错误"),
SERVICE_UNAVAILABLE(10002, "服务暂不可用"),
// 参数错误
PARAMETER_ERROR(20001, "参数错误"),
PARAMETER_VALIDATION_ERROR(20002, "参数校验失败"),
PARAMETER_MISSING(20003, "参数缺失"),
PARAMETER_FORMAT_ERROR(20004, "参数格式错误"),
// 业务错误
BUSINESS_ERROR(30001, "业务错误"),
DATA_NOT_FOUND(30002, "数据不存在"),
DATA_ALREADY_EXISTS(30003, "数据已存在"),
OPERATION_NOT_ALLOWED(30004, "操作不允许"),
// 权限错误
UNAUTHORIZED(40001, "未授权"),
FORBIDDEN(40002, "禁止访问"),
TOKEN_EXPIRED(40003, "令牌已过期"),
TOKEN_INVALID(40004, "令牌无效"),
// 外部服务错误
EXTERNAL_SERVICE_ERROR(50001, "外部服务错误"),
EXTERNAL_SERVICE_TIMEOUT(50002, "外部服务超时");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public static ErrorCode fromCode(int code) {
for (ErrorCode errorCode : values()) {
if (errorCode.code == code) {
return errorCode;
}
}
return SYSTEM_ERROR;
}
}
4.2.2 统一错误响应格式
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {
/**
* 错误码
*/
private int code;
/**
* 错误信息
*/
private String message;
/**
* 错误详情
*/
private String detail;
/**
* 请求路径
*/
private String path;
/**
* 请求ID(用于链路追踪)
*/
private String requestId;
/**
* 时间戳
*/
private LocalDateTime timestamp;
/**
* 错误堆栈(仅开发环境显示)
*/
private String stackTrace;
/**
* 子错误信息(用于复杂错误)
*/
private List<SubError> subErrors;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SubError {
private String field;
private String message;
private Object rejectedValue;
}
/**
* 成功响应
*/
public static ErrorResponse success() {
return ErrorResponse.builder()
.code(ErrorCode.SUCCESS.getCode())
.message(ErrorCode.SUCCESS.getMessage())
.timestamp(LocalDateTime.now())
.build();
}
/**
* 构建错误响应
*/
public static ErrorResponse of(ErrorCode errorCode, String detail) {
return ErrorResponse.builder()
.code(errorCode.getCode())
.message(errorCode.getMessage())
.detail(detail)
.timestamp(LocalDateTime.now())
.build();
}
/**
* 构建错误响应(带子错误)
*/
public static ErrorResponse of(ErrorCode errorCode, String detail, List<SubError> subErrors) {
return ErrorResponse.builder()
.code(errorCode.getCode())
.message(errorCode.getMessage())
.detail(detail)
.timestamp(LocalDateTime.now())
.subErrors(subErrors)
.build();
}
}
4.2.3 自定义业务异常
/**
* 基础业务异常
*/
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
private final Map<String, Object> context;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public BusinessException(ErrorCode errorCode, String message, Map<String, Object> context) {
super(message);
this.errorCode = errorCode;
this.context = context != null ? new HashMap<>(context) : new HashMap<>();
}
public BusinessException(ErrorCode errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public ErrorCode getErrorCode() {
return errorCode;
}
public Map<String, Object> getContext() {
return Collections.unmodifiableMap(context);
}
public BusinessException withContext(String key, Object value) {
this.context.put(key, value);
return this;
}
}
/**
* 数据不存在异常
*/
public class DataNotFoundException extends BusinessException {
public DataNotFoundException(String entityName, Object id) {
super(ErrorCode.DATA_NOT_FOUND,
String.format("%s不存在: %s", entityName, id));
withContext("entity", entityName)
.withContext("id", id);
}
}
/**
* 参数校验异常
*/
public class ParameterValidationException extends BusinessException {
private final List<ErrorResponse.SubError> validationErrors;
public ParameterValidationException(List<ErrorResponse.SubError> validationErrors) {
super(ErrorCode.PARAMETER_VALIDATION_ERROR, "参数校验失败");
this.validationErrors = validationErrors;
}
public List<ErrorResponse.SubError> getValidationErrors() {
return validationErrors;
}
}
4.2.4 全局异常处理器
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
private final Environment environment;
public GlobalExceptionHandler(Environment environment) {
this.environment = environment;
}
/**
* 参数校验异常处理
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(
MethodArgumentNotValidException ex,
HttpServletRequest request) {
List<ErrorResponse.SubError> subErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> ErrorResponse.SubError.builder()
.field(error.getField())
.message(error.getDefaultMessage())
.rejectedValue(error.getRejectedValue())
.build())
.collect(Collectors.toList());
ErrorResponse errorResponse = ErrorResponse.builder()
.code(ErrorCode.PARAMETER_VALIDATION_ERROR.getCode())
.message(ErrorCode.PARAMETER_VALIDATION_ERROR.getMessage())
.detail("请求参数校验失败")
.path(request.getRequestURI())
.requestId(getRequestId(request))
.timestamp(LocalDateTime.now())
.subErrors(subErrors)
.build();
log.warn("参数校验失败: {}", subErrors);
return ResponseEntity.badRequest().body(errorResponse);
}
/**
* 约束校验异常处理(@Validated在方法参数上的校验)
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleConstraintViolationException(
ConstraintViolationException ex,
HttpServletRequest request) {
List<ErrorResponse.SubError> subErrors = ex.getConstraintViolations()
.stream()
.map(violation -> ErrorResponse.SubError.builder()
.field(getFieldNameFromViolation(violation))
.message(violation.getMessage())
.rejectedValue(violation.getInvalidValue())
.build())
.collect(Collectors.toList());
ErrorResponse errorResponse = ErrorResponse.builder()
.code(ErrorCode.PARAMETER_VALIDATION_ERROR.getCode())
.message(ErrorCode.PARAMETER_VALIDATION_ERROR.getMessage())
.detail("参数约束校验失败")
.path(request.getRequestURI())
.requestId(getRequestId(request))
.timestamp(LocalDateTime.now())
.subErrors(subErrors)
.build();
log.warn("参数约束校验失败: {}", subErrors);
return ResponseEntity.badRequest().body(errorResponse);
}
/**
* 业务异常处理
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
BusinessException ex,
HttpServletRequest request) {
ErrorResponse errorResponse = ErrorResponse.builder()
.code(ex.getErrorCode().getCode())
.message(ex.getMessage())
.detail("业务处理失败")
.path(request.getRequestURI())
.requestId(getRequestId(request))
.timestamp(LocalDateTime.now())
.build();
log.warn("业务异常: {}", ex.getMessage(), ex);
return ResponseEntity.status(getHttpStatus(ex.getErrorCode())).body(errorResponse);
}
/**
* 数据访问异常处理
*/
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ErrorResponse> handleDataAccessException(
DataAccessException ex,
HttpServletRequest request) {
ErrorResponse errorResponse = ErrorResponse.builder()
.code(ErrorCode.SYSTEM_ERROR.getCode())
.message("数据访问异常")
.detail("数据操作过程中发生异常")
.path(request.getRequestURI())
.requestId(getRequestId(request))
.timestamp(LocalDateTime.now())
.stackTrace(isDevelopment() ? getStackTrace(ex) : null)
.build();
log.error("数据访问异常: {}", ex.getMessage(), ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
/**
* 全局异常处理
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex,
HttpServletRequest request) {
ErrorResponse errorResponse = ErrorResponse.builder()
.code(ErrorCode.SYSTEM_ERROR.getCode())
.message("系统内部错误")
.detail("服务器遇到意外错误")
.path(request.getRequestURI())
.requestId(getRequestId(request))
.timestamp(LocalDateTime.now())
.stackTrace(isDevelopment() ? getStackTrace(ex) : null)
.build();
log.error("系统异常: {}", ex.getMessage(), ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
// 辅助方法
private String getFieldNameFromViolation(ConstraintViolation<?> violation) {
String path = violation.getPropertyPath().toString();
return path.contains(".") ?
path.substring(path.lastIndexOf(".") + 1) : path;
}
private String getRequestId(HttpServletRequest request) {
Object requestId = request.getAttribute("X-Request-ID");
return requestId != null ? requestId.toString() :
UUID.randomUUID().toString().replace("-", "");
}
private boolean isDevelopment() {
return Arrays.stream(environment.getActiveProfiles())
.anyMatch(profile -> profile.equals("dev") || profile.equals("development"));
}
private String getStackTrace(Throwable ex) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
return sw.toString();
}
private HttpStatus getHttpStatus(ErrorCode errorCode) {
switch (errorCode) {
case UNAUTHORIZED:
return HttpStatus.UNAUTHORIZED;
case FORBIDDEN:
return HttpStatus.FORBIDDEN;
case DATA_NOT_FOUND:
return HttpStatus.NOT_FOUND;
case PARAMETER_ERROR:
case PARAMETER_VALIDATION_ERROR:
case PARAMETER_MISSING:
case PARAMETER_FORMAT_ERROR:
return HttpStatus.BAD_REQUEST;
default:
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
}
五、定时任务系统深度解析
5.1 Spring Task 架构设计
graph TB
A[定时任务入口] --> B[@EnableScheduling]
B --> C[ScheduledAnnotationBeanPostProcessor]
C --> D[任务注册]
D --> E[Cron任务]
D --> F[固定延迟任务]
D --> G[固定速率任务]
E --> H[TaskScheduler]
F --> H
G --> H
H --> I[ThreadPoolTaskScheduler]
H --> J[ConcurrentTaskScheduler]
I --> K[任务执行]
J --> K
K --> L[业务逻辑]
K --> M[异常处理]
K --> N[执行监控]
subgraph "任务类型"
O[Cron表达式]
P[Fixed Delay]
Q[Fixed Rate]
R[Initial Delay]
end
subgraph "线程池配置"
S[核心线程数]
T[最大线程数]
U[队列容量]
V[拒绝策略]
end
5.2 定时任务完整实现
5.2.1 基础定时任务配置
@Configuration
@EnableScheduling
@EnableAsync
public class SchedulingConfig implements SchedulingConfigurer, AsyncConfigurer {
private static final int SCHEDULER_POOL_SIZE = 10;
private static final int ASYNC_POOL_SIZE = 20;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskScheduler());
}
@Bean(destroyMethod = "shutdown")
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(SCHEDULER_POOL_SIZE);
scheduler.setThreadNamePrefix("scheduled-task-");
scheduler.setAwaitTerminationSeconds(60);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setErrorHandler(t -> {
log.error("定时任务执行异常: ", t);
});
scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
scheduler.initialize();
return scheduler;
}
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(ASYNC_POOL_SIZE);
executor.setMaxPoolSize(ASYNC_POOL_SIZE * 2);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("异步任务执行异常, method: {}, params: {}", method.getName(), params, ex);
};
}
}
5.2.2 复杂业务定时任务实现
@Slf4j
@Component
public class BusinessScheduledTasks {
@Autowired
private OrderService orderService;
@Autowired
private UserService userService;
@Autowired
private SystemMonitorService monitorService;
@Autowired
private MetricsCollector metricsCollector;
/**
* 订单超时取消任务
* 每5分钟执行一次,处理30分钟未支付的订单
*/
@Scheduled(cron = "0 */5 * * * ?")
@SchedulerLock(name = "orderTimeoutTask", lockAtMostFor = "10m", lockAtLeastFor = "1m")
public void processOrderTimeout() {
log.info("开始执行订单超时取消任务");
long startTime = System.currentTimeMillis();
try {
int processedCount = orderService.cancelTimeoutOrders(Duration.ofMinutes(30));
metricsCollector.recordScheduledTaskSuccess("orderTimeoutTask");
log.info("订单超时取消任务执行完成,处理订单数量: {}, 耗时: {}ms",
processedCount, System.currentTimeMillis() - startTime);
} catch (Exception e) {
metricsCollector.recordScheduledTaskFailure("orderTimeoutTask");
log.error("订单超时取消任务执行失败", e);
throw e; // 重新抛出异常,确保任务标记为失败
}
}
/**
* 用户数据统计任务
* 每天凌晨2点执行
*/
@Scheduled(cron = "0 0 2 * * ?")
@SchedulerLock(name = "userStatisticsTask", lockAtMostFor = "1h", lockAtLeastFor = "30m")
public void generateUserStatistics() {
log.info("开始执行用户数据统计任务");
long startTime = System.currentTimeMillis();
try {
UserStatistics statistics = userService.generateDailyStatistics(LocalDate.now().minusDays(1));
metricsCollector.recordUserStatistics(statistics);
metricsCollector.recordScheduledTaskSuccess("userStatisticsTask");
log.info("用户数据统计任务执行完成,统计日期: {}, 耗时: {}ms",
statistics.getStatisticsDate(), System.currentTimeMillis() - startTime);
} catch (Exception e) {
metricsCollector.recordScheduledTaskFailure("userStatisticsTask");
log.error("用户数据统计任务执行失败", e);
throw e;
}
}
/**
* 系统健康检查任务
* 每30秒执行一次,固定速率
*/
@Scheduled(fixedRate = 30000)
public void systemHealthCheck() {
SystemHealth health = monitorService.checkSystemHealth();
if (health.getStatus() == SystemHealth.Status.DOWN) {
log.error("系统健康检查失败: {}", health.getDetails());
// 发送告警通知
monitorService.sendAlert(health);
} else if (health.getStatus() == SystemHealth.Status.DEGRADED) {
log.warn("系统健康检查降级: {}", health.getDetails());
}
metricsCollector.recordSystemHealth(health);
}
/**
* 数据清理任务
* 每周日凌晨3点执行
*/
@Scheduled(cron = "0 0 3 ? * SUN")
@SchedulerLock(name = "dataCleanupTask", lockAtMostFor = "2h", lockAtLeastFor = "1h")
public void cleanupExpiredData() {
log.info("开始执行数据清理任务");
long startTime = System.currentTimeMillis();
try {
// 清理90天前的日志
int logCount = monitorService.cleanupExpiredLogs(LocalDateTime.now().minusDays(90));
// 清理30天前的临时文件
int fileCount = monitorService.cleanupTempFiles(LocalDateTime.now().minusDays(30));
// 清理过期的缓存数据
int cacheCount = monitorService.cleanupExpiredCache();
metricsCollector.recordScheduledTaskSuccess("dataCleanupTask");
log.info("数据清理任务执行完成,清理日志: {}条, 文件: {}个, 缓存: {}个, 总耗时: {}ms",
logCount, fileCount, cacheCount, System.currentTimeMillis() - startTime);
} catch (Exception e) {
metricsCollector.recordScheduledTaskFailure("dataCleanupTask");
log.error("数据清理任务执行失败", e);
throw e;
}
}
/**
* 异步报表生成任务
* 使用@Async实现异步执行
*/
@Async
@Scheduled(cron = "0 0 4 * * ?") // 每天凌晨4点
@SchedulerLock(name = "reportGenerationTask", lockAtMostFor = "2h")
public void generateDailyReports() {
log.info("开始异步执行日报表生成任务");
long startTime = System.currentTimeMillis();
try {
LocalDate reportDate = LocalDate.now().minusDays(1);
// 生成销售报表
SalesReport salesReport = orderService.generateSalesReport(reportDate);
// 生成用户行为报表
UserBehaviorReport behaviorReport = userService.generateBehaviorReport(reportDate);
// 生成系统性能报表
SystemPerformanceReport performanceReport = monitorService.generatePerformanceReport(reportDate);
// 发送报表邮件
monitorService.sendDailyReports(salesReport, behaviorReport, performanceReport);
metricsCollector.recordScheduledTaskSuccess("reportGenerationTask");
log.info("日报表生成任务执行完成,耗时: {}ms", System.currentTimeMillis() - startTime);
} catch (Exception e) {
metricsCollector.recordScheduledTaskFailure("reportGenerationTask");
log.error("日报表生成任务执行失败", e);
// 异步任务异常由AsyncUncaughtExceptionHandler处理
}
}
/**
* 动态配置的定时任务
* 从数据库读取配置执行
*/
@Scheduled(fixedDelayString = "${app.scheduling.config-refresh-interval:300000}")
public void refreshScheduledTasks() {
log.debug("开始刷新定时任务配置");
List<TaskConfig> taskConfigs = monitorService.getActiveTaskConfigs();
for (TaskConfig config : taskConfigs) {
if (config.isEnabled()) {
scheduleDynamicTask(config);
} else {
cancelDynamicTask(config);
}
}
}
private void scheduleDynamicTask(TaskConfig config) {
// 动态任务调度实现
log.info("调度动态任务: {}, Cron: {}", config.getTaskName(), config.getCronExpression());
}
private void cancelDynamicTask(TaskConfig config) {
// 动态任务取消实现
log.info("取消动态任务: {}", config.getTaskName());
}
}
5.2.3 分布式定时任务锁
@Component
public class DistributedSchedulerLock {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LOCK_PREFIX = "scheduler:lock:";
private static final Duration DEFAULT_LOCK_TIME = Duration.ofMinutes(10);
/**
* 尝试获取分布式锁
*/
public boolean tryLock(String lockName, Duration lockTime) {
String lockKey = LOCK_PREFIX + lockName;
String lockValue = generateLockValue();
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, lockTime != null ? lockTime : DEFAULT_LOCK_TIME);
return Boolean.TRUE.equals(acquired);
}
/**
* 释放分布式锁
*/
public void unlock(String lockName) {
String lockKey = LOCK_PREFIX + lockName;
redisTemplate.delete(lockKey);
}
/**
* 带锁执行任务
*/
public <T> T executeWithLock(String lockName, Duration lockTime, Supplier<T> task) {
if (!tryLock(lockName, lockTime)) {
log.info("任务 {} 正在其他节点执行,跳过本次执行", lockName);
return null;
}
try {
return task.get();
} finally {
unlock(lockName);
}
}
private String generateLockValue() {
return String.format("%s-%s-%d",
getLocalIp(),
UUID.randomUUID().toString().substring(0, 8),
System.currentTimeMillis());
}
private String getLocalIp() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
return "unknown";
}
}
}
// 自定义注解用于分布式锁
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SchedulerLock {
String name();
String lockAtMostFor() default "10m";
String lockAtLeastFor() default "1m";
}
// 切面实现
@Aspect
@Component
public class SchedulerLockAspect {
@Autowired
private DistributedSchedulerLock schedulerLock;
@Around("@annotation(lockAnnotation)")
public Object aroundScheduledTask(ProceedingJoinPoint joinPoint,
SchedulerLock lockAnnotation) throws Throwable {
String lockName = lockAnnotation.name();
Duration lockAtMostFor = parseDuration(lockAnnotation.lockAtMostFor());
Duration lockAtLeastFor = parseDuration(lockAnnotation.lockAtLeastFor());
if (!schedulerLock.tryLock(lockName, lockAtMostFor)) {
log.debug("任务 {} 已在其他节点执行,跳过", lockName);
return null;
}
try {
log.debug("成功获取锁 {},开始执行任务", lockName);
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;
// 如果执行时间小于最小锁定时长,等待至最小时间
if (executionTime < lockAtLeastFor.toMillis()) {
Thread.sleep(lockAtLeastFor.toMillis() - executionTime);
}
return result;
} finally {
schedulerLock.unlock(lockName);
log.debug("任务 {} 执行完成,释放锁", lockName);
}
}
private Duration parseDuration(String durationStr) {
if (durationStr.endsWith("ms")) {
return Duration.ofMillis(Long.parseLong(durationStr.substring(0, durationStr.length() - 2)));
} else if (durationStr.endsWith("s")) {
return Duration.ofSeconds(Long.parseLong(durationStr.substring(0, durationStr.length() - 1)));
} else if (durationStr.endsWith("m")) {
return Duration.ofMinutes(Long.parseLong(durationStr.substring(0, durationStr.length() - 1)));
} else if (durationStr.endsWith("h")) {
return Duration.ofHours(Long.parseLong(durationStr.substring(0, durationStr.length() - 1)));
} else {
return Duration.ofMinutes(Long.parseLong(durationStr));
}
}
}
5.3 定时任务监控和管理
5.3.1 任务执行监控
@Component
public class ScheduledTaskMonitor {
private final Map<String, TaskExecutionInfo> taskExecutionMap = new ConcurrentHashMap<>();
@EventListener
public void handleTaskStarted(ScheduledTaskStartedEvent event) {
String taskName = event.getTask().toString();
TaskExecutionInfo info = new TaskExecutionInfo();
info.setStartTime(System.currentTimeMillis());
info.setStatus(TaskExecutionInfo.Status.RUNNING);
taskExecutionMap.put(taskName, info);
log.info("定时任务开始执行: {}", taskName);
}
@EventListener
public void handleTaskFinished(ScheduledTaskFinishedEvent event) {
String taskName = event.getTask().toString();
TaskExecutionInfo info = taskExecutionMap.get(taskName);
if (info != null) {
info.setEndTime(System.currentTimeMillis());
info.setStatus(TaskExecutionInfo.Status.SUCCESS);
info.setExecutionTime(info.getEndTime() - info.getStartTime());
}
log.info("定时任务执行完成: {}, 耗时: {}ms", taskName, info.getExecutionTime());
}
@EventListener
public void handleTaskFailed(ScheduledTaskFailedEvent event) {
String taskName = event.getTask().toString();
TaskExecutionInfo info = taskExecutionMap.get(taskName);
if (info != null) {
info.setEndTime(System.currentTimeMillis());
info.setStatus(TaskExecutionInfo.Status.FAILED);
info.setExecutionTime(info.getEndTime() - info.getStartTime());
info.setError(event.getException().getMessage());
}
log.error("定时任务执行失败: {}", taskName, event.getException());
}
public Map<String, TaskExecutionInfo> getTaskExecutionInfo() {
return new HashMap<>(taskExecutionMap);
}
@Data
public static class TaskExecutionInfo {
private long startTime;
private long endTime;
private long executionTime;
private Status status;
private String error;
public enum Status {
RUNNING, SUCCESS, FAILED
}
}
}
5.3.2 定时任务管理端点
@RestController
@RequestMapping("/api/scheduler")
public class SchedulerManagementController {
@Autowired
private ScheduledTaskMonitor taskMonitor;
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
@GetMapping("/tasks")
public ResponseEntity<Map<String, Object>> getScheduledTasks() {
Map<String, Object> result = new HashMap<>();
result.put("taskExecutions", taskMonitor.getTaskExecutionInfo());
result.put("threadPool", getThreadPoolInfo());
result.put("timestamp", LocalDateTime.now());
return ResponseEntity.ok(result);
}
@PostMapping("/tasks/{taskName}/trigger")
public ResponseEntity<String> triggerTask(@PathVariable String taskName) {
// 手动触发定时任务的逻辑
log.info("手动触发定时任务: {}", taskName);
return ResponseEntity.ok("任务触发成功");
}
private Map<String, Object> getThreadPoolInfo() {
Map<String, Object> info = new HashMap<>();
ScheduledExecutorService executor = taskScheduler.getScheduledExecutor();
ThreadPoolExecutor threadPool = (ThreadPoolExecutor) executor;
info.put("poolSize", threadPool.getPoolSize());
info.put("activeCount", threadPool.getActiveCount());
info.put("corePoolSize", threadPool.getCorePoolSize());
info.put("maximumPoolSize", threadPool.getMaximumPoolSize());
info.put("queueSize", threadPool.getQueue().size());
info.put("completedTaskCount", threadPool.getCompletedTaskCount());
return info;
}
}
六、Spring Boot 自动配置原理深度解析
6.1 自动配置完整流程
6.2 自动配置源码分析
6.2.1 @SpringBootApplication 分解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication {
// 排除特定的自动配置类
Class<?>[] exclude() default {};
// 通过类名排除自动配置类
String[] excludeName() default {};
// 扫描的基础包
String[] scanBasePackages() default {};
// 扫描的基础包类
Class<?>[] scanBasePackageClasses() default {};
// Bean名称生成器
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
// 是否启用@Configuration属性扫描
boolean proxyBeanMethods() default true;
}
6.2.2 @EnableAutoConfiguration 核心
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
// 排除特定的自动配置类
Class<?>[] exclude() default {};
// 通过类名排除自动配置类
String[] excludeName() default {};
}
6.2.3 AutoConfigurationImportSelector 核心方法
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 获取自动配置条目
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获取注解属性
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 获取候选配置列表
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 移除重复项
configurations = removeDuplicates(configurations);
// 获取排除的配置
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 检查排除项是否有效
checkExcludedClasses(configurations, exclusions);
// 移除排除的配置
configurations.removeAll(exclusions);
// 应用过滤
configurations = getConfigurationClassFilter().filter(configurations);
// 触发自动配置导入事件
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 从spring.factories文件加载配置
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
// 断言配置不为空
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
}
6.3 条件注解详解
6.3.1 常用条件注解
// 类条件注解
@ConditionalOnClass // 类路径下存在指定的类
@ConditionalOnMissingClass // 类路径下不存在指定的类
@ConditionalOnBean // 容器中存在指定的Bean
@ConditionalOnMissingBean // 容器中不存在指定的Bean
// 属性条件注解
@ConditionalOnProperty // 配置属性满足条件
@ConditionalOnExpression // SpEL表达式条件
@ConditionalOnResource // 资源文件存在
// Web条件注解
@ConditionalOnWebApplication // 是Web应用
@ConditionalOnNotWebApplication // 不是Web应用
@ConditionalOnWarDeployment // WAR包部署
// 其他条件注解
@ConditionalOnJava // Java版本条件
@ConditionalOnJndi // JNDI条件
@ConditionalOnCloudPlatform // 云平台条件
6.3.2 条件注解使用示例
@Configuration
// 当类路径下存在DataSource类时生效
@ConditionalOnClass(DataSource.class)
// 当配置了spring.datasource.url属性时生效
@ConditionalOnProperty(prefix = "spring.datasource", name = "url")
// 当容器中不存在DataSource类型的Bean时生效
@ConditionalOnMissingBean(DataSource.class)
public class DataSourceAutoConfiguration {
@Bean
// 当配置了spring.datasource.type属性且值为com.zaxxer.hikari.HikariDataSource时生效
@ConditionalOnProperty(prefix = "spring.datasource", name = "type", havingValue = "com.zaxxer.hikari.HikariDataSource")
// 或者当没有配置spring.datasource.type属性时生效
@ConditionalOnMissingBean(DataSource.class)
public DataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = properties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
// Hikari连接池配置
if (properties.getName() != null) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
@Bean
// 当配置了spring.datasource.type属性且值为org.apache.tomcat.jdbc.pool.DataSource时生效
@ConditionalOnProperty(prefix = "spring.datasource", name = "type", havingValue = "org.apache.tomcat.jdbc.pool.DataSource", matchIfMissing = true)
@ConditionalOnMissingBean(DataSource.class)
public DataSource tomcatDataSource(DataSourceProperties properties) {
TomcatDataSource dataSource = new TomcatDataSource();
// Tomcat连接池配置
dataSource.setUrl(properties.getUrl());
dataSource.setUsername(properties.getUsername());
dataSource.setPassword(properties.getPassword());
return dataSource;
}
}
6.4 自定义 Starter 开发
6.4.1 自定义 Starter 项目结构
my-spring-boot-starter/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── starter/
│ │ ├── MyService.java
│ │ ├── MyProperties.java
│ │ ├── MyAutoConfiguration.java
│ │ └── MyHealthIndicator.java
│ └── resources/
│ └── META-INF/
│ ├── spring.factories
│ └── additional-spring-configuration-metadata.json
└── pom.xml
6.4.2 配置属性类
@ConfigurationProperties(prefix = "my.starter")
@Validated
@Data
public class MyProperties {
/**
* 服务端点地址
*/
@NotBlank
private String endpoint = "http://localhost:8080";
/**
* 连接超时时间(毫秒)
*/
@Min(1000)
@Max(60000)
private int connectTimeout = 5000;
/**
* 读取超时时间(毫秒)
*/
@Min(1000)
@Max(120000)
private int readTimeout = 30000;
/**
* 是否启用服务
*/
private boolean enabled = true;
/**
* 重试配置
*/
private Retry retry = new Retry();
/**
* 缓存配置
*/
private Cache cache = new Cache();
@Data
public static class Retry {
/**
* 最大重试次数
*/
@Min(0)
@Max(10)
private int maxAttempts = 3;
/**
* 重试间隔(毫秒)
*/
@Min(100)
private long backoffInterval = 1000;
/**
* 是否启用重试
*/
private boolean enabled = true;
}
@Data
public static class Cache {
/**
* 缓存过期时间(秒)
*/
@Min(60)
private long expireSeconds = 3600;
/**
* 缓存最大大小
*/
@Min(100)
private int maxSize = 1000;
/**
* 是否启用缓存
*/
private boolean enabled = true;
}
}
6.4.3 自动配置类
@Configuration
@EnableConfigurationProperties(MyProperties.class)
// 当类路径下存在MyService时生效
@ConditionalOnClass(MyService.class)
// 当配置了my.starter.enabled=true或未配置时生效
@ConditionalOnProperty(prefix = "my.starter", name = "enabled", matchIfMissing = true)
// 在Web应用启动后配置
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
public class MyAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(MyAutoConfiguration.class);
@Bean
// 当容器中不存在MyService类型的Bean时生效
@ConditionalOnMissingBean(MyService.class)
public MyService myService(MyProperties properties, ObjectProvider<RestTemplateBuilder> restTemplateBuilder) {
log.info("初始化MyService,端点: {}", properties.getEndpoint());
RestTemplate restTemplate = restTemplateBuilder.getIfAvailable(RestTemplateBuilder::new)
.setConnectTimeout(Duration.ofMillis(properties.getConnectTimeout()))
.setReadTimeout(Duration.ofMillis(properties.getReadTimeout()))
.build();
return new MyService(properties, restTemplate);
}
@Bean
// 当配置了my.starter.cache.enabled=true时生效
@ConditionalOnProperty(prefix = "my.starter.cache", name = "enabled", havingValue = "true")
@ConditionalOnMissingBean(MyServiceCacheManager.class)
public MyServiceCacheManager myServiceCacheManager(MyProperties properties) {
log.info("初始化MyService缓存管理器,过期时间: {}秒", properties.getCache().getExpireSeconds());
return new MyServiceCacheManager(properties.getCache());
}
@Bean
// 当是Web应用时生效
@ConditionalOnWebApplication
public MyHealthIndicator myHealthIndicator(MyService myService) {
return new MyHealthIndicator(myService);
}
@Bean
// 当配置了my.starter.retry.enabled=true时生效
@ConditionalOnProperty(prefix = "my.starter.retry", name = "enabled", havingValue = "true")
public MyServiceRetryTemplate myServiceRetryTemplate(MyProperties properties) {
return new MyServiceRetryTemplate(properties.getRetry());
}
}
6.4.4 服务实现类
public class MyService {
private final MyProperties properties;
private final RestTemplate restTemplate;
private final MyServiceCacheManager cacheManager;
private final MyServiceRetryTemplate retryTemplate;
public MyService(MyProperties properties, RestTemplate restTemplate) {
this.properties = properties;
this.restTemplate = restTemplate;
this.cacheManager = null;
this.retryTemplate = null;
}
public MyService(MyProperties properties, RestTemplate restTemplate,
MyServiceCacheManager cacheManager, MyServiceRetryTemplate retryTemplate) {
this.properties = properties;
this.restTemplate = restTemplate;
this.cacheManager = cacheManager;
this.retryTemplate = retryTemplate;
}
public String getData(String key) {
// 如果有缓存管理器,先尝试从缓存获取
if (cacheManager != null) {
String cachedData = cacheManager.get(key);
if (cachedData != null) {
return cachedData;
}
}
// 执行实际的数据获取逻辑
String data = fetchDataFromEndpoint(key);
// 如果有缓存管理器,将数据放入缓存
if (cacheManager != null) {
cacheManager.put(key, data);
}
return data;
}
private String fetchDataFromEndpoint(String key) {
String url = String.format("%s/api/data/%s", properties.getEndpoint(), key);
// 如果有重试模板,使用重试逻辑
if (retryTemplate != null) {
return retryTemplate.execute(() -> {
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
return response.getBody();
});
} else {
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
return response.getBody();
}
}
public boolean isHealthy() {
try {
String healthUrl = properties.getEndpoint() + "/health";
ResponseEntity<String> response = restTemplate.getForEntity(healthUrl, String.class);
return response.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
return false;
}
}
}
6.4.5 健康检查指示器
public class MyHealthIndicator implements HealthIndicator {
private final MyService myService;
public MyHealthIndicator(MyService myService) {
this.myService = myService;
}
@Override
public Health health() {
try {
boolean isHealthy = myService.isHealthy();
if (isHealthy) {
return Health.up()
.withDetail("endpoint", myService.getProperties().getEndpoint())
.withDetail("timestamp", System.currentTimeMillis())
.build();
} else {
return Health.down()
.withDetail("endpoint", myService.getProperties().getEndpoint())
.withDetail("error", "服务端点不可用")
.build();
}
} catch (Exception e) {
return Health.down(e)
.withDetail("endpoint", myService.getProperties().getEndpoint())
.build();
}
}
}
6.4.6 Spring Factories 配置
在 src/main/resources/META-INF/spring.factories 中:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.MyAutoConfiguration
# Configuration Metadata
org.springframework.boot.autoconfigure.AutoConfigurationMetadata=\
com.example.starter.MyAutoConfigurationMetadata
6.4.7 配置元数据
在 src/main/resources/META-INF/additional-spring-configuration-metadata.json 中:
{
"properties": [
{
"name": "my.starter.endpoint",
"type": "java.lang.String",
"description": "MyService服务端点地址",
"defaultValue": "http://localhost:8080"
},
{
"name": "my.starter.connect-timeout",
"type": "java.lang.Integer",
"description": "连接超时时间(毫秒)",
"defaultValue": 5000,
"sourceType": "com.example.starter.MyProperties"
},
{
"name": "my.starter.read-timeout",
"type": "java.lang.Integer",
"description": "读取超时时间(毫秒)",
"defaultValue": 30000,
"sourceType": "com.example.starter.MyProperties"
},
{
"name": "my.starter.enabled",
"type": "java.lang.Boolean",
"description": "是否启用MyService",
"defaultValue": true,
"sourceType": "com.example.starter.MyProperties"
},
{
"name": "my.starter.retry.max-attempts",
"type": "java.lang.Integer",
"description": "最大重试次数",
"defaultValue": 3,
"sourceType": "com.example.starter.MyProperties$Retry"
},
{
"name": "my.starter.retry.backoff-interval",
"type": "java.lang.Long",
"description": "重试间隔时间(毫秒)",
"defaultValue": 1000,
"sourceType": "com.example.starter.MyProperties$Retry"
},
{
"name": "my.starter.cache.expire-seconds",
"type": "java.lang.Long",
"description": "缓存过期时间(秒)",
"defaultValue": 3600,
"sourceType": "com.example.starter.MyProperties$Cache"
}
],
"hints": [
{
"name": "my.starter.endpoint",
"values": [
{
"value": "http://localhost:8080",
"description": "本地开发环境"
},
{
"value": "http://test.example.com",
"description": "测试环境"
},
{
"value": "http://api.example.com",
"description": "生产环境"
}
]
}
]
}
七、生产环境最佳实践
7.1 多环境配置管理
7.1.1 配置文件组织
src/main/resources/
├── application.yml
├── application-dev.yml
├── application-test.yml
├── application-staging.yml
├── application-prod.yml
└── application-local.yml
7.1.2 主配置文件
# application.yml
spring:
profiles:
active: @activatedProperties@
application:
name: my-spring-boot-app
# 通用配置
server:
servlet:
session:
timeout: 30m
encoding:
charset: UTF-8
enabled: true
force: true
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,beans
enabled-by-default: false
endpoint:
health:
enabled: true
show-details: when-authorized
metrics:
enabled: true
env:
enabled: true
beans:
enabled: true
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
level:
com.example: INFO
org.springframework: WARN
org.hibernate: WARN
7.1.3 环境特定配置
# application-dev.yml
spring:
profiles: dev
server:
port: 8080
logging:
level:
com.example: DEBUG
org.springframework.web: DEBUG
file:
name: logs/dev-application.log
path: ./logs
max-size: 10MB
max-history: 7
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
shutdown:
enabled: true
# 开发环境特定配置
app:
features:
enable-swagger: true
enable-dev-tools: true
security:
require-ssl: false
# application-prod.yml
spring:
profiles: prod
server:
port: 80
compression:
enabled: true
mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
min-response-size: 1024
servlet:
context-path: /api
logging:
level:
com.example: INFO
org.springframework: WARN
file:
name: /var/log/myapp/application.log
path: /var/log/myapp
max-size: 100MB
max-history: 30
logback:
rollingpolicy:
max-file-size: 100MB
total-size-cap: 1GB
management:
endpoints:
web:
exposure:
include: health,info,metrics
base-path: /internal/actuator
endpoint:
health:
show-details: never
show-components: never
shutdown:
enabled: false
server:
port: 8081
address: 127.0.0.1
# 生产环境特定配置
app:
features:
enable-swagger: false
enable-dev-tools: false
security:
require-ssl: true
monitoring:
enable-apm: true
enable-log-aggregation: true
7.2 健康检查与监控
7.2.1 自定义健康检查
@Component
public class ComprehensiveHealthIndicator implements HealthIndicator {
@Autowired
private DataSource dataSource;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private RestTemplate restTemplate;
@Value("${app.external-service.url:}")
private String externalServiceUrl;
@Override
public Health health() {
Health.Builder builder = Health.up();
// 数据库健康检查
builder.withDetail("database", checkDatabaseHealth());
// Redis健康检查
builder.withDetail("redis", checkRedisHealth());
// 外部服务健康检查
if (StringUtils.isNotBlank(externalServiceUrl)) {
builder.withDetail("external-service", checkExternalServiceHealth());
}
// 磁盘空间检查
builder.withDetail("disk", checkDiskSpace());
// 内存使用情况
builder.withDetail("memory", checkMemoryUsage());
return builder.build();
}
private HealthComponent checkDatabaseHealth() {
try (Connection conn = dataSource.getConnection()) {
boolean isValid = conn.isValid(5);
long maxConnections = -1;
long activeConnections = -1;
// 尝试获取连接池信息
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
maxConnections = hikariDataSource.getMaximumPoolSize();
activeConnections = hikariDataSource.getHikariPoolMXBean().getActiveConnections();
}
return HealthComponent.builder()
.status(isValid ? "UP" : "DOWN")
.details(Map.of(
"validation", isValid,
"maxConnections", maxConnections,
"activeConnections", activeConnections
))
.build();
} catch (Exception e) {
return HealthComponent.builder()
.status("DOWN")
.details(Map.of("error", e.getMessage()))
.build();
}
}
private HealthComponent checkRedisHealth() {
try {
String result = redisTemplate.execute((RedisCallback<String>) connection -> {
connection.ping();
return "PONG";
});
return HealthComponent.builder()
.status("PONG".equals(result) ? "UP" : "DOWN")
.details(Map.of("response", result))
.build();
} catch (Exception e) {
return HealthComponent.builder()
.status("DOWN")
.details(Map.of("error", e.getMessage()))
.build();
}
}
private HealthComponent checkExternalServiceHealth() {
try {
ResponseEntity<String> response = restTemplate.getForEntity(
externalServiceUrl + "/health", String.class);
return HealthComponent.builder()
.status(response.getStatusCode().is2xxSuccessful() ? "UP" : "DOWN")
.details(Map.of(
"statusCode", response.getStatusCodeValue(),
"responseTime", System.currentTimeMillis() // 实际项目中应该测量响应时间
))
.build();
} catch (Exception e) {
return HealthComponent.builder()
.status("DOWN")
.details(Map.of("error", e.getMessage()))
.build();
}
}
private HealthComponent checkDiskSpace() {
File root = new File("/");
long total = root.getTotalSpace();
long free = root.getFreeSpace();
double usage = (double) (total - free) / total * 100;
return HealthComponent.builder()
.status(usage < 90 ? "UP" : "DEGRADED")
.details(Map.of(
"total", formatBytes(total),
"free", formatBytes(free),
"usage", String.format("%.2f%%", usage)
))
.build();
}
private HealthComponent checkMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long usedMemory = totalMemory - freeMemory;
double usage = (double) usedMemory / maxMemory * 100;
return HealthComponent.builder()
.status(usage < 80 ? "UP" : "DEGRADED")
.details(Map.of(
"max", formatBytes(maxMemory),
"used", formatBytes(usedMemory),
"usage", String.format("%.2f%%", usage)
))
.build();
}
private String formatBytes(long bytes) {
if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024));
String pre = "KMGTPE".charAt(exp - 1) + "i";
return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre);
}
@Data
@Builder
private static class HealthComponent {
private String status;
private Map<String, Object> details;
}
}
7.2.2 指标收集与监控
@Component
public class CustomMetricsCollector {
private final MeterRegistry meterRegistry;
// 计数器
private final Counter apiRequestsCounter;
private final Counter businessExceptionsCounter;
// 计时器
private final Timer apiResponseTimer;
private final Timer databaseQueryTimer;
// 计量器
private final Gauge activeUsersGauge;
private final Gauge cacheHitRateGauge;
// 缓存命中率
private final AtomicLong cacheHits = new AtomicLong();
private final AtomicLong cacheMisses = new AtomicLong();
public CustomMetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
// 初始化指标
this.apiRequestsCounter = Counter.builder("api.requests")
.description("API请求总数")
.tag("application", "my-spring-boot-app")
.register(meterRegistry);
this.businessExceptionsCounter = Counter.builder("business.exceptions")
.description("业务异常总数")
.tag("application", "my-spring-boot-app")
.register(meterRegistry);
this.apiResponseTimer = Timer.builder("api.response.time")
.description("API响应时间")
.tag("application", "my-spring-boot-app")
.register(meterRegistry);
this.databaseQueryTimer = Timer.builder("database.query.time")
.description("数据库查询时间")
.tag("application", "my-spring-boot-app")
.register(meterRegistry);
this.activeUsersGauge = Gauge.builder("active.users")
.description("活跃用户数")
.tag("application", "my-spring-boot-app")
.register(meterRegistry, new AtomicLong(0));
this.cacheHitRateGauge = Gauge.builder("cache.hit.rate")
.description("缓存命中率")
.tag("application", "my-spring-boot-app")
.register(meterRegistry, this, collector -> {
long hits = cacheHits.get();
long misses = cacheMisses.get();
long total = hits + misses;
return total > 0 ? (double) hits / total : 0.0;
});
}
public void recordApiRequest(String endpoint, String method, int statusCode) {
apiRequestsCounter.increment();
// 添加额外标签
Counter.builder("api.requests.detailed")
.tag("endpoint", endpoint)
.tag("method", method)
.tag("status", String.valueOf(statusCode))
.register(meterRegistry)
.increment();
}
public void recordApiResponseTime(String endpoint, long durationMs) {
apiResponseTimer.record(Duration.ofMillis(durationMs));
Timer.builder("api.response.time.detailed")
.tag("endpoint", endpoint)
.register(meterRegistry)
.record(Duration.ofMillis(durationMs));
}
public void recordDatabaseQuery(String queryType, long durationMs) {
databaseQueryTimer.record(Duration.ofMillis(durationMs));
Timer.builder("database.query.time.detailed")
.tag("queryType", queryType)
.register(meterRegistry)
.record(Duration.ofMillis(durationMs));
}
public void recordBusinessException(String exceptionType) {
businessExceptionsCounter.increment();
Counter.builder("business.exceptions.detailed")
.tag("type", exceptionType)
.register(meterRegistry)
.increment();
}
public void recordCacheHit() {
cacheHits.incrementAndGet();
}
public void recordCacheMiss() {
cacheMisses.incrementAndGet();
}
public void updateActiveUsers(long count) {
// 更新活跃用户数
// 注意:这里需要实际实现获取活跃用户数的逻辑
}
@EventListener
public void handleRequestEvent(RequestHandledEvent event) {
recordApiRequest(event.getRequestUrl(), event.getHttpMethod(), event.getStatusCode());
}
}
7.3 性能优化配置
7.3.1 JVM 参数优化
#!/bin/bash
# start-app.sh
JAVA_OPTS="
-server
-Xms2g
-Xmx2g
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2
-XX:InitiatingHeapOccupancyPercent=35
-XX:+ExplicitGCInvokesConcurrent
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./logs/heapdump.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-Xloggc:./logs/gc.log
-Djava.awt.headless=true
-Djava.net.preferIPv4Stack=true
-Dfile.encoding=UTF-8
-Duser.timezone=Asia/Shanghai
-Dspring.profiles.active=prod
-Dmanagement.endpoints.web.exposure.include=health,info,metrics
"
java $JAVA_OPTS -jar my-spring-boot-app.jar
7.3.2 应用性能优化配置
# application-prod.yml 性能优化部分
spring:
# Servlet配置
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
# Jackson配置
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
serialization:
write-dates-as-timestamps: false
fail-on-empty-beans: false
deserialization:
fail-on-unknown-properties: false
# 数据源配置
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
connection-test-query: SELECT 1
# Redis配置
redis:
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 3000
timeout: 3000
# 缓存配置
cache:
type: redis
redis:
time-to-live: 1h
cache-null-values: false
use-key-prefix: true
# Tomcat优化配置
server:
tomcat:
max-connections: 10000
max-threads: 200
min-spare-threads: 10
connection-timeout: 10000
max-http-header-size: 8KB
# 静态资源缓存
static-resource-cache-ttl: 1h
# 日志异步配置
logging:
config: classpath:logback-spring.xml
7.3.3 Logback 异步日志配置
<!-- logback-spring.xml -->
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 应用名称 -->
<property name="APP_NAME" value="my-spring-boot-app"/>
<!-- 日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
<property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/${APP_NAME}.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>./logs/${APP_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 异步文件输出 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>1024</queueSize>
<neverBlock>true</neverBlock>
<appender-ref ref="FILE"/>
</appender>
<!-- 错误日志单独输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/${APP_NAME}-error.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>./logs/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步错误日志 -->
<appender name="ASYNC_ERROR_FILE" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>1024</queueSize>
<neverBlock>true</neverBlock>
<appender-ref ref="ERROR_FILE"/>
</appender>
<!-- 日志级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ASYNC_ERROR_FILE"/>
</root>
<!-- 特定包日志级别 -->
<logger name="com.example" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
</logger>
<logger name="org.springframework" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
<logger name="org.apache" level="WARN"/>
</configuration>
八、Spring Boot 面试题深度解析
8.1 核心原理面试题
8.1.1 Spring Boot 自动配置原理
问题:请详细说明 Spring Boot 自动配置的工作原理
回答要点:
-
@SpringBootApplication 注解
- 是 @Configuration、@EnableAutoConfiguration、@ComponentScan 的组合注解
- @EnableAutoConfiguration 是自动配置的核心入口
-
自动配置加载过程
// 关键类:AutoConfigurationImportSelector public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } -
spring.factories 机制
- 在 META-INF/spring.factories 文件中定义自动配置类
- 格式:org.springframework.boot.autoconfigure.EnableAutoConfiguration=配置类全限定名
-
条件注解机制
- @ConditionalOnClass:类路径下存在指定类时生效
- @ConditionalOnBean:容器中存在指定Bean时生效
- @ConditionalOnProperty:配置属性满足条件时生效
- @ConditionalOnMissingBean:容器中不存在指定Bean时生效
-
配置属性绑定
- 使用 @ConfigurationProperties 绑定配置
- 通过 @EnableConfigurationProperties 启用
示例说明:
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.datasource", name = "url")
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
8.1.2 Starter 工作原理
问题:Spring Boot Starter 的工作原理是什么?如何自定义一个 Starter?
回答要点:
-
Starter 组成
- 自动配置类(AutoConfiguration)
- 配置属性类(ConfigurationProperties)
- spring.factories 配置文件
- 可选:附加的配置元数据
-
自定义 Starter 步骤
- 创建配置属性类
- 创建自动配置类
- 配置 spring.factories
- 添加配置元数据
-
依赖管理
- Starter 应该包含所有必要的依赖
- 使用 BOM(Bill of Materials)管理版本
示例:
// 1. 配置属性类
@ConfigurationProperties(prefix = "my.starter")
@Data
public class MyStarterProperties {
private String endpoint;
private int timeout = 5000;
}
// 2. 自动配置类
@Configuration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyStarterProperties.class)
public class MyStarterAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyStarterProperties properties) {
return new MyService(properties);
}
}
// 3. spring.factories
// META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyStarterAutoConfiguration
8.2 配置和特性面试题
8.2.1 外部化配置
问题:Spring Boot 支持哪些外部化配置方式?加载优先级是怎样的?
回答要点:
-
配置源(从高到低优先级)
- 命令行参数(–server.port=8080)
- Java 系统属性(System.getProperties())
- 操作系统环境变量
- 打包在jar包外的配置文件(application-{profile}.properties/yml)
- 打包在jar包内的配置文件
- @Configuration 类上的 @PropertySource
- SpringApplication.setDefaultProperties
-
Profile 机制
- 通过 spring.profiles.active 指定激活的Profile
- 配置文件命名:application-{profile}.yml
- @Profile 注解控制Bean在不同环境下的加载
-
配置绑定
- @Value 注解:简单配置注入
- @ConfigurationProperties:类型安全的配置绑定
- @PropertySource:加载指定配置文件
示例:
# application.yml
spring:
profiles:
active: dev
application:
name: myapp
# application-dev.yml
server:
port: 8080
logging:
level:
root: DEBUG
# application-prod.yml
server:
port: 80
logging:
level:
root: INFO
8.2.2 监控和管理
问题:Spring Boot Actuator 提供了哪些端点?如何自定义健康检查?
回答要点:
-
常用端点
- /actuator/health:应用健康状态
- /actuator/info:应用信息
- /actuator/metrics:应用指标
- /actuator/env:环境变量
- /actuator/beans:所有Bean信息
-
健康检查自定义
- 实现 HealthIndicator 接口
- 注册为Spring Bean
- 支持复合健康检查
-
安全配置
- 通过 management.endpoints.web.exposure.include 控制暴露的端点
- 集成Spring Security进行端点保护
示例:
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
// 检查组件健康状态
if (isComponentHealthy()) {
return Health.up()
.withDetail("component", "正常")
.build();
} else {
return Health.down()
.withDetail("component", "异常")
.build();
}
} catch (Exception e) {
return Health.down(e).build();
}
}
}
8.3 性能优化面试题
8.3.1 启动性能优化
问题:如何优化 Spring Boot 应用的启动性能?
回答要点:
-
懒加载配置
@SpringBootApplication public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class) .lazyInitialization(true) .run(args); } } -
排除自动配置
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class }) -
组件扫描优化
@SpringBootApplication(scanBasePackages = "com.example.core") -
JVM 参数优化
- 使用G1垃圾回收器
- 合理设置堆内存大小
- 启用类数据共享
-
编译时优化
- 使用Spring Native进行原生编译
- 使用GraalVM减少启动时间
8.3.2 运行时性能优化
问题:如何优化 Spring Boot 应用的运行时性能?
回答要点:
-
连接池配置
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 -
缓存配置
- 使用Redis等分布式缓存
- 合理设置缓存过期时间
- 使用缓存注解(@Cacheable)
-
异步处理
@Async @EventListener public void handleEvent(MyEvent event) { // 异步处理逻辑 } -
数据库优化
- 使用连接池
- 合理设置事务边界
- 使用分页查询
-
监控和调优
- 使用Micrometer收集指标
- 使用APM工具进行性能分析
- 定期进行性能测试
结语:Spring Boot 学习路径和最佳实践
持续学习资源
-
官方文档
-
社区资源
-
实践项目
- 微服务项目实践
- 开源项目源码学习
- 技术博客和分享
Spring Boot 作为一个快速发展的框架,持续学习和实践是掌握它的关键。希望本文能够为你提供全面的 Spring Boot 知识体系,帮助你在实际项目中更好地应用这个强大的框架。
记住:理论结合实践,持续重构优化,才是技术成长的正道!

5354

被折叠的 条评论
为什么被折叠?



