1Spring Boot 是什么
约定优于配置,让 Java 企业开发像"自动挡汽车"
Spring Boot 是基于 Spring Framework 的快速开发框架,它通过自动配置(Auto-Configuration)、起步依赖(Starter)和内嵌服务器(Tomcat),让开发者只需极少的配置就能搭建生产级应用。核心特点:IoC 容器管理 Bean、AOP 面向切面、约定优于配置、内嵌 Tomcat 一键启动。
你不用 new 对象了。类上加 @Service/@Component,容器帮你创建、注入、管理。用的时候 @Autowired 自动拿。结果:松耦合、可替换、好测试。
架构图:分层 + IoC 容器
浏览器 / App / curl
接收 HTTP
分发请求
管理所有 Bean 的生命周期
自动装配 Bean
DataSource / Redis / MQ
外部化配置
多环境 Profile
Spring Boot vs Spring Framework
| 维度 | Spring Framework | Spring Boot |
|---|---|---|
| 配置方式 | XML / Java Config,手动配置多 | 自动配置 + application.yml |
| 服务器 | 需要打包成 WAR 部署到 Tomcat | 内嵌 Tomcat,java -jar 启动 |
| 依赖管理 | 手动指定每个依赖的版本号 | Starter 自动管理版本,parent POM |
| 启动方式 | 配置 ContextLoaderListener | SpringApplication.run() 一行启动 |
| 生产监控 | 需额外集成 | Actuator 内置健康检查/指标/审计 |
| 适用场景 | 需要精细控制的老项目 | 微服务、新项目(业界标配) |
Spring Boot 不是替代 Spring Framework 的新框架,而是在 Spring 之上做了一层封装。核心是 @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan。理解 Boot 的关键在于理解自动配置原理:spring.factories 中声明了 xxxAutoConfiguration,根据 @Conditional 决定是否加载。
自动配置原理
// Spring Boot 启动入口
@SpringBootApplication // = 3个注解的组合
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
// 自动配置核心:spring.factories 中声明的 AutoConfiguration
// 例如 DataSourceAutoConfiguration:
@Configuration
@ConditionalOnClass(DataSource.class) // classpath 有 DataSource 才生效
@ConditionalOnProperty(name = "spring.datasource.url") // 配置了 URL 才生效
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 你没手动定义才自动创建
public DataSource dataSource(DataSourceProperties props) {
return props.initializeDataSourceBuilder().build();
}
}
2IoC 容器与 Bean 生命周期 核心中的核心!
理解 Bean 生命周期 = 理解 Spring 的灵魂
实例化(调构造方法)→ 属性注入(@Autowired)→ 初始化(@PostConstruct → InitializingBean → BeanPostProcessor)→ 使用 → 销毁(@PreDestroy)
其中 BeanPostProcessor 是 AOP 实现的关键钩子。三级缓存(singletonObjects / earlySingletonObjects / singletonFactories)用来解决循环依赖。
Bean 生命周期 5 阶段详解
createBeanInstance
构造方法 new 出对象
populateBean
@Autowired 注入
initializeBean
@PostConstruct
Bean Ready
🔥 服务请求
destroyBean
@PreDestroy
BeanNameAware 等
postProcessBeforeInitialization
postProcessAfterInitialization
↑ 初始化阶段内部还有 3 个子步骤:Aware 注入 → 前置处理 → AOP 代理生成
生命周期代码演示
@Component
public class MyBean implements InitializingBean, DisposableBean {
@Autowired
private Dependency dep; // ② 属性填充阶段注入
public MyBean() {
System.out.println("① 构造方法: 实例化");
}
@PostConstruct
public void init() {
System.out.println("③ @PostConstruct: 初始化");
}
@Override
public void afterPropertiesSet() {
System.out.println("③ InitializingBean: 属性设置完成后");
}
@PreDestroy
public void cleanup() {
System.out.println("⑤ @PreDestroy: 销毁前清理");
}
@Override
public void destroy() {
System.out.println("⑤ DisposableBean: 销毁");
}
}
Bean 作用域(Scope)
| 作用域 | 说明 | 使用场景 |
|---|---|---|
| singleton(默认) | 容器中只有一个实例 | 无状态 Service、DAO |
| prototype | 每次获取都创建新实例 | 有状态对象、需要隔离的数据 |
| request | 每个 HTTP 请求一个实例 | Web 请求上下文信息 |
| session | 每个 HTTP Session 一个实例 | 用户登录信息 |
| application | 每个 ServletContext 一个 | 全局应用级缓存 |
singleton Bean 中 @Autowired 一个 prototype Bean,注入只发生一次,之后永远是同一个对象。解决方案:@Lookup 注解或 ObjectFactory。
// ❌ 错误: prototype 失效
@Service
public class OrderService {
@Autowired
private PriceCalculator calculator; // 只注入一次, 后续永远是同一个!
public BigDecimal calculate(Order order) {
calculator.reset(); // 多线程下有线程安全问题
return calculator.calc(order);
}
}
// ✅ 正确: 使用 @Lookup
@Service
public abstract class OrderService {
@Lookup // 每次调用都从容器获取新实例
protected abstract PriceCalculator getCalculator();
public BigDecimal calculate(Order order) {
PriceCalculator calc = getCalculator(); // 每次新的
return calc.calc(order);
}
}
循环依赖与三级缓存
Spring 用三级缓存解决 singleton Bean 的循环依赖(构造器循环依赖无法解决):
// DefaultSingletonBeanRegistry 中的三级缓存
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); // 一级: 完整 Bean
Map<String, Object> earlySingletonObjects = new HashMap<>(); // 二级: 提前曝光的半成品
Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(); // 三级: ObjectFactory
// 循环依赖解决流程 (A 依赖 B, B 依赖 A):
// 1. 创建 A → 实例化 A → 放入三级缓存
// 2. 填充 A 的属性, 发现需要 B → 创建 B
// 3. 创建 B → 实例化 B → 填充 B 的属性, 发现需要 A
// 4. 从三级缓存拿到 A 的 ObjectFactory → 提前暴露 A → 放入二级缓存
// 5. B 拿到 A 的引用, B 完成 → 放入一级缓存
// 6. A 继续完成初始化 → 放入一级缓存
// ⚠️ 为什么需要三级缓存而不是两级?
// 如果 A 被 AOP 代理, 三级缓存的 ObjectFactory 可以提前生成代理对象
// 保证 B 注入的是 A 的代理, 而不是原始对象
生产案例:Bean 注入失败
症状:新同事提交代码后,应用启动直接报 NoSuchBeanDefinitionException: No qualifying bean of type 'UserService',CI/CD 流水线红灯。
❌ 错误原因
// UserService 没有加 @Service 注解!
public class UserService { // 缺少 @Service
public User getUser(Long id) { ... }
}
// 或者: @ComponentScan 没扫到这个包
@SpringBootApplication
@ComponentScan("com.examplewrong") // 包名写错了
public class MyApp { ... }
✅ 正确做法
@Service // 必须加注解, 或者在 @Configuration 中 @Bean
public class UserService { ... }
// 或者使用 @Bean 方式注册
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService();
}
}
// 💡 排查技巧:
// 1. 检查类是否在 @ComponentScan 扫描范围内
// 2. 检查是否加了 @Service/@Component/@Repository/@Controller
// 3. 启动时加 --debug 查看 ConditionEvaluationReport
// 4. 使用 Actuator /beans 端点查看所有已注册 Bean
💡 经验法则:90% 的 Bean 注入失败都是因为忘了加注解或包扫描路径不对。
3AOP 与动态代理
Spring AOP 是事务、日志、限流的底层基石
5 种通知:@Before(方法前)/ @After(后,无论成败)/ @AfterReturning(成功后)/ @AfterThrowing(异常后)/ @Around(环绕,最强,能控制执行、改参数和返回值)。
事务(@Transactional)、日志、权限校验都是 AOP 实现。动态代理:JDK Proxy(基于接口)或 CGLIB(基于继承生成子类)。
JDK 动态代理 vs CGLIB 代理
implements UserService
事务/日志/权限
真实业务逻辑
⚠️ 目标类必须实现接口
// JDK Proxy 核心机制
Proxy.newProxyInstance(
classLoader,
new Class[]{UserService.class}, // 必须有接口
(proxy, method, args) -> {
// @Before
Object result = method.invoke(target, args);
// @After
return result;
}
);
extends UserServiceImpl
事务/日志/权限
真实业务逻辑
✅ 不需要接口,生成子类
// CGLIB 核心机制
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
// @Before
Object result = proxy.invokeSuper(obj, args);
// @After
return result;
});
enhancer.create();
① 目标类实现了接口 → 默认使用 JDK 动态代理
② 目标类没实现接口 → 使用 CGLIB
③ spring.aop.proxy-target-class=true(Spring Boot 2.x 默认 true)→ 强制使用 CGLIB
④ final 类/方法无法被 CGLIB 代理(不能继承)
@Transactional 原理与失效场景
// @Transactional 的本质 = 环绕通知
@Around("execution(* com.example.service.*.*(..))")
public Object manageTransaction(ProceedingJoinPoint pjp) throws Throwable {
TransactionStatus tx = transactionManager.begin(); // @Before: 开启事务
try {
Object result = pjp.proceed(); // 执行业务方法
transactionManager.commit(tx); // @AfterReturning: 提交
return result;
} catch (Exception e) {
// 默认只回滚 RuntimeException 和 Error
transactionManager.rollback(tx); // @AfterThrowing: 回滚
throw e;
}
}
// 所以事务的底层代理, 执行的是:
// 1. 开启数据库连接, 设置 autoCommit=false
// 2. 执行你的方法
// 3. 方法正常结束 → commit; 抛 RuntimeException → rollback
@Transactional 失效的 5 大场景
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 1. 类内部方法调用 | this.method() 不经过代理 | 注入自己/AopContext.currentProxy() |
| 2. 方法不是 public | Spring AOP 只代理 public 方法 | 改为 public |
| 3. 异常被 catch 吞掉 | 代理层没看到异常就不回滚 | catch 后 re-throw |
| 4. 异常类型不匹配 | 默认只回滚 RuntimeException | rollbackFor = Exception.class |
| 5. 数据库不支持事务 | MySQL MyISAM 引擎 | 用 InnoDB |
// ⚠️ 场景 1: 内部调用导致事务失效 (最经典的坑!)
@Service
public class OrderService {
public void batchProcess(List<Order> orders) {
for (Order order : orders) {
// this.process() 是直接调用, 不经过代理!
this.process(order); // ❌ 事务失效!
}
}
@Transactional
public void process(Order order) {
orderDao.update(order);
logDao.insert(buildLog(order));
}
}
// ✅ 修复方案 1: 注入自己
@Service
public class OrderService {
@Autowired
@Lazy
private OrderService self;
public void batchProcess(List<Order> orders) {
for (Order order : orders) {
self.process(order); // ✅ 通过代理调用!
}
}
}
// ✅ 修复方案 2: 从 AopContext 获取代理
((OrderService) AopContext.currentProxy()).process(order);
// ⚠️ 场景 4: 异常类型不匹配
@Transactional // 默认只回滚 RuntimeException
public void pay(Long orderId) {
try {
// 抛的是 checked Exception, 默认不回滚!
throw new Exception("支付通道异常");
} catch (Exception e) {
throw e;
}
}
// ✅ 修复: 指定 rollbackFor
@Transactional(rollbackFor = Exception.class) // 所有异常都回滚
public void pay(Long orderId) { ... }
生产案例:事务失效导致脏数据
症状:用户支付成功(第三方回调确认),但订单状态仍为"待支付"。运营每天收到几十单客诉。
排查过程:
- 查看代码发现
handleCallback()方法内部调用this.updateOrder() updateOrder()有@Transactional,但handleCallback()没加- 内部
this.调用绕过了代理,事务注解形同虚设 - updateOrder 中第一步 UPDATE 成功,第二步 INSERT 日志失败抛异常——但第一步的数据已经持久化了
解决:把 @Transactional(rollbackFor = Exception.class) 加到 handleCallback() 方法上,或使用 self.updateOrder() 经过代理。
💡 经验法则:任何涉及多表写入的方法,事务注解必须加在真正被外部调用的方法上。
4Spring MVC 请求全链路
一个 HTTP 请求在 Spring Boot 中经历了什么?
Filter是 Servlet 规范,在 DispatcherServlet 前后执行,不能直接用 Spring Bean。适合 CORS/编码/安全头。Interceptor是 Spring MVC 的,在 Controller 前后执行,可以 @Autowired。适合登录校验/权限/日志。
执行顺序:Filter → Interceptor.preHandle → Controller → Interceptor.postHandle → Filter。
DispatcherServlet 全链路
CORS/编码/鉴权
分发请求
登录校验/日志
处理请求
Filter vs Interceptor 详细对比
| 维度 | Filter | Interceptor |
|---|---|---|
| 规范 | Servlet 规范(javax.servlet) | Spring MVC(HandlerInterceptor) |
| 执行时机 | DispatcherServlet 前后 | Controller 方法前后 |
| 能否访问 Spring Bean | 需要手动注入 | 天然支持 @Autowired |
| 能否中断请求 | ✓ response.sendError() | ✓ preHandle return false |
| 典型用途 | CORS、编码、安全头 | 登录校验、权限检查、日志 |
| 执行顺序 | 先进后出(洋葱) | preHandle 顺序,postHandle 逆序 |
Filter 和 Interceptor 代码示例
// Filter: CORS 跨域配置
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
response.setHeader("Access-Control-Allow-Headers", "*");
chain.doFilter(req, res); // 放行
}
}
// 更推荐: 用 WebMvcConfigurer 统一配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://yourdomain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/login", "/api/register");
}
}
// Interceptor: 登录校验
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String token = req.getHeader("Authorization");
if (token == null || !tokenService.isValid(token)) {
res.setStatus(401);
return false; // 中断请求
}
return true; // 放行
}
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler, ModelAndView mv) {
// Controller 执行后, 视图渲染前
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) {
// 视图渲染后 (总是执行, 用于清理资源)
}
}
5线程池与异步编程
Java 并发是 Spring Boot 高性能的底层保障
任务来了 → 核心线程不满就新建执行 → 满了进队列排队 → 队列满了开临时线程(到 maxPoolSize)→ 连临时都满了执行拒绝策略。
阿里规约:禁用 Executors(无界队列/无限线程会 OOM),必须手动 new ThreadPoolExecutor + 有界队列。
ThreadPoolExecutor 7 大参数
corePoolSize=10 } maxPoolSize=20 } keepAlive=60s
核心线程
核心线程
10个核心
LinkedBlockingQueue (容量=500)
任务排队等待空闲线程
临时线程
临时线程
扩到20个
AbortPolicy: 抛 RejectedExecutionException
CallerRunsPolicy: 调用者线程自己跑
DiscardPolicy: 丢弃 | DiscardOldestPolicy: 丢最老的
线程池配置示例
@Configuration
@EnableAsync
public class ThreadPoolConfig {
// CPU 密集型: corePoolSize = CPU 核心数 + 1
// IO 密集型: corePoolSize = CPU 核心数 * 2 (或用公式: N * (1 + W/C))
// 实际项目中必须压测调优!
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程
executor.setMaxPoolSize(20); // 最大线程
executor.setQueueCapacity(500); // 队列容量
executor.setKeepAliveSeconds(60); // 临时线程空闲存活时间
executor.setThreadNamePrefix("biz-task-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略: 调用者自己跑
);
executor.setWaitForTasksToCompleteOnShutdown(true); // 优雅停机
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
症状:线上服务突然 OOM,jmap 发现堆积了大量 Runnable 等待对象。
原因:Executors.newFixedThreadPool() 底层用的是 无界队列 LinkedBlockingQueue(Integer.MAX_VALUE),任务无限堆积直到 OOM。
// ❌ 危险! 无界队列, 可能 OOM
ExecutorService pool = Executors.newFixedThreadPool(10);
// ✅ 正确! 手动指定有界队列
ThreadPoolExecutor pool = new ThreadPoolExecutor(
10, 20, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500), // 有界队列!
new ThreadPoolExecutor.CallerRunsPolicy()
);
// ⚠️ 同理: Executors.newCachedThreadPool() 最大线程数是 Integer.MAX_VALUE
// 也会导致创建大量线程而 OOM
@Async 与 CompletableFuture
| 方式 | 说明 | 适用场景 |
|---|---|---|
| @Async | 注解方式, 方法异步执行 | 简单异步(发邮件、写日志) |
| CompletableFuture | 编程式异步, 可编排 | 多任务编排、并行查询 |
| @Async + 返回 CompletableFuture | 两者结合 | 需要编排的异步任务 |
// @Async 基础用法
@Async("taskExecutor") // 指定线程池
public void sendEmail(String to, String content) {
// 在线程池中异步执行, 不阻塞主线程
mailSender.send(to, "Notification", content);
}
// CompletableFuture: 并行查询提高性能
// 场景:查用户详情需要查 3 张表(用户/订单/积分)
// 串行:50ms + 30ms + 40ms = 120ms
// 并行:三个同时跑,总耗时 = 最慢的那个 = 50ms
public UserDetail getUserDetail(Long userId) {
CompletableFuture<User> userFuture =
CompletableFuture.supplyAsync(
() -> userService.getById(userId),
taskExecutor);
CompletableFuture<List<Order>> orderFuture =
CompletableFuture.supplyAsync(
() -> orderService.getByUserId(userId),
taskExecutor);
CompletableFuture<Integer> pointsFuture =
CompletableFuture.supplyAsync(
() -> pointsService.getByUserId(userId),
taskExecutor);
CompletableFuture.allOf(
userFuture, orderFuture, pointsFuture
).join();
return new UserDetail(
userFuture.join(),
orderFuture.join(),
pointsFuture.join()
);
}
public UserDetail getUserDetail(Long userId) {
// ━━━ 第1步:发起3个异步任务 ━━━
// supplyAsync 写下去就立刻在子线程开始执行
// 返回的 xxxFuture 是"取件条",不是结果本身
CompletableFuture<User> userFuture =
CompletableFuture.supplyAsync(() -> userService.getById(userId), taskExecutor);
// ↑ 线程1 立刻查用户,主线程不等,拿取件条继续走
CompletableFuture<List<Order>> orderFuture =
CompletableFuture.supplyAsync(() -> orderService.getByUserId(userId), taskExecutor);
// ↑ 线程2 同时查订单
CompletableFuture<Integer> pointsFuture =
CompletableFuture.supplyAsync(() -> pointsService.getByUserId(userId), taskExecutor);
// ↑ 线程3 同时查积分。到这里3个并行跑着
// ━━━ 第2步:等全部跑完 ━━━
// 主线程在这里阻塞,直到3个都完成
CompletableFuture.allOf(userFuture, orderFuture, pointsFuture).join();
// ↑ .join() 就是"等"
// ━━━ 第3步:取出结果 ━━━
// 已跑完,.join() 瞬间返回,不会阻塞
return new UserDetail(
userFuture.join(), // 取出 User
orderFuture.join(), // 取出 List<Order>
pointsFuture.join() // 取出 Integer
);
}
// ━━━━ 关键认知 ━━━━
// supplyAsync(...) = 发起+立刻执行(不等,返回凭证)
// .join() = 等结果(阻塞直到完成)
// allOf(...).join() = 等全部完成
// xxxFuture = 不是结果,是"取件条"(快递单号)
6JVM 内存与 GC 调优
OOM 和 GC 停顿是 Java 生产环境最常见的问题
JVM 堆内存结构
-Xms / -Xmx
-XX:MetaspaceSize
-XX:MaxDirectMemorySize
JDK 8+: PermGen 已移除, 改为 Metaspace (使用本地内存)
常用 GC 参数
# JVM 启动推荐参数 (Spring Boot 生产环境)
java \
-Xms2g -Xmx2g \ # 初始/最大堆大小 (建议相等, 避免动态扩缩)
-XX:NewRatio=2 \ # 老年代:新生代 = 2:1
-XX:+UseG1GC \ # 使用 G1 收集器 (JDK 9+ 默认)
-XX:MaxGCPauseMillis=200 \ # 目标停顿时间 200ms
-XX:+HeapDumpOnOutOfMemoryError \ # OOM 时自动 dump
-XX:HeapDumpPath=/tmp/oom-dump.hprof \ # dump 文件路径
-XX:+PrintGCDetails -Xlog:gc*:/var/log/gc.log \ # GC 日志
-jar myapp.jar
OOM 排查实战
1. 静态集合无限增长
// ❌ static Map 只进不出, 永远不清理
public class UserSession {
private static final Map<String, User> sessions = new ConcurrentHashMap<>();
public static void put(String token, User user) {
sessions.put(token, user); // 放进去就再也不删了!
}
}
// ✅ 使用 Caffeine 缓存, 设置 TTL 自动淘汰
private final Cache<String, User> sessions = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterAccess(30, TimeUnit.MINUTES)
.build();
2. ThreadLocal 未清理
// ❌ 线程池中 ThreadLocal 不 remove, 内存泄漏
public class RequestContext {
private static final ThreadLocal<UserContext> context = new ThreadLocal<>();
}
// 在 Filter/Interceptor 中使用后必须清理
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object h, Exception e) {
RequestContext.context.remove(); // ✅ 关键! 线程池复用线程, 不清理会泄漏
}
3. 数据库连接 / 流未关闭
// ❌ 连接泄露
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT ...");
// 忘记 close! 连接池耗尽, 最终 OOM
// ✅ try-with-resources 自动关闭
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT ...")) {
// 使用 rs
} // 自动 close, 无需 finally
OOM 排查工具链
# 1. 查看进程概况
jps -l # 列出 Java 进程
jstat -gcutil <pid> 1000 10 # 每秒打印 GC 概况, 共 10 次
# 2. 导出堆内存快照
jmap -dump:format=b,file=heap.hprof <pid> # 手动 dump
# 或启动时加 -XX:+HeapDumpOnOutOfMemoryError 自动 dump
# 3. 查看对象统计
jmap -histo:live <pid> | head -20 # 存活对象 Top 20
# 4. 使用 Eclipse MAT 分析 dump 文件
# MAT 会自动检测泄漏嫌疑 (Leak Suspects Report)
# 查看 Dominator Tree, 找到占用最大的对象
# 5. 使用 Arthas 在线诊断 (不需要重启!)
$ java -jar arthas-boot.jar
[arthas@1234]$ dashboard # 实时面板
[arthas@1234]$ thread -n 3 # CPU 最高的 3 个线程
[arthas@1234]$ heapdump /tmp/dump.hprof # 在线 dump
[arthas@1234]$ vmtool --action getInstances --className java.util.HashMap --limit 10 # 查看对象实例
生产案例:内存从 500MB 涨到 2GB 后 OOM Killer
症状:Spring Boot 应用运行 12 小时后,Linux dmesg 显示 Out of memory: Killed process 1234 (java)。重启后重复。
排查过程:
- 启动参数已加
-XX:+HeapDumpOnOutOfMemoryError,但 JVM 还没触发 OOM 就被系统杀了 - 添加
process.memoryUsage等价的 JVM 监控:Prometheus + Micrometer 暴露jvm_memory_used_bytes - 发现堆内存呈阶梯式增长,每次定时任务执行后涨一截不回落
jmap -histo:live发现byte[]和String对象最多- 用 Arthas
heapdump导出快照,MAT 分析发现 ConcurrentHashMap 中存储了 800MB 的导出数据 - 根因:数据导出功能把全量数据加载到内存中的 Map,导出完成后没有清理引用
解决:改为流式导出(MyBatis Cursor + SXSSFWorkbook),内存占用从 800MB → 50MB。
// ❌ 旧代码: 全量加载到内存
List<Order> allOrders = orderMapper.selectAll(); // 100万条!
Map<Long, String> cache = new HashMap<>();
allOrders.forEach(o -> cache.put(o.getId(), o.toJson()));
// 导出后 cache 没有清理, 一直占用内存
// ✅ 新代码: 流式处理
try (Cursor<Order> cursor = orderMapper.scanAll();
SXSSFWorkbook wb = new SXSSFWorkbook(100)) { // 滑动窗口只保留100行
Sheet sheet = wb.createSheet("Orders");
int rowIdx = 0;
for (Order order : cursor) { // 每次只读一条
Row row = sheet.createRow(rowIdx++);
row.createCell(0).setCellValue(order.getId());
// 写完即释放, 内存恒定
}
}
💡 经验法则:任何"导出全部"功能都必须用流式处理,绝对不能一次性加载到内存。
7生产实践与优雅停机
不重视优雅停机 = 滚动更新丢请求
Graceful Shutdown(优雅停机)
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
@PreDestroy / DisposableBean
# application.yml - 优雅停机配置
server:
shutdown: graceful # 开启优雅停机 (默认: immediate)
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 最长等待 30 秒
# K8s deployment.yaml
spec:
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"] # 等 LB 摘除 Pod
readinessProbe: # 就绪探针
httpGet:
path: /actuator/health/readiness
port: 8080
livenessProbe: # 存活探针
httpGet:
path: /actuator/health/liveness
port: 8080
terminationGracePeriodSeconds: 45 # K8s 等待时间
症状:K8s 滚动更新时,用户偶发 502 Bad Gateway 和支付超时。
原因:Pod 收到 SIGTERM 后立即被杀死(没有配置 graceful shutdown),进行中的 HTTP 请求被中断。同时 K8s 还在往这个 Pod 发新请求(LB 没来得及摘除)。
解决:三管齐下:
- 开启
server.shutdown=graceful,等待请求完成 - K8s preStop hook
sleep 10,给 LB 时间摘除 - 配置 readinessProbe,收到 SIGTERM 后立即返回 NOT_READY
Actuator 监控端点
| 端点 | 说明 | 生产暴露 |
|---|---|---|
| /health | 健康检查(DB/Redis/MQ 状态) | ✓ (必须) |
| /info | 应用信息(版本/构建时间) | ✓ |
| /metrics | Prometheus 指标 | ✓ (监控) |
| /beans | 所有 Bean 列表 | ✗ (仅内部) |
| /env | 环境变量和配置 | ✗ (敏感信息) |
| /threaddump | 线程快照 | ✗ (排查时开) |
| /heapdump | 堆内存快照 | ✗ (排查时开) |
| /loggers | 动态日志级别 | ✗ (线上排障) |
# application.yml - Actuator 配置
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus # 只暴露安全的端点
exclude: env,beans,heapdump # 排除敏感端点
endpoint:
health:
show-details: when-authorized # 需要认证才显示详细信息
probes:
enabled: true # 开启 K8s 探针
metrics:
tags:
application: my-spring-boot-app
# 依赖
# implementation 'org.springframework.boot:spring-boot-starter-actuator'
# implementation 'io.micrometer:micrometer-registry-prometheus'
动态日志级别调整(不重启排障)
# 查看当前日志级别
curl http://localhost:8080/actuator/loggers/com.example
# 临时调高日志级别 (排查后调回去)
curl -X POST http://localhost:8080/actuator/loggers/com.example \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "DEBUG"}'
# 排查完毕, 调回 INFO
curl -X POST http://localhost:8080/actuator/loggers/com.example \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "INFO"}'
8生产实战速查
面试官最爱问的"你项目里怎么做的" —— 每个都是生产级代码模板
全局异常处理(生产必备)
不写全局异常处理 = 每个接口都要 try-catch = 代码里全是重复的异常处理。用一个 @RestControllerAdvice 统一兜底。
// ✅ 生产标准写法:统一异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
// 业务异常(自定义异常,带错误码)
@ExceptionHandler(BizException.class)
public Result> handleBiz(BizException e) {
log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
// 参数校验异常(@Valid 触发)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.error(400, msg); // 前端直接展示
}
// 兜底:所有未捕获的异常
@ExceptionHandler(Exception.class)
public Result> handleAll(Exception e) {
log.error("系统异常", e); // 记录完整堆栈
return Result.error(500, "系统繁忙,请稍后重试"); // 不暴露内部细节
}
}
// 自定义业务异常(带错误码,业务里直接 throw)
public class BizException extends RuntimeException {
private final int code;
public BizException(int code, String msg) { super(msg); this.code = code; }
public int getCode() { return code; }
}
// 业务代码里直接抛(不用 try-catch)
public void pay(Long orderId) {
if (order.getStatus() != PENDING) {
throw new BizException(40001, "订单状态不允许支付"); // 全局处理器接住
}
}
① handleAll(Exception.class) 里不要返回 e.getMessage() 给前端(会泄露 SQL/堆栈等敏感信息)。② handleValidation 的 message 要友好("手机号格式不正确" 而不是 "must match regex")。
参数校验(@Valid + JSR 303)
不用在业务方法里手写 if (name == null || name.isEmpty())。用注解一行搞定。
// 1. DTO 上加校验注解
public class OrderCreateDTO {
@NotBlank(message = "订单名称不能为空")
@Length(max = 50, message = "订单名称最多50字")
private String name;
@NotNull(message = "金额不能为空")
@DecimalMin(value = "0.01", message = "金额必须大于0")
private BigDecimal amount;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@NotNull(message = "商品列表不能为空")
@Size(min = 1, max = 100, message = "商品数量1-100")
@Valid // 嵌套校验(校验列表里每个元素的注解)
private List<OrderItemDTO> items;
@Email(message = "邮箱格式不正确")
private String email;
}
// 2. Controller 加 @Valid 触发校验
@PostMapping("/orders")
public Result<?> create(@Valid @RequestBody OrderCreateDTO dto) {
// 到这里说明校验全过了,不用再 if 判空
return Result.ok(orderService.create(dto));
}
// 3. 分组校验(同一DTO,不同场景不同校验规则)
public interface Create {} // 创建组
public interface Update {} // 更新组
public class UserDTO {
@Null(groups = Create.class) // 创建时必须为空(自动生成)
@NotNull(groups = Update.class) // 更新时必须不为空
private Long id;
@NotBlank(groups = {Create.class, Update.class})
private String name;
}
@PutMapping("/users")
public Result<?> update(@Validated(Update.class) @RequestBody UserDTO dto) { ... }
@NotBlank(字符串非空) / @NotNull(非null) / @NotEmpty(集合非空) / @Size(长度) / @Min/@Max(数值范围) / @Pattern(正则) / @Email / @Valid(嵌套)
统一返回格式
// 统一返回结构(所有接口返回这个)
@Data
public class Result<T> {
private int code; // 0=成功,非0=失败
private String message; // 提示信息
private T data; // 业务数据
private String traceId; // 链路ID(排查用)
public static <T> Result<T> ok(T data) { return build(0, "success", data); }
public static <T> Result<T> error(int code, String msg) { return build(code, msg, null); }
private static <T> Result<T> build(int c, String m, T d) {
Result<T> r = new Result<>();
r.code = c; r.message = m; r.data = d;
r.traceId = MDC.get("traceId"); // 从日志上下文取
return r;
}
}
// 分页结果(列表查询统一用)
@Data
public class PageResult<T> {
private List<T> list;
private long total;
private int page;
private int size;
// mybatis-plus 直接用 IPage,spring-data 用 Page
}
CORS 跨域配置
// 方式1:全局配置(推荐,一次配好所有接口)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://www.example.com", "http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true) // 允许带 Cookie
.maxAge(3600); // 预检请求缓存1小时
}
}
// 方式2:单个接口(@CrossOrigin,不推荐全局用)
@CrossOrigin(origins = "http://localhost:3000")
@RestController
public class UserController { ... }
// 方式3:生产环境推荐用 Nginx 反代(同域,彻底没有跨域问题)
// location /api/ { proxy_pass http://backend:8080/; }
① allowCredentials(true) 时 allowedOrigins 不能用 *(浏览器规范禁止),要用 allowedOriginPatterns。② Spring Security 环境下要在 SecurityConfig 里也配 CORS。
自定义 Starter(面试高频)
// 场景:封装公司内部的短信SDK,让别人引入 starter 就能用
// 1. 配置属性类
@Data
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
private String accessKey;
private String secretKey;
private String signName = "默认签名";
private int timeout = 3000;
}
// 2. 自动配置类
@Configuration
@ConditionalOnClass(SmsClient.class) // classpath有SmsClient才生效
@EnableConfigurationProperties(SmsProperties.class)
@ConditionalOnProperty(prefix = "sms", name = "enabled", havingValue = "true", matchIfMissing = true)
public class SmsAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 用户没自己定义才创建
public SmsClient smsClient(SmsProperties props) {
return new SmsClient(props.getAccessKey(), props.getSecretKey(), props.getTimeout());
}
}
// 3. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Boot 3.x)
// 或 META-INF/spring.factories(Boot 2.x)
// 内容:
// com.example.sms.SmsAutoConfiguration
// 4. 使用方只需 application.yml 配置 + pom 引入 starter
// sms:
// access-key: xxx
// secret-key: yyy
// 然后 @Autowired SmsClient 直接用
多环境配置 & 外部化
# 目录结构
src/main/resources/
├── application.yml # 公共配置
├── application-dev.yml # 开发环境
├── application-test.yml # 测试环境
└── application-prod.yml # 生产环境
# application.yml
spring:
profiles:
active: @profiles.active@ # Maven filter 注入
datasource:
url: jdbc:mysql://${DB_HOST:localhost}:3306/app # 环境变量+默认值
# 启动时指定环境
java -jar app.jar --spring.profiles.active=prod
# 或环境变量
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
# 或 Docker
docker run -e SPRING_PROFILES_ACTIVE=prod myapp
# 敏感配置不放代码库(用环境变量/Nacos/Vault)
spring:
datasource:
password: ${DB_PASSWORD} # 从环境变量读,不写死
接口文档(Knife4j / SpringDoc)
// Spring Boot 3.x 用 springdoc-openapi(替代旧版 Swagger)
// pom: springdoc-openapi-starter-webmvc-ui
// Controller 注解
@Tag(name = "订单管理", description = "订单CRUD")
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Operation(summary = "创建订单")
@Parameters({@Parameter(name = "X-Token", description = "登录令牌", in = ParameterIn.HEADER, required = true)})
@PostMapping
public Result<OrderVO> create(@Valid @RequestBody OrderCreateDTO dto) { ... }
}
// DTO 字段描述
@Schema(description = "订单创建请求")
public class OrderCreateDTO {
@Schema(description = "订单名称", example = "iPhone 15", requiredMode = REQUIRED)
@NotBlank
private String name;
}
// 访问 http://localhost:8080/swagger-ui.html 或 /doc.html (Knife4j)
文件上传 & 下载
// 上传(限制大小 + 类型)
@PostMapping(value = "/upload", consumes = "multipart/form-data")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
// 1. 校验
if (file.isEmpty()) throw new BizException(400, "文件不能为空");
if (file.getSize() > 10 * 1024 * 1024) throw new BizException(400, "文件不能超过10MB");
String suffix = FilenameUtils.getExtension(file.getOriginalFilename());
if (!Arrays.asList("jpg", "png", "pdf").contains(suffix.toLowerCase()))
throw new BizException(400, "不支持的文件类型");
// 2. 存储到 OSS(不要存本地,容器重启就没了)
String url = ossClient.upload(file.getInputStream(), "order/" + UUID.randomUUID() + "." + suffix);
return Result.ok(url);
}
// application.yml 限制上传大小
// spring:
// servlet:
// multipart:
// max-file-size: 10MB
// max-request-size: 50MB
// 大文件下载(流式输出,不撑内存)
@GetMapping("/download/{id}")
public void download(@PathVariable Long id, HttpServletResponse response) throws IOException {
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"report.xlsx\"");
orderService.exportExcel(id, response.getOutputStream()); // 流式写
}
Redis 序列化配置(生产必改)
// Spring Boot 默认用 JdkSerializationRedisSerializer
// 问题:存进去是乱码(二进制),redis-cli 看不懂,不同语言不能共用
// 生产标配:改成 JSON 序列化
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key 用 String 序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// Value 用 JSON 序列化(可读,跨语言)
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.activateDefaultTyping(om.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL); // 保留类型信息
jsonSerializer.setObjectMapper(om);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
定时任务
// 单机定时任务(简单场景够用)
@EnableScheduling
@SpringBootApplication
public class App { ... }
@Component
public class ReportTask {
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void generateDailyReport() {
log.info("开始生成日报");
reportService.generate();
}
@Scheduled(fixedDelay = 60000) // 上次执行完1分钟后再次执行
public void syncData() { ... }
@Scheduled(fixedRate = 5000) // 每5秒(不管上次有没有执行完)
public void heartbeat() { ... }
}
// 分布式定时任务(多实例只执行一次)
// 方案1:XXL-JOB / ElasticJob(专业调度平台)
// 方案2:Redis 分布式锁(轻量)
@Scheduled(cron = "0 0 2 * * ?")
public void generateReport() {
RLock lock = redisson.getLock("lock:report:daily");
if (!lock.tryLock(0, 30, TimeUnit.MINUTES)) return; // 没抢到锁就跳过
try { reportService.generate(); }
finally { lock.unlock(); }
}
① @Scheduled 默认单线程执行,多个任务会互相阻塞。配 spring.task.scheduling.pool.size=5 改成多线程。② 多实例部署时同一任务会被执行多次,必须加分布式锁。
9面试题精选 30 道
覆盖基础、核心机制、生产实战,每题附答案框架与追问
@SpringBootApplication = @SpringBootConfiguration(标记配置类)+ @EnableAutoConfiguration(开启自动配置)+ @ComponentScan(包扫描)。自动配置通过 spring.factories 中声明的 AutoConfiguration 类,配合 @Conditional 条件注解决定是否加载。@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})--spring.profiles.active=prod。也可以在代码中用 @Profile("dev") 控制 Bean 的加载。@ConfigurationProperties 可以配合 Profile 实现不同环境的配置映射。--debug 或 logging.level.org.springframework=DEBUG;2) Spring Boot 2.x+ 使用 ApplicationStartup 记录启动步骤耗时;3) 检查 @ComponentScan 范围是否过大;4) 检查是否有耗时的 @PostConstruct 方法;5) 检查数据库连接池初始化(HikariCP 初始化连接);6) jfr(Java Flight Recorder)录制启动过程分析。1. OOM:全量导出加载到内存 → 改为 MyBatis Cursor + EasyExcel 流式处理
2. 事务失效:内部 this 调用 → 注入 self 或 @Transactional 加在外部方法上
3. N+1 查询:循环中逐条查 DB → 批量查询 + MyBatis foreach + 二次组装
4. 线程池耗尽:无界队列堆积 → 有界队列 + CallerRunsPolicy + 监控告警
5. GC 停顿:Young 区太小导致频繁 Minor GC → 调大 -Xmn 或换 G1
回答技巧:STAR 法则(Situation → Task → Action → Result),量化结果("响应时间从 3s 降到 200ms")。
@ConfigurationProperties 批量绑定整个对象,支持松散绑定(my-app-name → myAppName)、JSR303 校验、Map/List。生产推荐 @ConfigurationProperties(结构化、可维护)。@RestControllerAdvice + @ExceptionHandler。分三层处理:① 业务异常(自定义 BizException,返回业务错误码)② 参数校验异常(MethodArgumentNotValidException,返回字段+错误信息)③ 兜底异常(Exception.class,记录堆栈但不暴露给前端)。AutoConfiguration.imports ④ 原生支持 GraalVM Native Image ⑤ Micrometer 替代 Actuator 指标。迁移注意:所有 javax.servlet.* 要改成 jakarta.servlet.*。TaskDecorator,在提交任务时复制主线程上下文到子线程:executor.setTaskDecorator(runnable -> {
RequestAttributes ctx = RequestContextHolder.getRequestAttributes();
String traceId = MDC.get("traceId");
return () -> {
try {
if (ctx != null) RequestContextHolder.setRequestAttributes(ctx);
if (traceId != null) MDC.put("traceId", traceId);
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
MDC.clear();
}
};
});Spring Security 场景用 DelegatingSecurityContextAsyncTaskExecutor 自动传递。| 阶段 | 内容 | 推荐资源 |
|---|---|---|
| 入门 | IoC/DI/AOP 基础、Spring Boot 脚手架 | Spring Guides (spring.io) |
| 进阶 | Bean 生命周期、AOP 原理、事务原理 | Spring 源码深度解析 (书) |
| 高级 | JVM 调优、自动配置源码、线程池调优 | 极客时间《深入拆解 Java 虚拟机》 |
| 生产 | 监控告警、GC 调优、分布式架构 | Spring Boot Production-Ready (docs) |