格式:知识点原理 → 面试表达模板 → 追问应对
一、自动配置原理(高频必考)
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.factories - 3.x:迁移到
meta-inf/spring/org.springframework.boot.autoconfigure.autoconfiguration.imports(性能更好,启动更快)
面试表达模板:
@springbootapplication包含@enableautoconfiguration,它通过autoconfigurationimportselector读取 classpath 下所有 jar 包的自动配置类列表,然后用@conditional系列注解按条件筛选出实际需要创建的 bean。核心设计是"约定大于配置"——只要引入了 starter,相关 bean 就自动就绪,但用户自定义的 bean 始终优先(@conditionalonmissingbean)。
追问 1:如何调试哪些自动配置类生效了?
# 启动时加参数,打印自动配置报告
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),分工明确:
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 ← 注册入口完整实现示例:
// 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 的生命周期有哪些阶段?
知识点讲解:
完整顺序(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推荐用于资源释放
示例:验证顺序:
@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 如何解决循环依赖?构造器注入为什么不能解决?
知识点讲解:
三级缓存的职责:
一级缓存 singletonobjects: 存放完整 bean(实例化 + 属性注入 + 初始化全部完成) ↓ 业务代码获取 bean 从这里取 二级缓存 earlysingletonobjects: 存放早期引用(已实例化,属性注入未完成) ↓ 解决 aop 场景:确保循环引用中拿到的是代理对象而非原始对象 三级缓存 singletonfactories: 存放 bean 工厂(objectfactory,调用时生成早期引用) ↓ 打破循环的关键:a 实例化后立即放入三级缓存
解决过程(a 依赖 b,b 依赖 a):
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 → 完成属性注入 → 初始化完成 → 放入一级缓存
为什么构造器注入无法解决:
字段注入:先实例化(调构造函数),再注入属性 → a 实例化完成后,可以先放入三级缓存,再去解决依赖 构造器注入:实例化和注入同步进行(参数必须在构造时传入) → a 构造时需要 b,b 构造时需要 a,永远无法开始实例化 → spring 直接抛 beancurrentlyincreationexception
面试表达模板:
spring 通过三级缓存解决字段注入的循环依赖。核心思路是"提前暴露":bean 实例化后立即将工厂函数放入三级缓存,其他 bean 需要它时可以提前拿到早期引用(可能是 aop 代理),从而打破循环。构造器注入无法解决,因为实例化和依赖注入同步进行,无法"提前暴露"。spring boot 2.6+ 默认禁止循环依赖,生产中遇到应优先重构代码解耦。
四、事务(高频必考)
q5. @transactional 失效的场景有哪些?
知识点讲解:
事务基于 aop 代理实现,凡是绕过代理的场景都会导致失效。
场景1:同类内部方法调用(最常见):
@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 方法:
// ❌ spring aop 只代理 public 方法
@transactional
protected void internalsave() { ... }
场景3:异常被吃掉:
@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:检查型异常默认不回滚:
// ❌ 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 默认用哪个?
知识点讲解:
jdk 动态代理: 原理:通过反射生成实现了相同接口的代理类 限制:目标类必须实现接口 性能:调用时有反射开销 cglib 代理: 原理:继承目标类并重写所有非 final 方法 限制:目标类/方法不能是 final 性能:jdk 7+ 后与 jdk 代理性能接近 spring boot 2.x 起默认 cglib: 原因:无需实现接口,更通用 配置:@enableaspectjautoproxy(proxytargetclass = true)(默认已开启)
验证代理类型的示例:
@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. 缓存穿透、击穿、雪崩的区别和解决方案?
知识点讲解:
三个问题的根本区别: 缓存穿透:查询的 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 作为二级缓存 |
击穿解决方案示例(互斥锁):
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频繁)、大量线程上下文切换。
标准排查步骤:
# 步骤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:常见问题识别:
看到 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 的核心,也是面试必问题。
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 处理异常
根据状态码快速定位问题:
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 认证的完整流程?
知识点讲解:
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 工具类:
@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)。
总结
到此这篇关于spring boot专家级面试题库及详细讲解的文章就介绍到这了,更多相关springboot面试题库内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论