Spring 工程实践与面试综合:6 期系列收官之作
本文是 “Spring 全家桶源码级深度解析” 系列的第 6 期(最终期),融合工程化规范、线上排查实战、30 道综合面试题和 EduLearn 完整复盘。
前 5 期回顾:第1期 IoC/DI | 第2期 自动配置 | 第3期 Spring MVC | 第4期 AOP 深度解析| 第5期 事务
目录
- 开篇:代码跑通了,但能上线吗?
- 一、EduLearn 工程化规范
- 1.1 分层架构:四层分离,单向依赖
- 1.2 统一返回体:ApiResponse
- 1.3 全局异常处理:@ControllerAdvice
- 1.4 参数校验:@Valid + JSR 303
- 1.5 日志规范
- 1.6 配置管理:application.yml 环境隔离
- 二、线上问题排查实战
- 2.1 OOM / 内存泄漏
- 2.2 CPU 飙升 100%
- 2.3 死锁
- 2.4 慢 SQL
- 2.5 接口响应慢
- 2.6 连接池耗尽
- 三、EduLearn 完整复盘
- 3.1 模块串联示例:用户下单全流程
- 四、30 道 Spring 综合面试题库
- IoC / DI(第1期)
- 自动配置(第2期)
- Spring MVC(第3期)
- AOP(第4期)
- 事务(第5期)
- 工程实践(第6期)
- 综合追问链
- 五、6 期系列总览
开篇:代码跑通了,但能上线吗?
前面 5 期,我们从 IoC 容器一路写到事务源码,用 EduLearn 在线教育平台串联了所有技术点。但写完代码只是起点——上线后才是真正的考验。
凌晨 3 点,CPU 突然飙到 100%;用户投诉下单后迟迟没反应;运维说内存一周涨了 2G……这些不是面试题,是真实的生产事故。
本期作为收官之作,不谈单个技术原理,而是把 5 期知识串成一个可落地的工程体系:分层架构怎么搭、异常怎么统一处理、问题怎么排查、面试怎么回答。
一、EduLearn 工程化规范
1.1 分层架构:四层分离,单向依赖
Controller → Service → DAO/Mapper → DB ↓ ↓ ↓ 统一返回体 事务管理 MyBatis XML 参数校验 AOP横切 PageHelper 全局异常铁律:上层依赖下层,下层绝不反向依赖上层。Service 不能引入 Controller 包,Mapper 不能引入 Service 包。
1.2 统一返回体:ApiResponse
没有统一返回体的 API 是灾难——每个接口返回格式不同,前端需要写 N 套解析逻辑。
@Data@NoArgsConstructor@AllArgsConstructorpublicclassApiResponse<T>{privateintcode;// 业务状态码privateStringmessage;// 提示信息privateTdata;// 响应数据privatelongtimestamp;// 时间戳publicstatic<T>ApiResponse<T>success(Tdata){returnnewApiResponse<>(200,"success",data,System.currentTimeMillis());}publicstatic<T>ApiResponse<T>error(intcode,Stringmessage){returnnewApiResponse<>(code,message,null,System.currentTimeMillis());}}Controller 层只返回ApiResponse:
@GetMapping("/courses/{id}")publicApiResponse<CourseVO>getCourse(@PathVariableLongid){returnApiResponse.success(courseService.getById(id));}1.3 全局异常处理:@ControllerAdvice
不要在每个 Controller 里写 try-catch——用@ControllerAdvice一刀切。
@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(MethodArgumentNotValidException.class)publicApiResponse<?>handleValidation(MethodArgumentNotValidExceptionex){Stringmsg=ex.getBindingResult().getFieldErrors().stream().map(e->e.getField()+": "+e.getDefaultMessage()).collect(Collectors.joining("; "));returnApiResponse.error(400,"参数校验失败: "+msg);}@ExceptionHandler(BusinessException.class)publicApiResponse<?>handleBusiness(BusinessExceptionex){returnApiResponse.error(ex.getCode(),ex.getMessage());}@ExceptionHandler(Exception.class)publicApiResponse<?>handleUnknown(Exceptionex){log.error("未知异常",ex);returnApiResponse.error(500,"服务器内部错误");}}自定义业务异常:
publicclassBusinessExceptionextendsRuntimeException{privatefinalintcode;publicBusinessException(intcode,Stringmessage){super(message);this.code=code;}publicintgetCode(){returncode;}}这样 Service 层只需throw new BusinessException(10001, "库存不足"),Controller 层零 try-catch。
1.4 参数校验:@Valid + JSR 303
@PostMapping("/orders")publicApiResponse<OrderVO>createOrder(@Valid@RequestBodyOrderDTOdto){returnApiResponse.success(orderService.create(dto));}@DatapublicclassOrderDTO{@NotNull(message="用户ID不能为空")privateLonguserId;@NotNull(message="课程ID不能为空")privateLongcourseId;@Min(value=1,message="数量至少为1")@Max(value=10,message="单次最多购买10门")privateintcount;}校验失败抛出的MethodArgumentNotValidException由GlobalExceptionHandler统一捕获,返回格式化错误信息。
1.5 日志规范
logging:level:root:INFOcom.edulearn:DEBUG# 业务包org.springframework.transaction:TRACE# 事务日志(排查用)org.springframework.web:DEBUG# MVC 请求日志pattern:console:"%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"日志使用规范:
| 级别 | 使用场景 |
|---|---|
| ERROR | 需要人工介入的异常(支付失败、数据库宕机) |
| WARN | 可自动恢复的异常(重试成功、降级处理) |
| INFO | 关键业务流程(下单、支付、退款) |
| DEBUG | 调试信息(方法入参、SQL 参数、缓存命中) |
禁止事项:①log.info里调用方法(如log.info("user: {}", user.toString())——toString在生产也会执行);② 循环内打 INFO 日志;③e.printStackTrace()(改用log.error("msg", e))。
// 正确log.info("订单创建成功, orderId={}, userId={}",order.getId(),dto.getUserId());// 错误log.info("订单创建成功"+order);// 字符串拼接浪费性能1.6 配置管理:application.yml 环境隔离
application.yml # 公共配置 application-dev.yml # 开发环境 application-test.yml # 测试环境 application-prod.yml # 生产环境通过spring.profiles.active=prod切换。敏感信息(数据库密码、API Key)不要硬编码,用环境变量或配置中心:
spring:datasource:password:${DB_PASSWORD}# 从环境变量读取二、线上问题排查实战
2.1 OOM / 内存泄漏
现象:应用运行几小时后崩溃,java.lang.OutOfMemoryError: Java heap space。
排查流程:
# 1. 找到 Java 进程jps-lv# 2. 看堆使用情况jmap-heapPID# 3. 查看存活对象 Top 20jmap-histo:livePID|head-20# 4. 如果上面看不出,导出堆快照jmap-dump:format=b,file=heap.hprof PID用 Eclipse MAT(Memory Analyzer Tool)打开heap.hprof,查看 Dominator Tree,找到占用内存最大的对象。
经典案例——ThreadLocal 内存泄漏:
// 问题代码publicclassRequestContext{privatestaticThreadLocal<User>currentUser=newThreadLocal<>();// 使用后没有 remove() !Tomcat 线程池复用线程 → 线程不消亡 → ThreadLocal 永远不回收}// 正确做法publicclassRequestContext{privatestaticThreadLocal<User>currentUser=newThreadLocal<>();publicstaticvoidclear(){currentUser.remove();// 在 Filter/Interceptor 的 afterCompletion 中调用}}预防:设置 JVM 参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/heapdump.hprof,OOM 时自动 dump。
2.2 CPU 飙升 100%
排查流程:
# 1. 找到进程内高 CPU 线程top-HpPID# 2. 记录高 CPU 线程的 TID(十进制),转十六进制printf'%x\n'TID# 3. 用 jstack 看这个线程在干什么jstack PID|grep-A20十六进制TIDArthas 一键版(强烈推荐):
# 启动 Arthasjava-jararthas-boot.jar# 查看最忙的 3 个线程dashboard# 直接找当前阻塞其他线程的元凶thread-b# 观察方法耗时trace com.edulearn.service.OrderService createOrder常见原因:
- 死循环(
while(true)没有 sleep) - 正则表达式回溯(如
(a+)+b匹配长字符串) - 频繁 Full GC(堆快满了,GC 线程持续工作 → 看 GC 日志)
2.3 死锁
现象:请求卡住不返回,线程数持续增长。
# jstack 直接帮你找到死锁jstack PID|grep"Found one Java-level deadlock"-A50输出会明确指出哪些线程持有哪些锁、等待哪些锁:
Found one Java-level deadlock: ============================== "Thread-1": waiting to lock monitor 0x00007f8a1c001a58 (object 0x000000076abcf1d0, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007f8a1c003f58 (object 0x000000076abcf1e0, a java.lang.Object), which is held by "Thread-1"Arthas 版:thread -b直接输出死锁链。
根因与预防:
- 统一加锁顺序(所有地方先锁 A 再锁 B)
- 减小锁粒度(不要锁整个方法,只锁关键代码块)
- 用
ReentrantLock.tryLock(timeout, unit)替代synchronized
2.4 慢 SQL
# 1. 找到正在执行的慢查询SHOW FULL PROCESSLIST;# 2. 启用慢查询日志SET GLOBAL slow_query_log=ON;SET GLOBAL long_query_time=1;-- 超过1秒记录# 3. 分析执行计划EXPLAIN SELECT * FROM orders WHERE user_id=123AND status='PAID';EXPLAIN 关键字段:
| 字段 | 含义 | 理想值 |
|---|---|---|
| type | 访问类型 | const > ref > range > index > ALL |
| key | 使用的索引 | 非空 |
| rows | 扫描行数 | 越小越好 |
| Extra | 额外信息 | 避免 Using filesort / Using temporary |
常见慢 SQL 优化:
- 缺少索引 →
CREATE INDEX idx_user_status ON orders(user_id, status) SELECT *→ 只查需要的列LIMIT 100000, 20深分页 → 改用游标分页(WHERE id > lastId LIMIT 20)- JOIN 字段类型不一致(隐式转换导致全表扫描)
- 未利用覆盖索引 →
EXPLAIN看 Extra 是否是Using index
2.5 接口响应慢
先用 SkyWalking / Pinpoint 等 APM 工具定位到具体接口,再用 Arthas trace:
arthas>trace com.edulearn.controller.OrderController createOrder-n5输出完整的调用链和每一层的耗时,一眼看出瓶颈在 Service、DAO 还是第三方调用。
2.6 连接池耗尽
# 连接池配置(HikariCP 默认)spring.datasource.hikari.maximum-pool-size=20spring.datasource.hikari.connection-timeout=30000如果日志出现Connection is not available, request timed out after 30000ms,排查:
- 是否有连接泄漏(获取连接后没关,可用
jstack查看线程是否 BLOCKED 在getConnection) - 最大连接数是否太小
- 是否有慢 SQL 长时间持有连接
三、EduLearn 完整复盘
6 期系列贯穿的 EduLearn 在线教育平台,最终架构如下:
| 模块 | 核心功能 | 用到的主要技术 |
|---|---|---|
| 用户模块 | 注册登录、JWT认证、角色权限、手机验证码 | IoC(MVC注入)、MVC(RESTful Controller)、AOP(日志切面)、Spring Security |
| 课程模块 | CRUD、ES搜索、分类标签、Redis缓存 | IoC(Bean管理)、自动配置(ES Starter)、MVC(分页查询) |
| 订单模块 | 下单、支付回调、退款、库存扣减 | 事务(核心)、AOP(日志)、MVC(RESTful) |
| 学习模块 | 课程进度、笔记、视频播放、作业提交 | MVC(文件上传)、自动配置(OSS Starter) |
| 消息模块 | 站内信、推送通知、RabbitMQ异步 | 事务(REQUIRES_NEW)、消息队列 |
技术栈全貌:
Spring Boot 2.7.x # 基础框架 ├── Spring IoC/DI # 第1期:Bean生命周期、依赖注入、自动装配 ├── Spring Boot 自动配置 # 第2期:Starter机制、条件注解、配置绑定 ├── Spring MVC # 第3期:DispatcherServlet、拦截器、RESTful ├── Spring AOP # 第4期:动态代理、切面编程、通知顺序 ├── Spring 事务 # 第5期:@Transactional、传播行为、失效场景 ├── Spring Security + JWT # 认证与授权 ├── MyBatis / MyBatis-Plus # 数据访问层 ├── Redis + Spring Cache # 缓存 ├── Elasticsearch # 全文搜索 ├── RabbitMQ # 消息队列 └── Actuator + Prometheus # 监控3.1 模块串联示例:用户下单全流程
这是整个系统最复杂的业务流程,串联了 5 个模块的技术点:
1. [用户模块] JWT 解析用户身份 → SecurityContext 2. [Controller] @Valid 校验 OrderDTO 参数 → 统一异常处理兜底 3. [AOP] @LogAspect 记录接口调用日志 4. [Service - 事务] @Transactional 开启事务 ├── [课程模块] 查询课程信息 + Redis 缓存预热 ├── [订单模块] 创建订单记录 → MyBatis insert ├── [课程模块] 扣减库存 → REQUIRED 加入当前事务 ├── [订单模块] 扣减余额 → MANDATORY 要求事务存在 └── [消息模块] 发送通知 → REQUIRES_NEW 独立事务 5. [事务] commit → 数据落地;rollback → 全部回滚 6. [AOP] @LogAspect 记录耗时和结果 7. [Controller] 返回 ApiResponse<OrderVO>四、30 道 Spring 综合面试题库
覆盖 6 期内容,按模块分类。
IoC / DI(第1期)
Q1:Spring IoC 容器的启动流程?
A:new AnnotationConfigApplicationContext(Config.class)→ 注册配置类 →refresh()→invokeBeanFactoryPostProcessors解析@ComponentScan和@Bean→finishBeanFactoryInitialization实例化所有单例 Bean。全部单例 Bean 在容器启动时创建(非懒加载),通过三级缓存解决循环依赖。
Q2:@Autowired和@Resource的区别?
A:@Autowired是 Spring 注解,默认按类型注入,配合@Qualifier按名称;@Resource是 JSR-250 标准,默认按名称,找不到再按类型。@Autowired可标记required=false。
Q3:Spring 如何解决循环依赖?
A:三级缓存。一级singletonObjects(成品)、二级earlySingletonObjects(半成品)、三级singletonFactories(ObjectFactory)。A 创建时提前暴露 ObjectFactory 到三级缓存 → B 创建时依赖 A,从三级缓存获取 A 的早期引用 → B 创建完成 → A 继续完成创建。
Q4:@Configuration和@Component的区别?
A:@Configuration标注的类会被 CGLIB 增强,@Bean方法内部调用其他@Bean方法时返回的是容器中的同一个实例(单例保证)。@Component没有这个增强,内部调用@Bean方法会创建新对象。
Q5:FactoryBean和BeanFactory的区别?
A:BeanFactory是 IoC 容器的顶层接口(工厂的工厂)。FactoryBean是生产特定类型 Bean 的工厂,常用于框架集成(如SqlSessionFactoryBean生产SqlSession)。
自动配置(第2期)
Q6:@SpringBootApplication包含哪三个注解?
A:@SpringBootConfiguration(=@Configuration)、@EnableAutoConfiguration(自动配置核心)、@ComponentScan(组件扫描)。
Q7:自动配置的实现原理?
A:@EnableAutoConfiguration→@Import(AutoConfigurationImportSelector.class)→ 读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports→ 加载所有自动配置类 → 每个配置类用@ConditionalOnClass、@ConditionalOnMissingBean等条件注解判断是否生效。
Q8:如何自定义一个 Starter?
A:① 创建xxx-spring-boot-autoconfigure模块,写自动配置类用@ConditionalOnClass控制生效条件;② 创建xxx-spring-boot-starter模块(空项目,引入 autoconfigure 模块);③ 在 autoconfigure 的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中声明配置类全限定名。
Spring MVC(第3期)
Q9:DispatcherServlet 的处理流程?
A:请求 →HandlerMapping找到 Handler(Controller 方法)→HandlerAdapter执行 →HandlerMethodArgumentResolver解析参数(@RequestBody/@RequestParam等)→ 执行 Controller 方法 →HttpMessageConverter序列化返回值 → 返回响应。中途经过HandlerInterceptor的preHandle/postHandle/afterCompletion。
Q10:拦截器(Interceptor)和过滤器(Filter)的区别?
A:Filter 是 Servlet 规范,在请求进入 Servlet 前执行,不感知 Spring 上下文;Interceptor 是 Spring 机制,在 HandlerMapping 之后、Controller 执行前后执行,能拿到 HandlerMethod 和 Spring Bean。Filter 在最外层,Interceptor 在 DispatcherServlet 内部。
Q11:@RequestBody和@ResponseBody的工作原理?
A:通过HttpMessageConverter做序列化/反序列化。Spring Boot 默认使用 Jackson(MappingJackson2HttpMessageConverter)处理 JSON。@RequestBody在参数解析阶段调用read()反序列化;@ResponseBody在返回值处理阶段调用write()序列化。
AOP(第4期)
Q12:JDK 动态代理和 CGLIB 的区别,Spring 如何选择?
A:JDK 基于接口 +Proxy.newProxyInstance+InvocationHandler,目标类必须实现接口;CGLIB 基于继承 +Enhancer+MethodInterceptor,通过生成子类覆写方法实现。Spring Boot 2.x 默认用 CGLIB。如果目标类实现了接口,也可以显式设置spring.aop.proxy-target-class=false来用 JDK 代理。
Q13:同一个切面中多个通知的执行顺序?
A:@Aroundstart →@Before→ 目标方法 →@AfterReturning/@AfterThrowing→@After→@Aroundend。@After无论异常都会执行(类似 finally),@AfterReturning只在正常返回时执行。
Q14:多切面作用在同一方法时,执行顺序如何控制?
A:通过@Order(n)控制,数值越小优先级越高。@Around是嵌套结构——外层切面的 Around 包裹内层切面的 Around,最后才到目标方法。
事务(第5期)
Q15:@Transactional失效的常见场景(至少说 3 个)?
A:① 自调用(this.method()不经过代理);② 方法非 public;③ 异常被 try-catch 吞掉;④ Checked Exception 默认不回滚,需rollbackFor = Exception.class;⑤ 多线程/@Async场景(ThreadLocal 线程隔离)。
Q16:7 种传播行为,至少说 3 个常用的?
A:REQUIRED(默认,同生共死)、REQUIRES_NEW(独立事务,挂起外部)、NESTED(保存点,部分回滚)。前两者高频,NESTED 依赖 JDBC savepoint。
Q17:REQUIRED和REQUIRES_NEW的本质区别?
A:REQUIRED加入外部事务,共用同一个 Connection,同一批操作同生共死。REQUIRES_NEW挂起外部连接,从 DataSource 获取新连接,开启独立事务——子事务的提交/回滚完全不影响外部。
Q18:事务怎么做到线程隔离的?为什么多线程会失效?
A:TransactionSynchronizationManager用ThreadLocal<Map<Object, Object>>存储当前线程的 DataSource→Connection 映射。新线程拿不到父线程的 ThreadLocal 数据,自然不在同一事务中。
工程实践(第6期)
Q19:你们项目的分层架构是怎样的?
A:Controller → Service → DAO/Mapper → DB。Controller 负责参数校验和统一返回,Service 负责业务逻辑和事务管理,DAO 负责数据访问。横切关注点(日志、异常、权限)通过 AOP 和全局异常处理统一管理。
Q20:线上 CPU 飙升,你的排查步骤?
A:top -Hp PID找到高 CPU 线程 →printf '%x' TID转十六进制 →jstack PID | grep 十六进制定位代码 → 或者 Arthasthread -n 3一键看最忙线程。常见原因:死循环、正则回溯、频繁 Full GC。
Q21:线上内存泄漏怎么排查?
A:jmap -histo:live PID | head -20看存活对象分布;如果不明显,jmap -dump导出堆快照,MAT 的 Dominator Tree 分析最大占用。注意 ThreadLocal 没 remove 导致的内存泄漏——tomcat 线程池复用导致线程不消亡。
Q22:慢 SQL 排查流程?
A:SHOW FULL PROCESSLIST看正在执行的 SQL → 启用慢查询日志long_query_time=1→EXPLAIN分析执行计划,关注type(避免 ALL)、key(必须有索引)、rows(越小越好)、Extra(避免 filesort/temporary)→ 加索引、优化 JOIN、避免SELECT *。
Q23:全局异常处理怎么做?
A:@RestControllerAdvice+@ExceptionHandler。自定义BusinessException(code, message),各层统一 throw,由全局处理器转为ApiResponse.error()返回。Controller 层零 try-catch。
Q24:Spring Security + JWT 的认证流程?
A:登录接口 → 验证用户名密码 → 生成 JWT token(含 userId、角色、过期时间)→ 返回 token。后续请求在 Header 带Authorization: Bearer token→ JWT 过滤器解析 token → 验证签名和过期时间 → 将用户信息放入 SecurityContext。
Q25:Redis 在你们项目中的使用场景?
A:① 课程详情缓存(@Cacheable,TTL 30分钟);② 分布式锁(SETNX,下单防超卖);③ 用户 session 存储(JWT 黑名单);④ 排行榜(ZSet,按学习时长排序)。
综合追问链
Q26:从浏览器输入 URL 到 Spring Boot 返回 JSON,中间经过哪些层?
A:Nginx 反向代理 → Tomcat 线程池接收 → Filter Chain → DispatcherServlet → HandlerMapping → HandlerInterceptor.preHandle → Controller 方法(参数解析 + 校验)→ Service(事务、缓存、AOP)→ DAO → DB → 返回值序列化(Jackson)→ HandlerInterceptor.postHandle/afterCompletion → Response。
Q27:Spring 中 Bean 的生命周期?
A:实例化 → 属性填充(依赖注入)→BeanNameAware/BeanFactoryAware→BeanPostProcessor.postProcessBeforeInitialization→@PostConstruct/InitializingBean.afterPropertiesSet→BeanPostProcessor.postProcessAfterInitialization(AOP 代理在此生成)→ 就绪 → 容器关闭时@PreDestroy/DisposableBean.destroy。
Q28:Spring Boot 如何实现 “约定优于配置”?
A:通过自动配置机制。Spring Boot 预设了合理的默认值(如 HikariCP 默认最大连接数 10,Tomcat 默认端口 8080),开发者只需在 application.yml 中覆盖需要的配置项。这种"默认即最佳实践"的理念大大减少了 XML 配置和样板代码。
Q29:你对 Spring 6 / Spring Boot 3.x 的升级了解吗?
A:最大变化是基线升级到 JDK 17 和 Jakarta EE 9(javax.*→jakarta.*)。AOT 编译和 GraalVM Native Image 支持是性能亮点。此外spring.factories改为AutoConfiguration.imports文件格式。如果你的项目还在 JDK 8/11,升级时需要处理包名变更和弃用 API。
Q30:如果让你从零搭建一个 Spring Boot 项目,你的技术选型清单?
A:Spring Boot 3.x + JDK 17 + MySQL 8.x + MyBatis-Plus + Redis + RabbitMQ + JWT + Actuator + Prometheus + Grafana。分层:Controller → Service → DAO。横切:全局异常处理 + AOP 日志 + 统一返回体。测试:JUnit 5 + Mockito。CI/CD:GitLab CI + Docker + K8s。
五、6 期系列总览
| 期数 | 主题 | 核心知识点 |
|---|---|---|
| 第1期 | IoC / DI | 容器启动流程、Bean生命周期、三级缓存、循环依赖 |
| 第2期 | 自动配置 | Starter机制、条件注解、配置绑定、自定义Starter |
| 第3期 | Spring MVC | DispatcherServlet、拦截器vs过滤器、RESTful、参数解析 |
| 第4期 | AOP | JDK vs CGLIB、切面通知顺序、@Order源码走读 |
| 第5期 | 事务 | 7种传播行为、5大失效场景、TransactionInterceptor源码 |
| 第6期 | 工程综合 | 分层架构、异常处理、排查实战、30道面试题、项目复盘 |
学习路线建议:按顺序阅读,第1-3期打基础,第4-5期攻源码,第6期做串联和面试冲刺。