Java 框架面试高频题总结:Spring/SpringMVC/SpringBoot/MyBatis 回答技巧

在 Java 后端面试中,Spring 生态(Spring、SpringMVC、SpringBoot)与 MyBatis 是必考内容。面试官不仅关注 “会不会用”,更看重 “懂不懂原理” 和 “有没有实战经验”—— 比如单例 Bean 的线程安全、Spring 事务失效的场景、MyBatis 延迟加载的原理等。本文基于黑马程序员框架篇课程核心考点,梳理 18 道高频面试题,每道题提供 “结构化回答模板” 和 “加分技巧”,帮你在面试中快速抓住重点,展现专业度。

一、Spring 框架面试题(核心必问)

1. Spring 的单例 Bean 是线程安全的吗?为什么?

回答模板:

Spring 的单例 Bean不是绝对线程安全的,核心取决于 Bean 是否包含 “可变状态”:

  1. 无状态 Bean(线程安全):若 Bean 中没有可修改的成员变量(如 Service、DAO 层),仅包含方法逻辑,则线程安全。例:UserService 仅注入 UserDAO,无成员变量,多线程调用getById时,不会修改 Bean 的状态,所以安全。
  2. 有状态 Bean(线程不安全):若 Bean 包含可修改的成员变量(如 Controller 中的计数器),多线程并发修改会导致数据混乱。例:UserController 中有private int count,多用户访问/getById时,count++会被并发修改,出现重复或错乱的数值。
  3. 解决方案
    • 改多例:用@Scope("prototype"),每个请求创建新 Bean,避免状态共享;
    • 线程安全变量:用AtomicInteger替代int,通过原子操作保证安全;
    • 局部变量:将可变状态定义在方法内(局部变量线程私有,不共享)。
加分技巧:
  • 举项目实例:“之前项目中 Controller 的计数器用int导致线程安全问题,改成AtomicInteger后解决,压测 QPS 从 500 提升到 1500”;
  • 提 Spring 设计理念:“Spring 默认单例是为了提高性能(减少 Bean 创建开销),但把线程安全交给开发者,因为框架无法预判 Bean 是否有状态”。

2. 什么是 AOP?你们项目中怎么用 AOP 的?

回答模板:

AOP(面向切面编程)是 Spring 的核心特性,把与业务无关但通用的逻辑(如日志、缓存、事务)抽取为 “切面”,动态织入业务方法,减少重复代码,降低耦合。

  1. AOP 核心概念

    • 切面(Aspect):抽取的公共模块(如操作日志切面);
    • 切点(Pointcut):匹配目标方法(如所有 Controller 的接口);
    • 环绕通知(@Around):方法执行前后的逻辑(最常用,可控制方法执行)。
  2. 项目实战:用 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;
        }
    }
    
  3. 其他 AOP 场景

    • 缓存:方法执行前查 Redis,命中直接返回,未命中执行方法并更新缓存;
    • 事务:Spring 声明式事务本质是 AOP 环绕通知(开启 / 提交 / 回滚事务)。
加分技巧:
  • 说清 AOP 的价值:“之前每个接口要写 5 行日志代码,用 AOP 后 1 个切面覆盖所有接口,代码量减少 80%”;
  • 提 AOP 底层:“AOP 基于动态代理,JDK 代理(实现接口)或 CGLIB 代理(无接口),项目中用 CGLIB(因为 Controller 没实现接口)”。

3. Spring 事务失效的场景有哪些?怎么解决?

回答模板:

