Spring Boot 专家级面试题库
格式知识点原理 → 面试表达模板 → 追问应对一、自动配置原理高频必考Q1. Spring Boot 自动配置的原理是什么知识点讲解自动配置的核心链路分四步SpringBootApplication └── EnableAutoConfiguration └── AutoConfigurationImportSelector └── 读取 META-INF/spring/...AutoConfiguration.imports └── 每个自动配置类 Conditional 按条件装配 Bean关键机制ConditionalOnMissingBean保证用户自定义优先即用户自己定义了 Bean自动配置就不再创建实现开箱即用但可覆盖。Spring Boot 2.x vs 3.x 区别2.x配置类列表在META-INF/spring.factories3.x迁移到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports性能更好启动更快面试表达模板SpringBootApplication包含EnableAutoConfiguration它通过AutoConfigurationImportSelector读取 classpath 下所有 jar 包的自动配置类列表然后用Conditional系列注解按条件筛选出实际需要创建的 Bean。核心设计是约定大于配置——只要引入了 Starter相关 Bean 就自动就绪但用户自定义的 Bean 始终优先ConditionalOnMissingBean。追问 1如何调试哪些自动配置类生效了# 启动时加参数打印自动配置报告java-jarapp.jar--debug# 或在 application.yml 中开启logging: level: org.springframework.boot.autoconfigure: DEBUG访问http://localhost:8080/actuator/conditions可以看到所有自动配置类的条件评估结果哪些匹配哪些不匹配及原因。Q2. 如何自定义一个 Starter知识点讲解Starter 依赖聚合模块starter-pom 自动配置模块autoconfigure分工明确my-log-spring-boot-starterstarter只有 pom.xml引入 autoconfigure my-log-spring-boot-autoconfigureautoconfigure包含配置逻辑 ├── MyLogProperties.java ← ConfigurationProperties 属性绑定 ├── MyLogService.java ← 核心功能 ├── MyLogAutoConfiguration.java ← AutoConfiguration 条件装配 └── META-INF/spring/...AutoConfiguration.imports ← 注册入口完整实现示例// 1. 属性类DataConfigurationProperties(prefixmy.log)publicclassMyLogProperties{privatebooleanenabledtrue;privateStringprefix[LOG];}// 2. 核心服务publicclassMyLogService{privatefinalMyLogPropertiesprops;publicMyLogService(MyLogPropertiesprops){this.propsprops;}publicvoidlog(Stringmsg){if(props.isEnabled())System.out.println(props.getPrefix() msg);}}// 3. 自动配置类AutoConfigurationConditionalOnClass(MyLogService.class)// classpath 有此类才生效EnableConfigurationProperties(MyLogProperties.class)publicclassMyLogAutoConfiguration{BeanConditionalOnMissingBean// 用户未自定义才创建publicMyLogServicemyLogService(MyLogPropertiesprops){returnnewMyLogService(props);}}面试表达模板Starter 分两个模块starter 只做依赖聚合autoconfigure 包含自动配置逻辑。核心是AutoConfigurationConditionalOnMissingBean前者让 Spring Boot 发现配置类后者保证用户自定义优先。META-INF 下的注册文件是 Spring SPI 机制的核心——启动时扫描所有 jar 包中的该文件汇总所有候选配置类。二、Bean 生命周期高频必考Q3. Bean 的生命周期有哪些阶段知识点讲解完整顺序14步 [实例化阶段] 1. 调用构造函数实例化 [属性注入阶段] 2. Autowired / Value 属性注入完成 [Aware 回调阶段] 3. BeanNameAware.setBeanName() 4. BeanFactoryAware.setBeanFactory() 5. ApplicationContextAware.setApplicationContext() [初始化阶段] 6. BeanPostProcessor.postProcessBeforeInitialization() 7. PostConstruct 方法 8. InitializingBean.afterPropertiesSet() 9. Bean(initMethod xxx) 10. BeanPostProcessor.postProcessAfterInitialization() ← AOP 代理在此创建 [使用阶段] 11. Bean 正常使用... [销毁阶段] 12. PreDestroy 方法 13. DisposableBean.destroy() 14. Bean(destroyMethod xxx)重要记忆点AOP 代理在第10步postProcessAfterInitialization创建所以在PostConstruct中拿到的this是原始对象不是代理PostConstruct推荐用于初始化依赖注入完成后执行PreDestroy推荐用于资源释放示例验证顺序ComponentSlf4jpublicclassLifecycleBeanimplementsBeanNameAware,InitializingBean,DisposableBean{AutowiredprivateSomeServicesomeService;// 步骤2属性注入OverridepublicvoidsetBeanName(Stringname){log.info(步骤3 BeanNameAware: {},name);}PostConstructpublicvoidpostConstruct(){log.info(步骤7 PostConstruct依赖已注入可安全使用 someService);// 常见用途初始化本地缓存、建立连接池、启动后台线程}OverridepublicvoidafterPropertiesSet(){log.info(步骤8 InitializingBean.afterPropertiesSet);}PreDestroypublicvoidpreDestroy(){log.info(步骤12 PreDestroy释放资源);// 常见用途关闭连接、清理临时文件、停止线程}Overridepublicvoiddestroy(){log.info(步骤13 DisposableBean.destroy);}}面试表达模板Bean 生命周期分四个阶段实例化→属性注入→初始化→销毁。开发中最常用的是PostConstruct初始化和PreDestroy销毁两者分别在属性注入完成后和容器关闭前调用。关键细节是 AOP 代理在postProcessAfterInitialization步骤创建这也是为什么同类内部方法调用绕过代理导致事务失效的根本原因。三、循环依赖高频必考Q4. Spring 如何解决循环依赖构造器注入为什么不能解决知识点讲解三级缓存的职责一级缓存 singletonObjects 存放完整 Bean实例化 属性注入 初始化全部完成 ↓ 业务代码获取 Bean 从这里取 二级缓存 earlySingletonObjects 存放早期引用已实例化属性注入未完成 ↓ 解决 AOP 场景确保循环引用中拿到的是代理对象而非原始对象 三级缓存 singletonFactories 存放 Bean 工厂ObjectFactory调用时生成早期引用 ↓ 打破循环的关键A 实例化后立即放入三级缓存解决过程A 依赖 BB 依赖 A1. 创建 A → 调用构造函数实例化 → 将 A 的 ObjectFactory 放入三级缓存 2. 注入 A 的属性需要 B→ 去创建 B 3. 创建 B → 实例化 B → 将 B 的 ObjectFactory 放入三级缓存 4. 注入 B 的属性需要 A→ 从三级缓存取出 A 的工厂 5. 调用工厂生成 A 的早期引用若 A 有 AOP此处生成代理→ 放入二级缓存 6. B 拿到 A 的引用完成属性注入 → B 初始化完成 → 放入一级缓存 7. A 拿到 B → 完成属性注入 → 初始化完成 → 放入一级缓存为什么构造器注入无法解决字段注入先实例化调构造函数再注入属性 → A 实例化完成后可以先放入三级缓存再去解决依赖 构造器注入实例化和注入同步进行参数必须在构造时传入 → A 构造时需要 BB 构造时需要 A永远无法开始实例化 → Spring 直接抛 BeanCurrentlyInCreationException面试表达模板Spring 通过三级缓存解决字段注入的循环依赖。核心思路是提前暴露Bean 实例化后立即将工厂函数放入三级缓存其他 Bean 需要它时可以提前拿到早期引用可能是 AOP 代理从而打破循环。构造器注入无法解决因为实例化和依赖注入同步进行无法提前暴露。Spring Boot 2.6 默认禁止循环依赖生产中遇到应优先重构代码解耦。四、事务高频必考Q5. Transactional 失效的场景有哪些知识点讲解事务基于 AOP 代理实现凡是绕过代理的场景都会导致失效。场景1同类内部方法调用最常见ServicepublicclassOrderService{// ❌ 调用 this.createLog()this 是原始对象不是代理publicvoidcreateOrder(){this.createLog();// 事务不生效}TransactionalpublicvoidcreateLog(){...}}// ✅ 正确方案注入自身代理ServicepublicclassOrderService{AutowiredprivateOrderServiceself;// Spring 注入的是代理对象publicvoidcreateOrder(){self.createLog();// 通过代理调用事务生效}TransactionalpublicvoidcreateLog(){...}}场景2非 public 方法// ❌ Spring AOP 只代理 public 方法TransactionalprotectedvoidinternalSave(){...}场景3异常被吃掉Transactionalpublicvoidsave(Useruser){try{userRepository.save(user);}catch(Exceptione){log.error(保存失败,e);// ❌ 异常被捕获Spring 认为正常不回滚}}// ✅ 正确捕获后重新抛出或手动标记回滚Transactionalpublicvoidsave(Useruser){try{userRepository.save(user);}catch(Exceptione){log.error(保存失败,e);TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();// 手动标记回滚}}场景4检查型异常默认不回滚// ❌ IOException 是检查型异常默认不回滚TransactionalpublicvoidreadAndSave()throwsIOException{thrownewIOException(IO 异常);// 不会回滚}// ✅ 显式指定回滚所有异常Transactional(rollbackForException.class)publicvoidreadAndSave()throwsIOException{...}面试表达模板Transactional失效主要有四类原因①同类内部调用绕过 AOP 代理最常见用自注入解决②方法非 publicAOP 限制③异常被捕获未重新抛出Spring 认为执行成功不回滚④检查型异常默认不回滚需指定rollbackFor Exception.class。排查时先确认代理是否生效再确认异常是否正常传播。五、AOP高频必考Q6. JDK 动态代理和 CGLIB 的区别Spring Boot 默认用哪个知识点讲解JDK 动态代理 原理通过反射生成实现了相同接口的代理类 限制目标类必须实现接口 性能调用时有反射开销 CGLIB 代理 原理继承目标类并重写所有非 final 方法 限制目标类/方法不能是 final 性能JDK 7 后与 JDK 代理性能接近 Spring Boot 2.x 起默认 CGLIB 原因无需实现接口更通用 配置EnableAspectJAutoProxy(proxyTargetClass true)默认已开启验证代理类型的示例SpringBootTestclassProxyTypeTest{AutowiredprivateUserServiceuserService;TestvoidcheckProxyType(){// Spring Boot 默认 CGLIB 代理System.out.println(userService.getClass().getName());// 输出类似com.example.UserServiceImpl$$SpringCGLIB$$0// 若是 JDK 代理com.sun.proxy.$Proxy28System.out.println(AopUtils.isCglibProxy(userService));// trueSystem.out.println(AopUtils.isJdkDynamicProxy(userService));// false}}面试表达模板JDK 动态代理要求目标类实现接口通过反射生成代理CGLIB 继承目标类重写方法不需要接口但 final 类/方法无法代理。Spring Boot 2.x 起默认 CGLIB所以没有接口的 Service 类也能被 AOP 代理。需要注意 final 方法如 Kotlin 默认 final无法被 CGLIB 代理会导致 AOP 失效。六、Redis 缓存高频必考Q7. 缓存穿透、击穿、雪崩的区别和解决方案知识点讲解三个问题的根本区别 缓存穿透查询的 key 在缓存和数据库都不存在 → 每次都打到 DB可被攻击者利用大量不存在的 key 缓存击穿热点 key 在某一时刻过期 → 瞬间大量并发同时打到 DB仅一个 key 的问题 缓存雪崩大量 key 同时过期或 Redis 宕机 → 大量请求同时打到 DB大规模问题解决方案对比问题方案代码要点穿透缓存空值set key EX 60穿透布隆过滤器启动时加载全量 ID 到 Bloom Filter击穿互斥锁SETNX lock 1 EX 10只有一个线程查 DB击穿逻辑过期不设 TTL由后台线程异步刷新雪崩TTL 随机偏移TTL 基础值 random(0, 300)雪崩Redis 高可用主从哨兵 / Cluster雪崩本地缓存兜底Caffeine 作为二级缓存击穿解决方案示例互斥锁publicUsergetUser(Longid){StringcacheKeyuser:id;// 1. 查缓存Useruser(User)redisTemplate.opsForValue().get(cacheKey);if(user!null)returnuser;// 2. 缓存未命中抢互斥锁只有一个线程能查 DBStringlockKeylock:user:id;BooleanlockedredisTemplate.opsForValue().setIfAbsent(lockKey,1,10,TimeUnit.SECONDS);if(Boolean.TRUE.equals(locked)){try{// 3. 双重检查防止锁等待期间另一线程已刷新缓存user(User)redisTemplate.opsForValue().get(cacheKey);if(user!null)returnuser;// 4. 查 DB 并写缓存useruserRepository.findById(id).orElse(null);if(user!null){redisTemplate.opsForValue().set(cacheKey,user,30,TimeUnit.MINUTES);}else{// 防穿透空值也缓存短 TTLredisTemplate.opsForValue().set(cacheKey,NULL,5,TimeUnit.MINUTES);}returnuser;}finally{redisTemplate.delete(lockKey);// 必须释放锁}}else{// 未抢到锁短暂等待后重试Thread.sleep(50);returngetUser(id);}}面试表达模板三个问题要分清楚穿透是 key 根本不存在用布隆过滤器或缓存空值击穿是热点 key 某一刻过期用互斥锁或逻辑过期雪崩是大量 key 同时失效TTL 加随机值散开配合 Redis 高可用和本地缓存兜底。生产中三种情况往往叠加出现需要综合防御。七、JVM 调优专家题Q8. 线上 CPU 飙高如何排查知识点讲解CPU 飙高的常见原因死循环、大量 GCFull GC频繁、大量线程上下文切换。标准排查步骤# 步骤1找到 CPU 最高的进程top-c# 记录 Java 进程的 PID假设是 12345# 步骤2找到进程内 CPU 最高的线程-H 显示线程top-H-p12345# 找到 CPU 最高的线程 TID假设是 12360# 步骤3将 TID 转为十六进制jstack 输出用十六进制printf%x\n12360# 输出3048# 步骤4查看线程堆栈jstack12345|grep-A30nid0x3048# 找到对应线程的调用栈定位到具体代码行步骤5常见问题识别看到 GC 相关线程GC task thread飙高 → Full GC 频繁 → 查看 jstat -gcutil 12345 1000 → 若老年代O列接近100%说明内存泄漏 → 用 jmap -dump:formatb,fileheap.hprof 12345 导出堆用 MAT 分析 看到业务线程在 RUNNABLE 状态栈顶是业务代码 → 代码死循环正则表达式回溯、while 无出口等 → 根据栈信息定位具体代码 看到大量线程在 BLOCKED 状态 → 锁竞争激烈synchronized 或 DB 连接池耗尽 → 考虑锁优化或增加连接池大小面试表达模板排查 CPU 飙高分三步①top -H -p PID找到 CPU 高的线程 TID②将 TID 转十六进制用jstack找到线程堆栈③根据线程状态判断原因RUNNABLE 且栈顶是业务代码 → 死循环GC 线程飙高 → 频繁 Full GC用jstat -gcutil确认再用堆 dump 分析内存泄漏大量 BLOCKED → 锁竞争或资源耗尽。八、面试自测表题目能否讲清原理能否写出代码能否应对追问自动配置链路Bean 生命周期14步三级缓存解决循环依赖Transactional 失效4种场景JDK代理 vs CGLIB缓存穿透/击穿/雪崩区别CPU飙高排查步骤自定义Starter步骤九、Spring MVC 请求链路高频Q9. 一个 HTTP 请求在 Spring MVC 内部的完整流程知识点讲解理解此流程是排查 404/415/400 的核心也是面试必问题。HTTP Request ↓ DispatcherServlet.doDispatch() ↓ ① HandlerMapping找处理器 RequestMappingHandlerMapping → 返回 HandlerExecutionChain Handler 匹配的 HandlerInterceptor 列表 ↓ ② HandlerAdapter执行处理器 RequestMappingHandlerAdapter ↓ HandlerMethodArgumentResolver解析参数 RequestParam → RequestParamMethodArgumentResolver RequestBody → RequestResponseBodyMethodProcessor └── HttpMessageConverterJSON→Java对象 PathVariable → PathVariableMethodArgumentResolver Valid → 触发 JSR-303 参数校验 ↓ 执行 Controller 方法 ↓ HandlerMethodReturnValueHandler处理返回值 ResponseBody → RequestResponseBodyMethodProcessor └── HttpMessageConverterJava对象→JSON ↓ ③ HandlerInterceptor 链 preHandle() ← Controller 执行前返回 false 则中断 postHandle() ← Controller 执行后视图渲染前 afterCompletion ← 请求完成即使有异常也会执行 ↓ ④ ExceptionHandlerExceptionResolver ExceptionHandler / RestControllerAdvice 处理异常根据状态码快速定位问题404 → HandlerMapping 找不到匹配的处理器 检查路径是否正确、RequestMapping 是否在 RestController 类上 405 → 路径匹配但 HTTP 方法不匹配 检查GetMapping vs PostMapping 是否对应 415 → Content-Type 不被支持没有合适的 MessageConverter 检查是否引入了 jackson-databindRequestBody 方法请求头是否有 Content-Type:application/json 400 → 参数绑定失败Valid 校验失败 → MethodArgumentNotValidException 全局异常处理中捕获并返回友好错误信息面试表达模板Spring MVC 的核心是DispatcherServlet一次请求依次经过①HandlerMapping找到 Controller 方法②HandlerAdapter解析参数MessageConverter 反序列化 JSON、执行方法、序列化返回值③HandlerInterceptor链前置/后置拦截④ExceptionHandlerExceptionResolver统一处理异常。掌握这条链路任何 HTTP 错误码都能快速定位根因。十、Spring Security安全Q10. JWT 认证的完整流程知识点讲解JWT 认证流程无状态不需要服务端存储 Session 登录阶段 客户端 POST /auth/login (username, password) ↓ UsernamePasswordAuthenticationFilter 拦截 ↓ AuthenticationManager → UserDetailsService 加载用户 ↓ PasswordEncoder 校验密码 ↓ 认证成功 → 生成 JWT含 userId、roles、过期时间 ↓ 返回 JWT 给客户端客户端存 localStorage / Cookie 后续请求阶段 客户端每次请求携带 Header: Authorization: Bearer JWT ↓ 自定义 JwtAuthenticationFilterOncePerRequestFilter ↓ 解析 JWT → 校验签名、检查过期时间 ↓ 从 JWT 中提取 userId、roles ↓ 将认证信息设置到 SecurityContextHolder ↓ FilterChain 继续执行后续过滤器和 Controller 可获取当前用户JWT 工具类ComponentpublicclassJwtUtil{// 建议从配置文件读取生产不要硬编码Value(${jwt.secret})privateStringsecret;Value(${jwt.expiration:86400})// 默认24小时privatelongexpirationSeconds;// 生成 JWTpublicStringgenerateToken(LonguserId,ListStringroles){returnJwts.builder().subject(userId.toString()).claim(roles,roles).issuedAt(newDate()).expiration(newDate(System.currentTimeMillis()expirationSeconds*1000)).signWith(getKey()).compact();}// 解析 JWT失败会抛 JwtExceptionpublicClaimsparseToken(Stringtoken){returnJwts.parser().verifyWith(getKey()).build().parseSignedClaims(token).getPayload();}privateSecretKeygetKey(){returnKeys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));}}// JWT 过滤器集成到 Spring Security 过滤器链ComponentRequiredArgsConstructorpublicclassJwtAuthFilterextendsOncePerRequestFilter{privatefinalJwtUtiljwtUtil;OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainchain)throwsIOException,ServletException{StringauthHeaderrequest.getHeader(Authorization);if(authHeader!nullauthHeader.startsWith(Bearer )){StringtokenauthHeader.substring(7);try{ClaimsclaimsjwtUtil.parseToken(token);LonguserIdLong.parseLong(claims.getSubject());// 将用户信息放入 SecurityContext后续可用 AuthenticationPrincipal 获取UsernamePasswordAuthenticationTokenauthnewUsernamePasswordAuthenticationToken(userId,null,((ListString)claims.get(roles)).stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));SecurityContextHolder.getContext().setAuthentication(auth);}catch(JwtExceptione){response.sendError(HttpServletResponse.SC_UNAUTHORIZED,Token 无效);return;}}chain.doFilter(request,response);}}面试表达模板JWT 认证是无状态的服务端不存 Session用私钥签发 Token校验时用公钥或相同密钥验证签名。流程是登录成功 → 生成 JWT → 客户端每次请求携带 → 自定义过滤器解析 JWT → 写入 SecurityContext → Controller 通过AuthenticationPrincipal获取当前用户。优点是水平扩展方便无需共享 Session缺点是 Token 签发后无法主动失效需要维护黑名单 Redis。