【Spring】面试突击系列(六):Spring 工程实践与面试综合
Spring 工程实践与面试综合6 期系列收官之作本文是 “Spring 全家桶源码级深度解析” 系列的第 6 期最终期融合工程化规范、线上排查实战、30 道综合面试题和 EduLearn 完整复盘。前 5 期回顾第1期 IoC/DI | 第2期 自动配置 | 第3期 Spring MVC | 第4期 AOP 深度解析| 第5期 事务目录开篇代码跑通了但能上线吗一、EduLearn 工程化规范1.1 分层架构四层分离单向依赖1.2 统一返回体ApiResponse1.3 全局异常处理ControllerAdvice1.4 参数校验Valid JSR 3031.5 日志规范1.6 配置管理application.yml 环境隔离二、线上问题排查实战2.1 OOM / 内存泄漏2.2 CPU 飙升 100%2.3 死锁2.4 慢 SQL2.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 套解析逻辑。DataNoArgsConstructorAllArgsConstructorpublicclassApiResponseT{privateintcode;// 业务状态码privateStringmessage;// 提示信息privateTdata;// 响应数据privatelongtimestamp;// 时间戳publicstaticTApiResponseTsuccess(Tdata){returnnewApiResponse(200,success,data,System.currentTimeMillis());}publicstaticTApiResponseTerror(intcode,Stringmessage){returnnewApiResponse(code,message,null,System.currentTimeMillis());}}Controller 层只返回ApiResponseGetMapping(/courses/{id})publicApiResponseCourseVOgetCourse(PathVariableLongid){returnApiResponse.success(courseService.getById(id));}1.3 全局异常处理ControllerAdvice不要在每个 Controller 里写 try-catch——用ControllerAdvice一刀切。RestControllerAdvicepublicclassGlobalExceptionHandler{ExceptionHandler(MethodArgumentNotValidException.class)publicApiResponse?handleValidation(MethodArgumentNotValidExceptionex){Stringmsgex.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.codecode;}publicintgetCode(){returncode;}}这样 Service 层只需throw new BusinessException(10001, 库存不足)Controller 层零 try-catch。1.4 参数校验Valid JSR 303PostMapping(/orders)publicApiResponseOrderVOcreateOrder(ValidRequestBodyOrderDTOdto){returnApiResponse.success(orderService.create(dto));}DatapublicclassOrderDTO{NotNull(message用户ID不能为空)privateLonguserId;NotNull(message课程ID不能为空)privateLongcourseId;Min(value1,message数量至少为1)Max(value10,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.activeprod切换。敏感信息数据库密码、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:formatb,fileheap.hprof PID用 Eclipse MATMemory Analyzer Tool打开heap.hprof查看 Dominator Tree找到占用内存最大的对象。经典案例——ThreadLocal 内存泄漏// 问题代码publicclassRequestContext{privatestaticThreadLocalUsercurrentUsernewThreadLocal();// 使用后没有 remove() Tomcat 线程池复用线程 → 线程不消亡 → ThreadLocal 永远不回收}// 正确做法publicclassRequestContext{privatestaticThreadLocalUsercurrentUsernewThreadLocal();publicstaticvoidclear(){currentUser.remove();// 在 Filter/Interceptor 的 afterCompletion 中调用}}预防设置 JVM 参数-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/logs/heapdump.hprofOOM 时自动 dump。2.2 CPU 飙升 100%排查流程# 1. 找到进程内高 CPU 线程top-HpPID# 2. 记录高 CPU 线程的 TID十进制转十六进制printf%x\nTID# 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|grepFound 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-1Arthas 版thread -b直接输出死锁链。根因与预防统一加锁顺序所有地方先锁 A 再锁 B减小锁粒度不要锁整个方法只锁关键代码块用ReentrantLock.tryLock(timeout, unit)替代synchronized2.4 慢 SQL# 1. 找到正在执行的慢查询SHOW FULL PROCESSLIST;# 2. 启用慢查询日志SET GLOBAL slow_query_logON;SET GLOBAL long_query_time1;-- 超过1秒记录# 3. 分析执行计划EXPLAIN SELECT * FROM orders WHERE user_id123AND statusPAID;EXPLAIN 关键字段字段含义理想值type访问类型const ref range index ALLkey使用的索引非空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 20JOIN 字段类型不一致隐式转换导致全表扫描未利用覆盖索引 →EXPLAIN看 Extra 是否是Using index2.5 接口响应慢先用 SkyWalking / Pinpoint 等 APM 工具定位到具体接口再用 Arthas tracearthastrace com.edulearn.controller.OrderController createOrder-n5输出完整的调用链和每一层的耗时一眼看出瓶颈在 Service、DAO 还是第三方调用。2.6 连接池耗尽# 连接池配置HikariCP 默认spring.datasource.hikari.maximum-pool-size20spring.datasource.hikari.connection-timeout30000如果日志出现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] 返回 ApiResponseOrderVO四、30 道 Spring 综合面试题库覆盖 6 期内容按模块分类。IoC / DI第1期Q1Spring IoC 容器的启动流程Anew AnnotationConfigApplicationContext(Config.class)→ 注册配置类 →refresh()→invokeBeanFactoryPostProcessors解析ComponentScan和Bean→finishBeanFactoryInitialization实例化所有单例 Bean。全部单例 Bean 在容器启动时创建非懒加载通过三级缓存解决循环依赖。Q2Autowired和Resource的区别AAutowired是 Spring 注解默认按类型注入配合Qualifier按名称Resource是 JSR-250 标准默认按名称找不到再按类型。Autowired可标记requiredfalse。Q3Spring 如何解决循环依赖A三级缓存。一级singletonObjects成品、二级earlySingletonObjects半成品、三级singletonFactoriesObjectFactory。A 创建时提前暴露 ObjectFactory 到三级缓存 → B 创建时依赖 A从三级缓存获取 A 的早期引用 → B 创建完成 → A 继续完成创建。Q4Configuration和Component的区别AConfiguration标注的类会被 CGLIB 增强Bean方法内部调用其他Bean方法时返回的是容器中的同一个实例单例保证。Component没有这个增强内部调用Bean方法会创建新对象。Q5FactoryBean和BeanFactory的区别ABeanFactory是 IoC 容器的顶层接口工厂的工厂。FactoryBean是生产特定类型 Bean 的工厂常用于框架集成如SqlSessionFactoryBean生产SqlSession。自动配置第2期Q6SpringBootApplication包含哪三个注解ASpringBootConfigurationConfiguration、EnableAutoConfiguration自动配置核心、ComponentScan组件扫描。Q7自动配置的实现原理AEnableAutoConfiguration→Import(AutoConfigurationImportSelector.class)→ 读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports→ 加载所有自动配置类 → 每个配置类用ConditionalOnClass、ConditionalOnMissingBean等条件注解判断是否生效。Q8如何自定义一个 StarterA① 创建xxx-spring-boot-autoconfigure模块写自动配置类用ConditionalOnClass控制生效条件② 创建xxx-spring-boot-starter模块空项目引入 autoconfigure 模块③ 在 autoconfigure 的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中声明配置类全限定名。Spring MVC第3期Q9DispatcherServlet 的处理流程A请求 →HandlerMapping找到 HandlerController 方法→HandlerAdapter执行 →HandlerMethodArgumentResolver解析参数RequestBody/RequestParam等→ 执行 Controller 方法 →HttpMessageConverter序列化返回值 → 返回响应。中途经过HandlerInterceptor的preHandle/postHandle/afterCompletion。Q10拦截器Interceptor和过滤器Filter的区别AFilter 是 Servlet 规范在请求进入 Servlet 前执行不感知 Spring 上下文Interceptor 是 Spring 机制在 HandlerMapping 之后、Controller 执行前后执行能拿到 HandlerMethod 和 Spring Bean。Filter 在最外层Interceptor 在 DispatcherServlet 内部。Q11RequestBody和ResponseBody的工作原理A通过HttpMessageConverter做序列化/反序列化。Spring Boot 默认使用 JacksonMappingJackson2HttpMessageConverter处理 JSON。RequestBody在参数解析阶段调用read()反序列化ResponseBody在返回值处理阶段调用write()序列化。AOP第4期Q12JDK 动态代理和 CGLIB 的区别Spring 如何选择AJDK 基于接口 Proxy.newProxyInstanceInvocationHandler目标类必须实现接口CGLIB 基于继承 EnhancerMethodInterceptor通过生成子类覆写方法实现。Spring Boot 2.x 默认用 CGLIB。如果目标类实现了接口也可以显式设置spring.aop.proxy-target-classfalse来用 JDK 代理。Q13同一个切面中多个通知的执行顺序AAroundstart →Before→ 目标方法 →AfterReturning/AfterThrowing→After→Aroundend。After无论异常都会执行类似 finallyAfterReturning只在正常返回时执行。Q14多切面作用在同一方法时执行顺序如何控制A通过Order(n)控制数值越小优先级越高。Around是嵌套结构——外层切面的 Around 包裹内层切面的 Around最后才到目标方法。事务第5期Q15Transactional失效的常见场景至少说 3 个A① 自调用this.method()不经过代理② 方法非 public③ 异常被 try-catch 吞掉④ Checked Exception 默认不回滚需rollbackFor Exception.class⑤ 多线程/Async场景ThreadLocal 线程隔离。Q167 种传播行为至少说 3 个常用的AREQUIRED默认同生共死、REQUIRES_NEW独立事务挂起外部、NESTED保存点部分回滚。前两者高频NESTED 依赖 JDBC savepoint。Q17REQUIRED和REQUIRES_NEW的本质区别AREQUIRED加入外部事务共用同一个 Connection同一批操作同生共死。REQUIRES_NEW挂起外部连接从 DataSource 获取新连接开启独立事务——子事务的提交/回滚完全不影响外部。Q18事务怎么做到线程隔离的为什么多线程会失效ATransactionSynchronizationManager用ThreadLocalMapObject, Object存储当前线程的 DataSource→Connection 映射。新线程拿不到父线程的 ThreadLocal 数据自然不在同一事务中。工程实践第6期Q19你们项目的分层架构是怎样的AController → Service → DAO/Mapper → DB。Controller 负责参数校验和统一返回Service 负责业务逻辑和事务管理DAO 负责数据访问。横切关注点日志、异常、权限通过 AOP 和全局异常处理统一管理。Q20线上 CPU 飙升你的排查步骤Atop -Hp PID找到高 CPU 线程 →printf %x TID转十六进制 →jstack PID | grep 十六进制定位代码 → 或者 Arthasthread -n 3一键看最忙线程。常见原因死循环、正则回溯、频繁 Full GC。Q21线上内存泄漏怎么排查Ajmap -histo:live PID | head -20看存活对象分布如果不明显jmap -dump导出堆快照MAT 的 Dominator Tree 分析最大占用。注意 ThreadLocal 没 remove 导致的内存泄漏——tomcat 线程池复用导致线程不消亡。Q22慢 SQL 排查流程ASHOW FULL PROCESSLIST看正在执行的 SQL → 启用慢查询日志long_query_time1→EXPLAIN分析执行计划关注type避免 ALL、key必须有索引、rows越小越好、Extra避免 filesort/temporary→ 加索引、优化 JOIN、避免SELECT *。Q23全局异常处理怎么做ARestControllerAdviceExceptionHandler。自定义BusinessException(code, message)各层统一 throw由全局处理器转为ApiResponse.error()返回。Controller 层零 try-catch。Q24Spring Security JWT 的认证流程A登录接口 → 验证用户名密码 → 生成 JWT token含 userId、角色、过期时间→ 返回 token。后续请求在 Header 带Authorization: Bearer token→ JWT 过滤器解析 token → 验证签名和过期时间 → 将用户信息放入 SecurityContext。Q25Redis 在你们项目中的使用场景A① 课程详情缓存CacheableTTL 30分钟② 分布式锁SETNX下单防超卖③ 用户 session 存储JWT 黑名单④ 排行榜ZSet按学习时长排序。综合追问链Q26从浏览器输入 URL 到 Spring Boot 返回 JSON中间经过哪些层ANginx 反向代理 → Tomcat 线程池接收 → Filter Chain → DispatcherServlet → HandlerMapping → HandlerInterceptor.preHandle → Controller 方法参数解析 校验→ Service事务、缓存、AOP→ DAO → DB → 返回值序列化Jackson→ HandlerInterceptor.postHandle/afterCompletion → Response。Q27Spring 中 Bean 的生命周期A实例化 → 属性填充依赖注入→BeanNameAware/BeanFactoryAware→BeanPostProcessor.postProcessBeforeInitialization→PostConstruct/InitializingBean.afterPropertiesSet→BeanPostProcessor.postProcessAfterInitializationAOP 代理在此生成→ 就绪 → 容器关闭时PreDestroy/DisposableBean.destroy。Q28Spring Boot 如何实现 “约定优于配置”A通过自动配置机制。Spring Boot 预设了合理的默认值如 HikariCP 默认最大连接数 10Tomcat 默认端口 8080开发者只需在 application.yml 中覆盖需要的配置项。这种默认即最佳实践的理念大大减少了 XML 配置和样板代码。Q29你对 Spring 6 / Spring Boot 3.x 的升级了解吗A最大变化是基线升级到 JDK 17 和 Jakarta EE 9javax.*→jakarta.*。AOT 编译和 GraalVM Native Image 支持是性能亮点。此外spring.factories改为AutoConfiguration.imports文件格式。如果你的项目还在 JDK 8/11升级时需要处理包名变更和弃用 API。Q30如果让你从零搭建一个 Spring Boot 项目你的技术选型清单ASpring Boot 3.x JDK 17 MySQL 8.x MyBatis-Plus Redis RabbitMQ JWT Actuator Prometheus Grafana。分层Controller → Service → DAO。横切全局异常处理 AOP 日志 统一返回体。测试JUnit 5 Mockito。CI/CDGitLab CI Docker K8s。五、6 期系列总览期数主题核心知识点第1期IoC / DI容器启动流程、Bean生命周期、三级缓存、循环依赖第2期自动配置Starter机制、条件注解、配置绑定、自定义Starter第3期Spring MVCDispatcherServlet、拦截器vs过滤器、RESTful、参数解析第4期AOPJDK vs CGLIB、切面通知顺序、Order源码走读第5期事务7种传播行为、5大失效场景、TransactionInterceptor源码第6期工程综合分层架构、异常处理、排查实战、30道面试题、项目复盘学习路线建议按顺序阅读第1-3期打基础第4-5期攻源码第6期做串联和面试冲刺。