Spring 事务失效的核心原因是 “事务通知未捕获异常” 或 “代理未生效”,项目中遇到过 3 种典型场景:

  1. 场景 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. 场景 2:抛出检查异常(非运行时异常)

    • 问题:Spring 默认只回滚RuntimeException,若抛出FileNotFoundException等检查异常,事务不回滚。

    • 解决:用rollbackFor = Exception.class

      指定回滚所有异常:

      @Transactional(rollbackFor = Exception.class) // 关键配置
      public void transfer() throws FileNotFoundException {
          new FileInputStream("test.txt"); // 检查异常
      }
      
  3. 场景 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 步,核心是 “实例化→注入→初始化→销毁”:

  1. 读取 Bean 定义:Spring 通过BeanDefinition封装 Bean 配置(如类名、作用域、属性),来源是@Component或 XML;
  2. 实例化 Bean:调用构造函数,在堆中创建 Bean 对象(此时仅分配内存,未初始化);
  3. 依赖注入:为 Bean 的成员变量赋值(如@Autowired注入 UserService);
  4. 处理 Aware 接口:若 Bean 实现BeanNameAwareApplicationContextAware,Spring 注入对应对象(如 Bean 名称、容器);
  5. BeanPostProcessor 前置处理:调用所有BeanPostProcessorpostProcessBeforeInitialization(如 AOP 代理准备);
  6. 初始化方法:执行初始化逻辑,优先级:
    • 实现InitializingBeanafterPropertiesSet
    • 自定义 init 方法(如@Bean(initMethod = "init"));
  7. BeanPostProcessor 后置处理:调用postProcessAfterInitialization(如生成 AOP 代理,Bean 最终可用);
  8. 销毁 Bean:容器关闭时执行销毁逻辑,优先级:
    • 实现DisposableBeandestroy
    • 自定义 destroy 方法(如@Bean(destroyMethod = "destroy"))。
加分技巧:
  • 举代码例子:“项目中自定义 Bean 的初始化方法,在afterPropertiesSet中加载配置文件,确保依赖注入完成后再初始化”;
  • 说关键区别:“实例化(构造函数)和初始化(afterPropertiesSet)是分开的,依赖注入在实例化之后,避免构造函数中使用未注入的 Bean”。

5. Spring 如何解决循环依赖?构造函数循环依赖怎么处理?

回答模板:

循环依赖是 “Bean 互相依赖形成闭环”(如 A 依赖 B,B 依赖 A),Spring 用三级缓存解决 “setter 注入” 的循环依赖,构造函数循环依赖需特殊处理。

  1. 三级缓存的结构

    缓存级别 变量名 作用
    一级缓存 singletonObjects 存储 “完整 Bean”(初始化完成,可用)
    二级缓存 earlySingletonObjects 存储 “早期 Bean”(实例化完成,未注入 / 初始化的半成品)
    三级缓存 singletonFactories 存储 “Bean 工厂”(ObjectFactory),用于生成早期 Bean 的代理对象
  2. 解决 setter 循环依赖的流程(A 依赖 B,B 依赖 A)

    • 实例化 A → 存入三级缓存(A 的工厂)→ 注入 B(B 未创建);
    • 实例化 B → 存入三级缓存(B 的工厂)→ 注入 A(从三级缓存取 A 的工厂,生成 A 的早期 Bean,存入二级缓存);
    • B 完成注入和初始化 → 存入一级缓存 → A 注入 B → A 完成初始化 → 存入一级缓存。
  3. 构造函数循环依赖(无法用三级缓存)

    • 问题:若 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)。

  1. 前后端分离场景(主流,返回 JSON):步骤:
    1. 用户发送 AJAX 请求(如GET /api/user/1),DispatcherServlet 接收;
    2. DispatcherServlet 调用 HandlerMapping,根据路径匹配UserController.getById,返回处理器执行链;
    3. DispatcherServlet 调用 HandlerAdapter,适配并调用getById方法;
    4. getById方法有@ResponseBody,SpringMVC 通过 HttpMessageConverter 将 User 对象转为 JSON;
    5. DispatcherServlet 直接返回 JSON 给前端(无视图渲染)。
  2. 传统 JSP 场景(已过时,返回 HTML):比前后端分离多 2 步:
    • 步骤 6:Controller 返回ModelAndView(如new ModelAndView("user", "user", user));
    • 步骤 7:DispatcherServlet 调用 ViewResolver,将逻辑视图名 “user” 解析为物理视图/WEB-INF/jsp/user.jsp,渲染后返回 HTML。
  3. 核心组件作用
    • 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 步:

  1. @SpringBootApplication 的本质:它是 3 个注解的组合,核心是@EnableAutoConfiguration

    @SpringBootApplication = @SpringBootConfiguration= @Configuration+ 
                            @ComponentScan(组件扫描) + 
                            @EnableAutoConfiguration(自动配置核心)
    
  2. @EnableAutoConfiguration 的作用

    • 通过@Import(AutoConfigurationImportSelector.class),读取类路径下META-INF/spring.factories文件,该文件定义了所有自动配置类(如RedisAutoConfigurationDataSourceAutoConfiguration);
    • 例:引入spring-boot-starter-data-redis依赖后,spring.factories中的RedisAutoConfiguration会被导入。
  3. 条件注解筛选配置类:自动配置类需满足 “条件注解” 才生效,比如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;
        }
    }
    
  4. 总结流程:引入依赖 → 加载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 步:

  1. 读取配置文件:加载mybatis-config.xml(全局配置:数据源、映射文件路径)和UserMapper.xml(SQL 映射);
  2. 创建 SqlSessionFactory:通过SqlSessionFactoryBuilder.build(is)生成,全局唯一;
  3. 创建 SqlSessionsqlSessionFactory.openSession(),代表一次数据库连接(线程不安全,用完关闭);
  4. 获取 Mapper 代理对象sqlSession.getMapper(UserMapper.class),MyBatis 动态生成代理对象(无需手动实现 Mapper 接口);
  5. 执行 SQL:调用userMapper.selectById(1),代理对象会:
    • 找到UserMapper.xmlid="selectById"的 SQL;
    • 解析#{id}为参数占位符,将 1 映射为 SQL 参数;
    • 用 Executor(执行器)执行 SQL,获取结果集;
  6. 结果映射:将数据库结果集映射为 User 对象(按resultType="com.itheima.pojo.User");
  7. 关闭 SqlSessionsqlSession.close(),释放连接。
