Spring Boot 的价值不在「省了 XML」,而在约定大于配置 + 可插拔的 starter 体系。理解自动装配,才知道为什么加个依赖就多了一堆 Bean,以及配置为什么不生效。
一、自动装配是怎么发生的
关键点:@ConditionalOnMissingBean 意味着你自己定义的 Bean 会覆盖自动配置。想改 Redis 序列化、想换 ObjectMapper,直接自己定义一个 @Bean 就行,不用排除自动配置类。
二、启动流程(知道节点,才知道在哪插手)
SpringApplication.run()
→ 推断应用类型(Servlet / Reactive)
→ 加载 ApplicationContextInitializer、ApplicationListener
→ 准备 Environment(读 application.yml、命令行参数、环境变量)
→ 创建 ApplicationContext
→ refreshContext(扫描 Bean、自动装配、启动内嵌 Tomcat)
→ 调用 ApplicationRunner / CommandLineRunner
→ 发布 ApplicationReadyEvent
常用的介入点:
| 想做什么 | 用什么 |
|---|---|
| 启动后执行一次初始化 | ApplicationRunner / CommandLineRunner(@Order 排序) |
| 环境准备阶段改配置 | EnvironmentPostProcessor |
| 监听启动完成事件 | @EventListener(ApplicationReadyEvent.class) |
| 启动失败兜底 | FailureAnalyzer |
三、必写的几个扩展点
1. 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public Result<?> biz(BizException e) {
log.warn("biz error: {}", e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> valid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ":" + f.getDefaultMessage())
.collect(Collectors.joining(", "));
return Result.fail(400, msg);
}
@ExceptionHandler(Exception.class)
public Result<?> ex(Exception e) {
log.error("unexpected", e); // ★ 必须打完整栈,否则没法排查
return Result.fail(500, "服务繁忙,请稍后重试");
}
}
对外不要暴露堆栈和 SQL 异常,对内要打完整日志。这两件事不冲突。
2. 参数校验
@Data
public class CreateOrderReq {
@NotBlank(message = "订单号不能为空")
private String orderNo;
@NotNull @Min(value = 1, message = "数量至少为 1")
private Integer qty;
}
@PostMapping("/order")
public Result<Void> create(@Valid @RequestBody CreateOrderReq req) { ... }
3. 拦截器 vs 过滤器
| 过滤器 Filter | 拦截器 Interceptor | |
|---|---|---|
| 归属 | Servlet 规范 | Spring MVC |
| 能拿到 | 原始 request/response | Handler 方法、参数 |
| 典型用途 | 编码、跨域、XSS 过滤 | 登录态、权限、日志 TraceId |
4. 异步与定时任务
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("bizExecutor")
public Executor bizExecutor() {
ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor();
e.setCorePoolSize(8); e.setMaxPoolSize(32);
e.setQueueCapacity(2000);
e.setThreadNamePrefix("biz-");
e.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
e.setTaskDecorator(new MdcTaskDecorator()); // ★ 传递 TraceId,否则异步日志断链
e.initialize();
return e;
}
}
// 使用:@Async("bizExecutor") 一定要指定线程池名,别用默认
@Async 的三个坑:必须指定线程池(默认 SimpleAsyncTaskExecutor 每次新建线程)、不能同类自调用(同事务)、要自己做 TraceId 传递。
四、生产配置清单(application.yml 关键项)
server:
shutdown: graceful # 优雅停机,配合下面的超时
tomcat:
threads:
max: 400 # 默认 200,高并发下不够
connection-timeout: 20000
max-swallow-size: -1
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
mvc:
throw-exception-if-no-handler-found: true
jackson:
default-property-inclusion: non_null # 返回体不吐 null 字段
time-zone: Asia/Shanghai
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000 # ★ 要小于 MySQL 的 wait_timeout
leak-detection-threshold: 60000 # 连接泄漏检测,开发环境必开
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: never # ★ 公网不要暴露 details
max-lifetime 一定要小于 MySQL 的 wait_timeout(默认 8 小时),否则连接池里会出现已被服务端断开的死连接。
五、配置不生效 / 启动慢的排查
| 现象 | 排查 |
|---|---|
| 配置改了没生效 | application.yml 加载顺序:命令行 > 环境变量 > application-{profile}.yml > application.yml;确认 spring.profiles.active |
| 不知道哪个配置生效 | Actuator 的 /actuator/env + /actuator/configprops |
| 自动配置到底开没开 | --debug 看 CONDITION EVALUATION DELTA;或 /actuator/conditions |
| 启动慢 | 加 -Dspring.main.lazy-initialization=true 验证是否 Bean 太多;用 SpringStartupAnalyzer 生成启动耗时火焰图 |
| 端口被占 | netstat -tlnp \| grep 8080 / lsof -i:8080 |
六、打包与启动命令
mvn clean package -DskipTests
java -jar app.jar --spring.profiles.active=prod
# 指定内存与 GC
java -Xms2g -Xmx2g -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -jar app.jar
# 后台运行并留日志
nohup java -jar app.jar > /data/logs/app.log 2>&1 &
# 优雅停机(先摘流量,再 kill)
kill -TERM <pid> # 不要用 kill -9
kill -9 是禁用项:正在处理的请求直接断、事务来不及提交、优雅停机逻辑完全不执行。正确顺序是「注册中心/网关摘流量 → 等待 30s → kill -TERM」。
七、自定义 starter(团队基建常用)
做一个内部 starter 只需要三步:
xxx-spring-boot-autoconfigure模块里写@Configuration+@ConditionalOnXxxMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里写上配置类全限定名- 再包一个
xxx-spring-boot-starter只做依赖聚合
内部中间件(统一日志、TraceId、多租户上下文)做成 starter,比每个服务复制代码靠谱得多。
一起聊聊