格式:知识点原理 → 面试表达模板 → 追问应对
一、自动配置原理(高频必考)
Q1. Spring Boot 自动配置的原理是什么?
知识点讲解:
自动配置的核心链路分四步:
text
@SpringBootApplication
└── @EnableAutoConfiguration
└── AutoConfigurationImportSelector
└── 读取 META-INF/spring/...AutoConfiguration.imports
└── 每个自动配置类 + @Conditional 按条件装配 Bean
关键机制:@ConditionalOnMissingBean 保证"用户自定义优先",即用户自己定义了 Bean,自动配置就不再创建,实现"开箱即用但可覆盖"。
Spring Boot 2.x vs 3.x 区别:
- 2.x:配置类列表在
META-INF/spring.factories - 3.x:迁移到
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(性能更好,启动更快)
面试表达模板:
@SpringBootApplication包含@EnableAutoConfiguration,它通过AutoConfigurationImportSelector读取 classpath 下所有 jar 包的自动配置类列表,然后用@Conditional系列注解按条件筛选出实际需要创建的 Bean。核心设计是"约定大于配置"------只要引入了 Starter,相关 Bean 就自动就绪,但用户自定义的 Bean 始终优先(@ConditionalOnMissingBean)。
追问 1:如何调试哪些自动配置类生效了?
bash
# 启动时加参数,打印自动配置报告
java -jar app.jar --debug
# 或在 application.yml 中开启
logging:
level:
org.springframework.boot.autoconfigure: DEBUG
访问 http://localhost:8080/actuator/conditions 可以看到所有自动配置类的条件评估结果(哪些匹配,哪些不匹配及原因)。
Q2. 如何自定义一个 Starter?
知识点讲解:
Starter = 依赖聚合模块(starter-pom) + 自动配置模块(autoconfigure),分工明确:
text
my-log-spring-boot-starter(starter,只有 pom.xml,引入 autoconfigure)
my-log-spring-boot-autoconfigure(autoconfigure,包含配置逻辑)
├── MyLogProperties.java ← @ConfigurationProperties 属性绑定
├── MyLogService.java ← 核心功能
├── MyLogAutoConfiguration.java ← @AutoConfiguration 条件装配
└── META-INF/spring/...AutoConfiguration.imports ← 注册入口
完整实现示例:
java
// 1. 属性类
@Data
@ConfigurationProperties(prefix = "my.log")
public class MyLogProperties {
private boolean enabled = true;
private String prefix = "[LOG]";
}
// 2. 核心服务
public class MyLogService {
private final MyLogProperties props;
public MyLogService(MyLogProperties props) { this.props = props; }
public void log(String msg) {
if (props.isEnabled()) System.out.println(props.getPrefix() + " " + msg);
}
}
// 3. 自动配置类
@AutoConfiguration
@ConditionalOnClass(MyLogService.class) // classpath 有此类才生效
@EnableConfigurationProperties(MyLogProperties.class)
public class MyLogAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 用户未自定义才创建
public MyLogService myLogService(MyLogProperties props) {
return new MyLogService(props);
}
}
面试表达模板:
Starter 分两个模块:starter 只做依赖聚合,autoconfigure 包含自动配置逻辑。核心是
@AutoConfiguration+@ConditionalOnMissingBean,前者让 Spring Boot 发现配置类,后者保证用户自定义优先。META-INF 下的注册文件是 Spring SPI 机制的核心------启动时扫描所有 jar 包中的该文件,汇总所有候选配置类。
二、Bean 生命周期(高频必考)
Q3. Bean 的生命周期有哪些阶段?
知识点讲解:
text
完整顺序(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推荐用于资源释放
示例:验证顺序:
java
@Component
@Slf4j
public class LifecycleBean implements BeanNameAware, InitializingBean, DisposableBean {
@Autowired
private SomeService someService; // 步骤2:属性注入
@Override
public void setBeanName(String name) {
log.info("步骤3 BeanNameAware: {}", name);
}
@PostConstruct
public void postConstruct() {
log.info("步骤7 @PostConstruct:依赖已注入,可安全使用 someService");
// 常见用途:初始化本地缓存、建立连接池、启动后台线程
}
@Override
public void afterPropertiesSet() {
log.info("步骤8 InitializingBean.afterPropertiesSet");
}
@PreDestroy
public void preDestroy() {
log.info("步骤12 @PreDestroy:释放资源");
// 常见用途:关闭连接、清理临时文件、停止线程
}
@Override
public void destroy() {
log.info("步骤13 DisposableBean.destroy");
}
}
面试表达模板:
Bean 生命周期分四个阶段:实例化→属性注入→初始化→销毁。开发中最常用的是
@PostConstruct(初始化)和@PreDestroy(销毁),两者分别在属性注入完成后和容器关闭前调用。关键细节是 AOP 代理在postProcessAfterInitialization步骤创建,这也是为什么同类内部方法调用绕过代理导致事务失效的根本原因。
三、循环依赖(高频必考)
Q4. Spring 如何解决循环依赖?构造器注入为什么不能解决?
知识点讲解:
三级缓存的职责:
text
一级缓存 singletonObjects:
存放完整 Bean(实例化 + 属性注入 + 初始化全部完成)
↓ 业务代码获取 Bean 从这里取
二级缓存 earlySingletonObjects:
存放早期引用(已实例化,属性注入未完成)
↓ 解决 AOP 场景:确保循环引用中拿到的是代理对象而非原始对象
三级缓存 singletonFactories:
存放 Bean 工厂(ObjectFactory,调用时生成早期引用)
↓ 打破循环的关键:A 实例化后立即放入三级缓存
解决过程(A 依赖 B,B 依赖 A):
text
1. 创建 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 → 完成属性注入 → 初始化完成 → 放入一级缓存
为什么构造器注入无法解决:
text
字段注入:先实例化(调构造函数),再注入属性
→ A 实例化完成后,可以先放入三级缓存,再去解决依赖
构造器注入:实例化和注入同步进行(参数必须在构造时传入)
→ A 构造时需要 B,B 构造时需要 A,永远无法开始实例化
→ Spring 直接抛 BeanCurrentlyInCreationException
面试表达模板:
Spring 通过三级缓存解决字段注入的循环依赖。核心思路是"提前暴露":Bean 实例化后立即将工厂函数放入三级缓存,其他 Bean 需要它时可以提前拿到早期引用(可能是 AOP 代理),从而打破循环。构造器注入无法解决,因为实例化和依赖注入同步进行,无法"提前暴露"。Spring Boot 2.6+ 默认禁止循环依赖,生产中遇到应优先重构代码解耦。
四、事务(高频必考)
Q5. @Transactional 失效的场景有哪些?
知识点讲解:
事务基于 AOP 代理实现,凡是绕过代理的场景都会导致失效。
场景1:同类内部方法调用(最常见):
java
@Service
public class OrderService {
// ❌ 调用 this.createLog(),this 是原始对象不是代理
public void createOrder() {
this.createLog(); // 事务不生效
}
@Transactional
public void createLog() { ... }
}
// ✅ 正确方案:注入自身代理
@Service
public class OrderService {
@Autowired
private OrderService self; // Spring 注入的是代理对象
public void createOrder() {
self.createLog(); // 通过代理调用,事务生效
}
@Transactional
public void createLog() { ... }
}
场景2:非 public 方法:
java
// ❌ Spring AOP 只代理 public 方法
@Transactional
protected void internalSave() { ... }
场景3:异常被吃掉:
java
@Transactional
public void save(User user) {
try {
userRepository.save(user);
} catch (Exception e) {
log.error("保存失败", e); // ❌ 异常被捕获,Spring 认为正常,不回滚
}
}
// ✅ 正确:捕获后重新抛出,或手动标记回滚
@Transactional
public void save(User user) {
try {
userRepository.save(user);
} catch (Exception e) {
log.error("保存失败", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // 手动标记回滚
}
}
场景4:检查型异常默认不回滚:
java
// ❌ IOException 是检查型异常,默认不回滚
@Transactional
public void readAndSave() throws IOException {
throw new IOException("IO 异常"); // 不会回滚!
}
// ✅ 显式指定回滚所有异常
@Transactional(rollbackFor = Exception.class)
public void readAndSave() throws IOException { ... }
面试表达模板:
@Transactional失效主要有四类原因:①同类内部调用绕过 AOP 代理(最常见,用自注入解决);②方法非 public(AOP 限制);③异常被捕获未重新抛出(Spring 认为执行成功不回滚);④检查型异常默认不回滚(需指定rollbackFor = Exception.class)。排查时先确认代理是否生效,再确认异常是否正常传播。
五、AOP(高频必考)
Q6. JDK 动态代理和 CGLIB 的区别?Spring Boot 默认用哪个?
知识点讲解:
text
JDK 动态代理:
原理:通过反射生成实现了相同接口的代理类
限制:目标类必须实现接口
性能:调用时有反射开销
CGLIB 代理:
原理:继承目标类并重写所有非 final 方法
限制:目标类/方法不能是 final
性能:JDK 7+ 后与 JDK 代理性能接近
Spring Boot 2.x 起默认 CGLIB:
原因:无需实现接口,更通用
配置:@EnableAspectJAutoProxy(proxyTargetClass = true)(默认已开启)
验证代理类型的示例:
java
@SpringBootTest
class ProxyTypeTest {
@Autowired
private UserService userService;
@Test
void checkProxyType() {
// Spring Boot 默认 CGLIB 代理
System.out.println(userService.getClass().getName());
// 输出类似:com.example.UserServiceImpl$$SpringCGLIB$$0
// 若是 JDK 代理:com.sun.proxy.$Proxy28
System.out.println(AopUtils.isCglibProxy(userService)); // true
System.out.println(AopUtils.isJdkDynamicProxy(userService)); // false
}
}
面试表达模板:
JDK 动态代理要求目标类实现接口,通过反射生成代理;CGLIB 继承目标类重写方法,不需要接口但 final 类/方法无法代理。Spring Boot 2.x 起默认 CGLIB,所以没有接口的 Service 类也能被 AOP 代理。需要注意 final 方法(如 Kotlin 默认 final)无法被 CGLIB 代理,会导致 AOP 失效。
六、Redis 缓存(高频必考)
Q7. 缓存穿透、击穿、雪崩的区别和解决方案?
知识点讲解:
text
三个问题的根本区别:
缓存穿透:查询的 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 作为二级缓存 |
击穿解决方案示例(互斥锁):
java
public User getUser(Long id) {
String cacheKey = "user:" + id;
// 1. 查缓存
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) return user;
// 2. 缓存未命中,抢互斥锁(只有一个线程能查 DB)
String lockKey = "lock:user:" + id;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 3. 双重检查(防止锁等待期间另一线程已刷新缓存)
user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) return user;
// 4. 查 DB 并写缓存
user = userRepository.findById(id).orElse(null);
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
} else {
// 防穿透:空值也缓存(短 TTL)
redisTemplate.opsForValue().set(cacheKey, "NULL", 5, TimeUnit.MINUTES);
}
return user;
} finally {
redisTemplate.delete(lockKey); // 必须释放锁
}
} else {
// 未抢到锁:短暂等待后重试
Thread.sleep(50);
return getUser(id);
}
}
面试表达模板:
三个问题要分清楚:穿透是 key 根本不存在(用布隆过滤器或缓存空值);击穿是热点 key 某一刻过期(用互斥锁或逻辑过期);雪崩是大量 key 同时失效(TTL 加随机值散开,配合 Redis 高可用和本地缓存兜底)。生产中三种情况往往叠加出现,需要综合防御。
七、JVM 调优(专家题)
Q8. 线上 CPU 飙高如何排查?
知识点讲解:
CPU 飙高的常见原因:死循环、大量 GC(Full GC频繁)、大量线程上下文切换。
标准排查步骤:
bash
# 步骤1:找到 CPU 最高的进程
top -c
# 记录 Java 进程的 PID,假设是 12345
# 步骤2:找到进程内 CPU 最高的线程(-H 显示线程)
top -H -p 12345
# 找到 CPU 最高的线程 TID,假设是 12360
# 步骤3:将 TID 转为十六进制(jstack 输出用十六进制)
printf '%x\n' 12360
# 输出:3048
# 步骤4:查看线程堆栈
jstack 12345 | grep -A 30 "nid=0x3048"
# 找到对应线程的调用栈,定位到具体代码行
步骤5:常见问题识别:
text
看到 GC 相关线程(GC task thread)飙高:
→ Full GC 频繁 → 查看 jstat -gcutil 12345 1000
→ 若老年代(O列)接近100%,说明内存泄漏
→ 用 jmap -dump:format=b,file=heap.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 的核心,也是面试必问题。
text
HTTP Request
↓
DispatcherServlet.doDispatch()
↓
① HandlerMapping(找处理器)
RequestMappingHandlerMapping → 返回 HandlerExecutionChain
(Handler + 匹配的 HandlerInterceptor 列表)
↓
② HandlerAdapter(执行处理器)
RequestMappingHandlerAdapter
↓
HandlerMethodArgumentResolver(解析参数)
@RequestParam → RequestParamMethodArgumentResolver
@RequestBody → RequestResponseBodyMethodProcessor
└── HttpMessageConverter(JSON→Java对象)
@PathVariable → PathVariableMethodArgumentResolver
@Valid → 触发 JSR-303 参数校验
↓
执行 Controller 方法
↓
HandlerMethodReturnValueHandler(处理返回值)
@ResponseBody → RequestResponseBodyMethodProcessor
└── HttpMessageConverter(Java对象→JSON)
↓
③ HandlerInterceptor 链
preHandle() ← Controller 执行前(返回 false 则中断)
postHandle() ← Controller 执行后,视图渲染前
afterCompletion ← 请求完成(即使有异常也会执行)
↓
④ ExceptionHandlerExceptionResolver
@ExceptionHandler / @RestControllerAdvice 处理异常
根据状态码快速定位问题:
text
404 → HandlerMapping 找不到匹配的处理器
检查:路径是否正确、@RequestMapping 是否在 @RestController 类上
405 → 路径匹配但 HTTP 方法不匹配
检查:@GetMapping vs @PostMapping 是否对应
415 → Content-Type 不被支持(没有合适的 MessageConverter)
检查:是否引入了 jackson-databind;@RequestBody 方法请求头是否有 Content-Type:application/json
400 → 参数绑定失败(@Valid 校验失败 → MethodArgumentNotValidException)
全局异常处理中捕获并返回友好错误信息
面试表达模板:
Spring MVC 的核心是
DispatcherServlet,一次请求依次经过:①HandlerMapping找到 Controller 方法;②HandlerAdapter解析参数(MessageConverter 反序列化 JSON)、执行方法、序列化返回值;③HandlerInterceptor链(前置/后置拦截);④ExceptionHandlerExceptionResolver统一处理异常。掌握这条链路,任何 HTTP 错误码都能快速定位根因。
十、Spring Security(安全)
Q10. JWT 认证的完整流程?
知识点讲解:
text
JWT 认证流程(无状态,不需要服务端存储 Session):
登录阶段:
客户端 POST /auth/login (username, password)
↓
UsernamePasswordAuthenticationFilter 拦截
↓
AuthenticationManager → UserDetailsService 加载用户
↓
PasswordEncoder 校验密码
↓
认证成功 → 生成 JWT(含 userId、roles、过期时间)
↓
返回 JWT 给客户端(客户端存 localStorage / Cookie)
后续请求阶段:
客户端每次请求携带 Header: Authorization: Bearer <JWT>
↓
自定义 JwtAuthenticationFilter(OncePerRequestFilter)
↓
解析 JWT → 校验签名、检查过期时间
↓
从 JWT 中提取 userId、roles
↓
将认证信息设置到 SecurityContextHolder
↓
FilterChain 继续执行(后续过滤器和 Controller 可获取当前用户)
JWT 工具类:
java
@Component
public class JwtUtil {
// 建议从配置文件读取,生产不要硬编码
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:86400}") // 默认24小时
private long expirationSeconds;
// 生成 JWT
public String generateToken(Long userId, List<String> roles) {
return Jwts.builder()
.subject(userId.toString())
.claim("roles", roles)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000))
.signWith(getKey())
.compact();
}
// 解析 JWT(失败会抛 JwtException)
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
private SecretKey getKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
}
// JWT 过滤器(集成到 Spring Security 过滤器链)
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Claims claims = jwtUtil.parseToken(token);
Long userId = Long.parseLong(claims.getSubject());
// 将用户信息放入 SecurityContext(后续可用 @AuthenticationPrincipal 获取)
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userId, null,
((List<String>) claims.get("roles")).stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()));
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token 无效");
return;
}
}
chain.doFilter(request, response);
}
}
面试表达模板:
JWT 认证是无状态的:服务端不存 Session,用私钥签发 Token,校验时用公钥(或相同密钥)验证签名。流程是:登录成功 → 生成 JWT → 客户端每次请求携带 → 自定义过滤器解析 JWT → 写入 SecurityContext → Controller 通过
@AuthenticationPrincipal获取当前用户。优点是水平扩展方便(无需共享 Session),缺点是 Token 签发后无法主动失效(需要维护黑名单 Redis)。