加分技巧:
  • 说关键组件:“Executor 是 MyBatis 的核心执行器,有 SimpleExecutor(默认,每次执行新 SQL)和 ReuseExecutor(复用 PreparedStatement),项目中用 ReuseExecutor 提升性能”;
  • 提配置优化:“在 mybatis-config.xml 中配置mapUnderscoreToCamelCase=true,自动将数据库字段user_name映射为 Java 属性userName,不用写 resultMap”。

10. MyBatis 的延迟加载是什么?原理是什么?

回答模板:

MyBatis 的延迟加载(懒加载)是 “查询主表时不加载关联表,用到关联数据时再加载”,减少不必要的 SQL,适用于一对多关联(如用户 - 订单)。

  1. 延迟加载 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 = ?
  2. 开启延迟加载:在mybatis-config.xml中配置:

    <settings>
        <setting name="lazyLoadingEnabled" value="true"/> <!-- 开启延迟加载 -->
        <setting name="aggressiveLazyLoading" value="false"/> <!-- 按需加载 -->
    </settings>
    
  3. 原理:CGLIB 动态代理

    • MyBatis 为 User 生成 CGLIB 代理对象,当调用getOrderList()时,进入代理的拦截器;
    • 拦截器判断orderList是否为null,若是则执行关联 SQL(selectByUserId);
    • 将查询结果通过user.setOrderList(orderList)赋值,再返回orderList
加分技巧:
  • 举项目场景:“用户列表页只需显示用户名,不需要订单,用延迟加载后,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 接口
  1. 一级缓存示例

    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
    }
    
  2. 二级缓存示例

    // 第一个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 个核心加分点

  1. “原理 + 实战” 结合,拒绝空泛面试官不喜欢听 “我知道 AOP 是面向切面编程”,而喜欢听 “我们用 AOP 做操作日志,减少了 80% 重复代码,底层是 CGLIB 代理”—— 用具体场景、数据、工具证明你真的做过。
  2. 理清逻辑链:问题→原因→方案→效果回答任何问题都按这个逻辑:比如 “Spring 事务失效”→ 原因是 “异常被捕获”→ 方案是 “手动抛出异常”→ 效果是 “事务正常回滚,线上 bug 减少 90%”,逻辑清晰,体现解决问题的能力。
  3. 重点掌握 “关联知识点”,应对追问面试常追问 “底层原理”:比如问 SpringBoot 自动配置,会追问@EnableAutoConfiguration的作用;问 MyBatis 延迟加载,会追问 CGLIB 代理的细节 —— 提前把 “表层知识点” 和 “底层原理” 关联起来,避免被问住。

最后,框架面试的核心是 “理解而非死记”:比如 Spring 的循环依赖,不要背三级缓存的定义,要能讲清 “为什么需要三级缓存”“构造函数依赖为什么无法解决”;MyBatis 的缓存,要能说清 “什么时候用二级缓存”“失效场景是什么”。理解了原理,无论面试官怎么问,都能从容应对。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