面试必看:Java 框架面试高频题总结:Spring/SpringMVC/SpringBoot/MyBatis 回答技巧
Java 框架面试高频题总结:Spring/SpringMVC/SpringBoot/MyBatis 回答技巧
在 Java 后端面试中,Spring 生态(Spring、SpringMVC、SpringBoot)与 MyBatis 是必考内容。面试官不仅关注 “会不会用”,更看重 “懂不懂原理” 和 “有没有实战经验”—— 比如单例 Bean 的线程安全、Spring 事务失效的场景、MyBatis 延迟加载的原理等。本文基于黑马程序员框架篇课程核心考点,梳理 18 道高频面试题,每道题提供 “结构化回答模板” 和 “加分技巧”,帮你在面试中快速抓住重点,展现专业度。
一、Spring 框架面试题(核心必问)
1. Spring 的单例 Bean 是线程安全的吗?为什么?
回答模板:
Spring 的单例 Bean不是绝对线程安全的,核心取决于 Bean 是否包含 “可变状态”:
- 无状态 Bean(线程安全):若 Bean 中没有可修改的成员变量(如 Service、DAO 层),仅包含方法逻辑,则线程安全。例:UserService 仅注入 UserDAO,无成员变量,多线程调用
getById时,不会修改 Bean 的状态,所以安全。 - 有状态 Bean(线程不安全):若 Bean 包含可修改的成员变量(如 Controller 中的计数器),多线程并发修改会导致数据混乱。例:UserController 中有
private int count,多用户访问/getById时,count++会被并发修改,出现重复或错乱的数值。 - 解决方案:
- 改多例:用
@Scope("prototype"),每个请求创建新 Bean,避免状态共享; - 线程安全变量:用
AtomicInteger替代int,通过原子操作保证安全; - 局部变量:将可变状态定义在方法内(局部变量线程私有,不共享)。
- 改多例:用
加分技巧:
- 举项目实例:“之前项目中 Controller 的计数器用
int导致线程安全问题,改成AtomicInteger后解决,压测 QPS 从 500 提升到 1500”; - 提 Spring 设计理念:“Spring 默认单例是为了提高性能(减少 Bean 创建开销),但把线程安全交给开发者,因为框架无法预判 Bean 是否有状态”。
2. 什么是 AOP?你们项目中怎么用 AOP 的?
回答模板:
AOP(面向切面编程)是 Spring 的核心特性,把与业务无关但通用的逻辑(如日志、缓存、事务)抽取为 “切面”,动态织入业务方法,减少重复代码,降低耦合。
-
AOP 核心概念:
- 切面(Aspect):抽取的公共模块(如操作日志切面);
- 切点(Pointcut):匹配目标方法(如所有 Controller 的接口);
- 环绕通知(@Around):方法执行前后的逻辑(最常用,可控制方法执行)。
-
项目实战:用 AOP 记录操作日志管理系统需要记录用户操作(用户名、请求方式、IP、时间),用 AOP 避免每个接口写重复代码:
@Aspect @Component public class OperLogAspect { @Autowired private OperLogDAO operLogDAO; // 切点:匹配所有Controller的@RequestMapping方法 @Pointcut("execution(* com.itheima.controller.*Controller.*(..)) && @annotation(RequestMapping)") public void logPointcut() {} @Around("logPointcut()") public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable { // 1. 获取请求信息(用户名、IP、请求方式) HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); OperLog log = new OperLog(); log.setUsername(SecurityUtils.getCurrentUsername()); // 当前登录用户 log.setIp(request.getRemoteAddr()); log.setRequestMethod(request.getMethod()); log.setOperTime(LocalDateTime.now()); // 2. 执行目标方法(如Controller的getById) Object result = joinPoint.proceed(); // 3. 保存日志到数据库 operLogDAO.insert(log); return result; } } -
其他 AOP 场景:
- 缓存:方法执行前查 Redis,命中直接返回,未命中执行方法并更新缓存;
- 事务:Spring 声明式事务本质是 AOP 环绕通知(开启 / 提交 / 回滚事务)。
加分技巧:
- 说清 AOP 的价值:“之前每个接口要写 5 行日志代码,用 AOP 后 1 个切面覆盖所有接口,代码量减少 80%”;
- 提 AOP 底层:“AOP 基于动态代理,JDK 代理(实现接口)或 CGLIB 代理(无接口),项目中用 CGLIB(因为 Controller 没实现接口)”。
3. Spring 事务失效的场景有哪些?怎么解决?
回答模板:
Spring 事务失效的核心原因是 “事务通知未捕获异常” 或 “代理未生效”,项目中遇到过 3 种典型场景:
-
场景 1:异常被手动捕获,未抛出
-
问题:Service 方法用
try-catch捕获异常且未重新抛出,Spring 无法感知异常,不会回滚。
@Transactional public void transfer(Integer fromId, Integer toId, Double money) { try { fromAccount.setMoney(fromAccount.getMoney() - money); accountDAO.updateById(fromAccount); int a = 1 / 0; // 模拟异常 toAccount.setMoney(toAccount.getMoney() + money); } catch (Exception e) { e.printStackTrace(); // 仅打印,未抛出 } } -
解决:
catch中手动抛出运行时异常:throw new RuntimeException(e);
-
-
场景 2:抛出检查异常(非运行时异常)
-
问题:Spring 默认只回滚
RuntimeException,若抛出FileNotFoundException等检查异常,事务不回滚。 -
解决:用rollbackFor = Exception.class
指定回滚所有异常:
@Transactional(rollbackFor = Exception.class) // 关键配置 public void transfer() throws FileNotFoundException { new FileInputStream("test.txt"); // 检查异常 }
-
-
场景 3:非 public 方法
-
问题:Spring 事务代理基于 AOP,仅对
public方法生效,非 public 方法无法被代理,事务失效。 -
解决:将方法改为public:
@Transactional(rollbackFor = Exception.class) public void transfer() { // 改为public // ... }
-
加分技巧:
- 提其他场景(进阶):“还遇到过‘事务方法内部调用’失效,比如 Service 的 A 方法调用本类的 B 方法(B 有 @Transactional),因为内部调用不经过代理,解决方法是注入自己(@Autowired private SelfService self),用 self 调用 B 方法”;
- 说排查方法:“用 DEBUG 看 Bean 是否是代理对象(类名带 $Proxy),若不是则代理未生效,大概率是方法非 public”。
4. Spring 的 Bean 生命周期是什么?
回答模板:
Spring Bean 的生命周期是 “从创建到销毁的完整流程”,共 8 步,核心是 “实例化→注入→初始化→销毁”:
- 读取 Bean 定义:Spring 通过
BeanDefinition封装 Bean 配置(如类名、作用域、属性),来源是@Component或 XML; - 实例化 Bean:调用构造函数,在堆中创建 Bean 对象(此时仅分配内存,未初始化);
- 依赖注入:为 Bean 的成员变量赋值(如
@Autowired注入 UserService); - 处理 Aware 接口:若 Bean 实现
BeanNameAware、ApplicationContextAware,Spring 注入对应对象(如 Bean 名称、容器); - BeanPostProcessor 前置处理:调用所有
BeanPostProcessor的postProcessBeforeInitialization(如 AOP 代理准备); - 初始化方法:执行初始化逻辑,优先级:
- 实现
InitializingBean的afterPropertiesSet; - 自定义 init 方法(如
@Bean(initMethod = "init"));
- 实现
- BeanPostProcessor 后置处理:调用
postProcessAfterInitialization(如生成 AOP 代理,Bean 最终可用); - 销毁 Bean:容器关闭时执行销毁逻辑,优先级:
- 实现
DisposableBean的destroy; - 自定义 destroy 方法(如
@Bean(destroyMethod = "destroy"))。
- 实现
加分技巧:
- 举代码例子:“项目中自定义 Bean 的初始化方法,在
afterPropertiesSet中加载配置文件,确保依赖注入完成后再初始化”; - 说关键区别:“实例化(构造函数)和初始化(afterPropertiesSet)是分开的,依赖注入在实例化之后,避免构造函数中使用未注入的 Bean”。
5. Spring 如何解决循环依赖?构造函数循环依赖怎么处理?
回答模板:
循环依赖是 “Bean 互相依赖形成闭环”(如 A 依赖 B,B 依赖 A),Spring 用三级缓存解决 “setter 注入” 的循环依赖,构造函数循环依赖需特殊处理。
-
三级缓存的结构:
缓存级别 变量名 作用 一级缓存 singletonObjects 存储 “完整 Bean”(初始化完成,可用) 二级缓存 earlySingletonObjects 存储 “早期 Bean”(实例化完成,未注入 / 初始化的半成品) 三级缓存 singletonFactories 存储 “Bean 工厂”( ObjectFactory),用于生成早期 Bean 的代理对象 -
解决 setter 循环依赖的流程(A 依赖 B,B 依赖 A):
- 实例化 A → 存入三级缓存(A 的工厂)→ 注入 B(B 未创建);
- 实例化 B → 存入三级缓存(B 的工厂)→ 注入 A(从三级缓存取 A 的工厂,生成 A 的早期 Bean,存入二级缓存);
- B 完成注入和初始化 → 存入一级缓存 → A 注入 B → A 完成初始化 → 存入一级缓存。
-
构造函数循环依赖(无法用三级缓存):
-
问题:若 A 和 B 通过构造函数注入(
public A(B b),public B(A a)),实例化 A 时需先创建 B,实例化 B 时需先创建 A,循环等待报错。 -
解决:用@Lazy延迟加载,注入时生成代理对象,避免立即创建依赖 Bean:
@Component public class A { private B b; // @Lazy:注入B的代理对象,使用时才创建B public A(@Lazy B b) { this.b = b; } }
-
加分技巧:
- 说清三级缓存的必要性:“二级缓存存早期 Bean,解决‘代理对象重复创建’;三级缓存存工厂,延迟生成代理(避免不需要代理的 Bean 生成代理)”;
- 提限制:“Spring 仅支持单例 Bean 的 setter 循环依赖,多例 Bean(prototype)无法解决,因为多例每次创建新对象,缓存无效”。
二、SpringMVC 面试题(高频)
6. SpringMVC 的执行流程是什么?前后端分离和传统 JSP 有什么区别?
回答模板:
SpringMVC 的核心是 “前端控制器 DispatcherServlet”,流程随场景不同略有差异,但核心组件一致(HandlerMapping、HandlerAdapter)。
- 前后端分离场景(主流,返回 JSON):步骤:
- 用户发送 AJAX 请求(如
GET /api/user/1),DispatcherServlet 接收; - DispatcherServlet 调用 HandlerMapping,根据路径匹配
UserController.getById,返回处理器执行链; - DispatcherServlet 调用 HandlerAdapter,适配并调用
getById方法; getById方法有@ResponseBody,SpringMVC 通过 HttpMessageConverter 将 User 对象转为 JSON;- DispatcherServlet 直接返回 JSON 给前端(无视图渲染)。
- 用户发送 AJAX 请求(如
- 传统 JSP 场景(已过时,返回 HTML):比前后端分离多 2 步:
- 步骤 6:Controller 返回
ModelAndView(如new ModelAndView("user", "user", user)); - 步骤 7:DispatcherServlet 调用 ViewResolver,将逻辑视图名 “user” 解析为物理视图
/WEB-INF/jsp/user.jsp,渲染后返回 HTML。
- 步骤 6:Controller 返回
- 核心组件作用:
- DispatcherServlet:入口,分发请求;
- HandlerMapping:找 Controller 方法;
- HandlerAdapter:调用 Controller 方法;
- HttpMessageConverter:前后端分离场景下转 JSON。
加分技巧:
- 举项目代码:“项目中用
@RestController(@Controller+@ResponseBody),所有接口返回 JSON,前端 Vue 通过 Axios 调用,流程简化,响应速度比 JSP 快 30%”; - 说参数绑定:“HandlerAdapter 会自动绑定请求参数,如
@PathVariable("id")取路径参数,@RequestBody取 JSON 参数,无需手动解析”。
三、SpringBoot 面试题(必问)
7. SpringBoot 自动配置原理是什么?
回答模板:
SpringBoot 自动配置的核心是 “约定优于配置”,通过@SpringBootApplication和 “条件注解” 实现,流程分 3 步:
-
@SpringBootApplication 的本质:它是 3 个注解的组合,核心是
@EnableAutoConfiguration:@SpringBootApplication = @SpringBootConfiguration(= @Configuration) + @ComponentScan(组件扫描) + @EnableAutoConfiguration(自动配置核心) -
@EnableAutoConfiguration 的作用:
- 通过
@Import(AutoConfigurationImportSelector.class),读取类路径下META-INF/spring.factories文件,该文件定义了所有自动配置类(如RedisAutoConfiguration、DataSourceAutoConfiguration); - 例:引入
spring-boot-starter-data-redis依赖后,spring.factories中的RedisAutoConfiguration会被导入。
- 通过
-
条件注解筛选配置类:自动配置类需满足 “条件注解” 才生效,比如
RedisAutoConfiguration:@Configuration @ConditionalOnClass({RedisOperations.class}) // 类路径有RedisOperations才生效(引入了redis依赖) @ConditionalOnMissingBean(name = "redisTemplate") // 容器中没有redisTemplate才自动创建 public class RedisAutoConfiguration { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); return template; } } -
总结流程:引入依赖 → 加载
spring.factories→ 条件注解筛选 → 自动创建 Bean(如 redisTemplate)。
加分技巧:
- 举项目配置:“项目中配置
spring.redis.host=127.0.0.1,SpringBoot 通过@EnableConfigurationProperties(RedisProperties.class)将配置绑定到 RedisProperties,自动注入到 RedisAutoConfiguration”; - 说自定义自动配置:“曾自定义一个 Excel 自动配置类,在
spring.factories中注册,引入依赖后自动创建 Excel 工具 Bean,其他项目可直接用”。
8. SpringBoot 和 Spring 的区别是什么?
回答模板:
SpringBoot 是 “Spring 的增强工具”,核心区别在 “配置复杂度” 和 “开发效率”:
| 维度 | Spring | SpringBoot |
|---|---|---|
| 配置方式 | 需 XML 或 JavaConfig(如 @Configuration) | 自动配置,仅需 application.yml |
| 依赖管理 | 需手动导入所有依赖(如 spring-context、spring-webmvc) | starter 依赖(如 spring-boot-starter-web 包含所有 Web 依赖) |
| 内嵌容器 | 需部署到 Tomcat | 内嵌 Tomcat,直接运行 JAR 包 |
| 开发效率 | 配置繁琐,启动慢 | 开箱即用,启动快(秒级) |
例:开发 Web 项目,Spring 需写 5 个配置类(扫描、视图解析、事务等),SpringBoot 只需引入spring-boot-starter-web,1 行配置都不用写,直接开发 Controller。
加分技巧:
- 说核心价值:“之前用 Spring 开发项目,配置 + 依赖管理要 1 天,用 SpringBoot 后 1 小时搞定,开发效率提升 80%”;
- 提底层关系:“SpringBoot 没有替代 Spring,而是封装了 Spring,内部还是用 Spring 的 IOC、AOP,只是简化了配置”。
四、MyBatis 面试题(高频)
9. MyBatis 的执行流程是什么?
回答模板:
MyBatis 的执行流程围绕 “SqlSession” 展开,从读取配置到执行 SQL,共 7 步:
- 读取配置文件:加载
mybatis-config.xml(全局配置:数据源、映射文件路径)和UserMapper.xml(SQL 映射); - 创建 SqlSessionFactory:通过
SqlSessionFactoryBuilder.build(is)生成,全局唯一; - 创建 SqlSession:
sqlSessionFactory.openSession(),代表一次数据库连接(线程不安全,用完关闭); - 获取 Mapper 代理对象:
sqlSession.getMapper(UserMapper.class),MyBatis 动态生成代理对象(无需手动实现 Mapper 接口); - 执行 SQL:调用userMapper.selectById(1),代理对象会:
- 找到
UserMapper.xml中id="selectById"的 SQL; - 解析
#{id}为参数占位符,将 1 映射为 SQL 参数; - 用 Executor(执行器)执行 SQL,获取结果集;
- 找到
- 结果映射:将数据库结果集映射为 User 对象(按
resultType="com.itheima.pojo.User"); - 关闭 SqlSession:
sqlSession.close(),释放连接。
加分技巧:
- 说关键组件:“Executor 是 MyBatis 的核心执行器,有 SimpleExecutor(默认,每次执行新 SQL)和 ReuseExecutor(复用 PreparedStatement),项目中用 ReuseExecutor 提升性能”;
- 提配置优化:“在 mybatis-config.xml 中配置
mapUnderscoreToCamelCase=true,自动将数据库字段user_name映射为 Java 属性userName,不用写 resultMap”。
10. MyBatis 的延迟加载是什么?原理是什么?
回答模板:
MyBatis 的延迟加载(懒加载)是 “查询主表时不加载关联表,用到关联数据时再加载”,减少不必要的 SQL,适用于一对多关联(如用户 - 订单)。
-
延迟加载 vs 立即加载:
- 立即加载:查用户时同时查订单(
select u.*, o.* from user u left join order o on u.id = o.user_id); - 延迟加载:查用户时仅查用户表,调用
user.getOrderList()时才执行select * from order where user_id = ?。
- 立即加载:查用户时同时查订单(
-
开启延迟加载:在
mybatis-config.xml中配置:<settings> <setting name="lazyLoadingEnabled" value="true"/> <!-- 开启延迟加载 --> <setting name="aggressiveLazyLoading" value="false"/> <!-- 按需加载 --> </settings> -
原理:CGLIB 动态代理:
- MyBatis 为 User 生成 CGLIB 代理对象,当调用
getOrderList()时,进入代理的拦截器; - 拦截器判断
orderList是否为null,若是则执行关联 SQL(selectByUserId); - 将查询结果通过
user.setOrderList(orderList)赋值,再返回orderList。
- MyBatis 为 User 生成 CGLIB 代理对象,当调用
加分技巧:
- 举项目场景:“用户列表页只需显示用户名,不需要订单,用延迟加载后,SQL 执行次数从 1000 次(立即加载)降到 100 次(仅查用户),页面加载速度提升 5 倍”;
- 说注意事项:“延迟加载需确保 SqlSession 未关闭,若关闭会报错,项目中用 SqlSessionTemplate(Spring 整合 MyBatis 提供),自动管理 Session 生命周期”。
11. MyBatis 的一、二级缓存有什么区别?
回答模板:
MyBatis 的两级缓存都是为了减少数据库查询,核心区别在 “作用域” 和 “共享性”:
| 维度 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession(会话) | Mapper(namespace) |
| 默认是否开启 | 是 | 否 |
| 开启方式 | 无需配置 | 1. 全局配置cacheEnabled=true;2. Mapper.xml 加<cache/> |
| 数据共享 | 仅当前 Session | 所有 Session |
| 失效场景 | Session 关闭 / 提交 / 增删改 | Mapper 的增删改 |
| 序列化 | 无需 | 需实现 Serializable 接口 |
-
一级缓存示例:
try (SqlSession sqlSession = factory.openSession()) { UserMapper m1 = sqlSession.getMapper(UserMapper.class); UserMapper m2 = sqlSession.getMapper(UserMapper.class); m1.selectById(1); // 执行SQL,存一级缓存 m2.selectById(1); // 从缓存取,不执行SQL } -
二级缓存示例:
// 第一个Session try (SqlSession sqlSession1 = factory.openSession()) { UserMapper m1 = sqlSession1.getMapper(UserMapper.class); m1.selectById(1); // 执行SQL sqlSession1.close(); // 同步到二级缓存 } // 第二个Session try (SqlSession sqlSession2 = factory.openSession()) { UserMapper m2 = sqlSession2.getMapper(UserMapper.class); m2.selectById(1); // 从二级缓存取,不执行SQL }
加分技巧:
- 说实战选择:“项目中一级缓存默认用,二级缓存仅在‘查询多、增删改少’的场景用(如商品分类列表),避免缓存频繁失效”;
- 提整合 Spring:“Spring 整合 MyBatis 后,SqlSession 由 Spring 管理,关闭 Session 时自动同步二级缓存,无需手动处理”。
五、框架面试总结:3 个核心加分点
- “原理 + 实战” 结合,拒绝空泛面试官不喜欢听 “我知道 AOP 是面向切面编程”,而喜欢听 “我们用 AOP 做操作日志,减少了 80% 重复代码,底层是 CGLIB 代理”—— 用具体场景、数据、工具证明你真的做过。
- 理清逻辑链:问题→原因→方案→效果回答任何问题都按这个逻辑:比如 “Spring 事务失效”→ 原因是 “异常被捕获”→ 方案是 “手动抛出异常”→ 效果是 “事务正常回滚,线上 bug 减少 90%”,逻辑清晰,体现解决问题的能力。
- 重点掌握 “关联知识点”,应对追问面试常追问 “底层原理”:比如问 SpringBoot 自动配置,会追问
@EnableAutoConfiguration的作用;问 MyBatis 延迟加载,会追问 CGLIB 代理的细节 —— 提前把 “表层知识点” 和 “底层原理” 关联起来,避免被问住。
最后,框架面试的核心是 “理解而非死记”:比如 Spring 的循环依赖,不要背三级缓存的定义,要能讲清 “为什么需要三级缓存”“构造函数依赖为什么无法解决”;MyBatis 的缓存,要能说清 “什么时候用二级缓存”“失效场景是什么”。理解了原理,无论面试官怎么问,都能从容应对。
更多推荐
所有评论(0)