Springboot面试全面整理

Springboot面试

一、核心基础提问

1.SpringBoot 优点是什么?
  • 自动配置,简化开发:无需手动编写 XML 配置,SpringBoot 自动识别依赖,按需装配 Bean,减少配置冗余;

  • 内置容器,部署便捷 :内置 Tomcat、Jetty、Undertow 三种容器,无需额外部署容器,直接打成 jar 包,通过 java -jar 即可启动;

  • 依赖管理,避免冲突:提供 Starter 依赖聚合,统一管理依赖版本,解决 Spring 项目中 "版本冲突" 的痛点(比如 Spring 与 SpringMVC 版本不兼容);

  • 开箱即用,集成丰富:集成了常用的框架(SpringMVC、MyBatis、Redis、Kafka 等),引入对应 Starter 即可快速使用,无需额外配置;

  • 强大的配置体系:支持 yml、properties 配置文件,支持多环境切换、配置绑定、松散绑定,满足不同环境(dev/test/prod)的开发需求;

  • 监控便捷,易于维护:内置 Actuator 监控组件,可快速查看项目健康状态、接口信息、线程、内存等,便于线上问题排查;

  • 微服务友好:是 SpringCloud 微服务的基础,无缝集成微服务相关组件(Eureka、Gateway、Feign 等),支持分布式开发;

  • 降低入门门槛:简化了 Spring 框架的使用难度,即使是新手也能快速搭建一个可运行的 Spring 项目。

2.SpringBoot 自动配置原理

SpringBoot 自动配置的核心是「约定大于配置」,底层依赖 3 个核心注解 + SPI 机制 + 条件注解,完整流程如下:

(1)核心注解(三个注解缺一不可)

  • 主类上的

    复制代码
    @SpringBootApplication

    :是一个复合注解,包含 3 个核心子注解:

    1. @SpringBootConfiguration:本质是 @Configuration,标记当前类是配置类,允许定义 Bean;

    2. @ComponentScan:自动扫描当前包及其子包下的 @Component@Service@Controller@Repository 等注解,将其注册为 Spring Bean;

    3. @EnableAutoConfiguration开启自动配置的核心注解,也是自动配置的入口。

(2)@EnableAutoConfiguration 的底层逻辑

  1. @EnableAutoConfiguration 内部导入了 AutoConfigurationImportSelector 类;

  2. AutoConfigurationImportSelector 通过 selectImports() 方法,读取 classpath 下的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件(SpringBoot 2.7+ 版本,之前是 META-INF/spring.factories);

  3. 该文件中配置了所有可自动配置的类(比如 DataSourceAutoConfigurationWebMvcAutoConfiguration),SpringBoot 会加载这些配置类;

  4. 每个自动配置类都包含 条件注解 (如 @ConditionalOnClass@ConditionalOnMissingBean),SpringBoot 会根据 "是否满足条件",决定是否装配该类中的 Bean。

(3)条件注解(自动配置的 "开关")

常用条件注解,决定 Bean 是否被装配:

  • @ConditionalOnClass:当类路径下存在指定类时,才装配 Bean(比如引入 MyBatis 依赖,才会装配 MyBatis 相关 Bean);

  • @ConditionalOnMissingBean:当容器中不存在指定 Bean 时,才装配(避免用户自定义 Bean 被自动配置覆盖);

  • @ConditionalOnProperty:当配置文件中存在指定配置项,且值符合要求时,才装配;

  • @ConditionalOnWebApplication:当项目是 Web 项目时,才装配(比如 Tomcat 相关 Bean)。

(4)总结自动配置流程

启动项目 → 触发 @EnableAutoConfiguration → 加载 imports 文件中的自动配置类 → 通过条件注解判断是否装配 Bean → 完成自动配置,Bean 注入容器。

一句话概括:SpringBoot 先约定好默认配置,再根据用户引入的依赖、自定义的配置,按需调整,不用用户手动干预。

3.SpringBoot 启动流程

SpringBoot 启动的核心是 SpringApplication.run() 方法,整个流程分为 7 个步骤:

  1. 执行主类的 main 方法 ,调用 SpringApplication.run(主类.class, args),传入主类和命令行参数;

  2. 初始化 SpringApplication 实例

    • 加载项目中的资源(配置文件、类路径资源);

    • 初始化监听器(ApplicationListener),用于监听启动过程中的各个事件;

    • 确定应用类型(Web 应用 / 非 Web 应用),默认是 Web 应用(内置 Tomcat);

  3. 准备环境(Environment)

    • 加载系统环境变量、命令行参数、配置文件(application.yml/properties);

    • 合并配置,确定当前环境(dev/test/prod),优先级:命令行参数 > 外部配置 > 内部配置;

  4. 创建并初始化 ApplicationContext 容器

    (Spring 核心容器):

    • 根据应用类型,创建对应的容器(Web 应用创建 AnnotationConfigServletWebServerApplicationContext);

    • 为容器设置环境、监听器等;

  5. 刷新容器(refresh () 方法,核心步骤):

    • 加载 BeanDefinition(解析注解、扫描 Bean);

    • 触发自动配置,装配自动配置类中的 Bean;

    • 初始化所有单例 Bean(默认单例,非懒加载);

    • 触发 Bean 生命周期的各个阶段(实例化、属性填充、初始化);

  6. 启动内置 Web 容器

    • 根据自动配置,启动内置 Tomcat(默认),绑定端口(默认 8080);

    • 将 SpringMVC 的 DispatcherServlet 注册到 Tomcat 中,处理请求;

  7. 启动完成,触发回调

    • 执行 ApplicationRunner、CommandLineRunner 接口的实现类(项目启动后执行的逻辑);

    • 打印启动日志(如 "Started XXXApplication in 2.3 seconds"),项目启动完成。

补充(面试加分)

  • 启动过程中如果出现异常,会触发 SpringApplication 的异常处理机制,打印异常信息并终止启动;

  • 可以通过自定义监听器,监听启动过程中的事件(如 ApplicationStartingEvent、ApplicationReadyEvent),扩展启动逻辑。

4.Starter 是什么?工作原理?自定义 Starter 怎么做?

(1)Starter 是什么?

Starter 是 SpringBoot 提供的依赖聚合包,本质是 "一组依赖 + 自动配置类",核心作用是 "开箱即用"------ 用户只需引入一个 Starter 依赖,SpringBoot 就会自动配置相关的 Bean,无需手动配置。

比如:引入 spring-boot-starter-web,就会自动配置 SpringMVC、Tomcat、DispatcherServlet 等,直接开发 Web 接口;引入 spring-boot-starter-data-redis,就会自动配置 RedisTemplate、ConnectionFactory 等,直接操作 Redis。

(2)Starter 工作原理

  1. Starter 依赖中包含了该场景所需的所有依赖(比如 spring-boot-starter-web 包含 SpringMVC、Tomcat、jackson 等依赖);

  2. Starter 关联一个 autoconfigure(自动配置)模块,该模块中包含自动配置类(如 WebMvcAutoConfiguration);

  3. 自动配置类中通过条件注解,按需装配 Bean;

  4. SpringBoot 启动时,会扫描 autoconfigure 模块中的自动配置类,完成 Bean 装配。

(3)自定义 Starter 步骤

自定义 Starter 分为 2 个模块(规范写法),步骤清晰,可直接说:

  1. 创建两个 Maven 模块:

    • 模块 1:xxx-spring-boot-autoconfigure(自动配置模块)------ 存放自动配置类、Bean 定义;

    • 模块 2:xxx-spring-boot-starter(依赖聚合模块)------ 只做依赖引入,不写业务代码;

  2. 开发 autoconfigure 模块:

    • 编写自动配置类(如 XxxAutoConfiguration),使用 @Configuration 标记,配合条件注解(@ConditionalOnClass@ConditionalOnMissingBean);

    • 编写配置绑定类(如 XxxProperties),使用 @ConfigurationProperties 绑定配置文件中的参数;

    • META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中,注册自动配置类;

  3. 开发 starter 模块:

    • 在 pom.xml 中引入 autoconfigure 模块的依赖,无需其他代码;
  4. 测试自定义 Starter:

    • 新建 SpringBoot 项目,引入自定义的 starter 依赖;

    • 配置相关参数,启动项目,验证自动配置是否生效(如 Bean 是否成功注入容器)。

补充(面试避坑)

  • 自定义 Starter 命名规范:官方 Starter 命名为 spring-boot-starter-xxx,自定义 Starter 命名为 xxx-spring-boot-starter(避免和官方冲突);

  • 核心原则:自动配置类要 "按需装配",避免冗余,允许用户自定义 Bean 覆盖自动配置。

二、配置体系

1.SpringBoot 配置文件的种类、加载顺序?

(1)配置文件种类

SpringBoot 支持 3 种配置文件,优先级从高到低:

  1. application.yml:最常用,可读性强,支持分层配置、松散绑定,推荐使用;

  2. application.properties:传统配置文件,key=value 格式,兼容性好(老项目常用);

  3. application.yaml:和 yml 格式一致,只是后缀不同,使用较少。

(2)配置文件加载顺序(从高到低,高优先级覆盖低优先级)

  1. 命令行参数(如 java -jar xxx.jar --spring.profiles.active=prod);

  2. 外部配置文件(如服务器上的配置文件,通过 --spring.config.location 指定路径);

  3. 项目内部 config 目录下的配置文件(src/main/resources/config/application.yml);

  4. 项目内部 resources 目录下的配置文件(src/main/resources/application.yml);

  5. 内置默认配置(SpringBoot 自带的默认配置,如 Tomcat 默认端口 8080)。

补充(面试加分)

  • 配置文件可以放在不同位置,高优先级的配置会覆盖低优先级的同名配置;

  • 可以通过 spring.config.location 手动指定配置文件路径,适合生产环境(配置文件外置,方便修改,无需重新打包)。

2.多环境配置怎么实现?

SpringBoot 支持多环境配置,核心是 "分文件配置 + 激活指定环境",常用两种方式,推荐第一种:

方式 1:多文件配置(最常用,清晰易懂)

  1. 新建 3 个配置文件,分别对应不同环境:

    • application-dev.yml:开发环境(本地开发,端口 8080,连接本地数据库);

    • application-test.yml:测试环境(测试服务器,端口 8081,连接测试数据库);

    • application-prod.yml:生产环境(正式服务器,端口 80,连接生产数据库);

  2. 在主配置文件 application.yml 中,激活指定环境:yaml

复制代码
spring:
  profiles:
    active: dev  # 激活开发环境,可改为 test/prod
  1. 打包 / 启动时,通过命令行参数指定环境(覆盖主配置):bash运行
复制代码
# 启动生产环境
java -jar xxx.jar --spring.profiles.active=prod
# 启动测试环境
java -jar xxx.jar --spring.profiles.active=test

方式 2:单文件多环境(适合简单场景)

在同一个 application.yml 中,用 --- 分隔不同环境的配置:yaml

复制代码
# 主配置(所有环境共用)
spring:
  profiles:
    active: dev  # 激活开发环境
​
# 开发环境
---
spring:
  profiles: dev
server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dev_db
​
# 测试环境
---
spring:
  profiles: test
server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://test-server:3306/test_db
​
# 生产环境
---
spring:
  profiles: prod
server:
  port: 80
spring:
  datasource:
    url: jdbc:mysql://prod-server:3306/prod_db

补充(面试加分)

  • 多环境配置中,共用配置放在主配置文件,环境专属配置放在对应环境的文件中,避免重复;

  • 生产环境中,建议将敏感配置(如数据库密码、Redis 密码)外置(如服务器环境变量、配置中心),避免硬编码。

3.@ConfigurationProperties 和 @Value 的区别?

两者都是 SpringBoot 中用于注入配置文件参数的注解,但适用场景不同,核心区别如下,结合项目说更加分:

维度 @Value @ConfigurationProperties
注入方式 单个参数注入,支持 SpEL 表达式 批量注入,支持前缀匹配,绑定整个配置对象
支持类型 基本类型(String、int、boolean)、String [] 基本类型、复杂对象(实体类)、集合、Map
松散绑定 不支持(必须严格匹配配置文件中的 key) 支持(如配置文件中是 user-name,可绑定到 userName)
数据校验 不支持 支持(配合 @Validated、@NotNull、@Min 等注解)
适用场景 简单配置,单个参数注入(如端口、日志级别) 复杂配置,批量参数注入(如数据库配置、Redis 配置)
项目实战示例
  • 用 @Value 注入单个简单配置:java运行
复制代码
// 注入端口号
@Value("${server.port}")
private Integer port;
​
// 注入日志级别(支持 SpEL)
@Value("${logging.level.root:info}") // 冒号后是默认值
private String logLevel;
  • 用 @ConfigurationProperties 注入复杂配置(如数据库):java
复制代码
// 配置文件 yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
​
// 配置绑定类
@ConfigurationProperties(prefix = "spring.datasource")
@Component
@Validated // 开启数据校验
public class DataSourceProperties {
    @NotNull(message = "数据库URL不能为空")
    private String url;
    private String username;
    private String password;
    @NotNull(message = "驱动类名不能为空")
    private String driverClassName; // 松散绑定,对应配置文件中的 driver-class-name
    
    // getter/setter 省略
}

总结(面试口述)

  • 简单配置(单个参数)用 @Value,灵活、简洁;

  • 复杂配置(多个参数、实体类)用 @ConfigurationProperties,批量绑定、支持校验、可读性强,企业级项目优先使用。

4.SpringBoot 如何加载外部配置

生产环境中,配置文件通常外置(避免硬编码、方便修改,无需重新打包),SpringBoot 支持 4 种加载外部配置的方式,优先级从高到低:

  1. 命令行参数

    (最常用):启动时通过 --key=value指定,覆盖配置文件中的参数:bash运行

    复制代码
    java -jar xxx.jar --spring.datasource.password=123456 --server.port=80
  2. 指定外部配置文件路径:启动时通过

    复制代码
    --spring.config.location

    指定外部配置文件的路径:bash运行

    复制代码
    # 加载服务器上的配置文件
    java -jar xxx.jar --spring.config.location=/usr/local/config/application-prod.yml
  3. 环境变量:将配置参数设置为服务器的环境变量,SpringBoot 会自动读取(适合敏感配置,如密码);

    • 配置文件中可以通过 ${环境变量名} 引用:+

      yaml

      复制代码
      spring:
        datasource:
          password: ${DB_PASSWORD} # 读取服务器环境变量 DB_PASSWORD
  4. 配置中心(微服务常用):将配置文件放在 Nacos、Apollo 等配置中心,SpringBoot 启动时从配置中心拉取配置,支持动态刷新配置。

三、SpringMVC 核心

1.SpringBoot 内置的 Web 容器有哪些?如何切换?

(1)内置 Web 容器(4 种)

SpringBoot 内置了 4 种 Web 容器,默认使用 Tomcat:

  1. Tomcat(默认):轻量级、稳定,适合大多数 Web 项目;

  2. Jetty:轻量级,启动速度快,适合嵌入式项目、微服务;

  3. Undertow:高性能、高并发,基于 NIO,适合高并发场景;

  4. Netty:基于 NIO 的异步容器,适合 Reactive 编程(Spring WebFlux)。

(2)切换 Web 容器(以切换为 Undertow 为例)

核心:排除默认的 Tomcat 依赖,引入目标容器的 Starter 依赖,无需额外配置:

复制代码
<!-- pom.xml 配置 -->
<dependencies>
    <!-- 引入 Web 依赖,排除默认 Tomcat -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- 引入 Undertow 容器 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
</dependencies>

补充(面试加分)

  • 切换 Jetty 的方式和 Undertow 类似,排除 Tomcat 后,引入 spring-boot-starter-jetty 依赖;

  • 生产环境中,高并发场景推荐使用 Undertow,启动速度快、并发性能好;普通场景用默认 Tomcat 即可。

2.拦截器 Interceptor 和过滤器 Filter 的区别

两者都是用于 "拦截请求、处理请求" 的组件,但所处层级、作用范围、使用场景完全不同,核心区别如下,结合项目场景说更加分:

维度 Filter(过滤器) Interceptor(拦截器)
所处层级 Servlet 容器层(Tomcat 层面),早于 Spring 容器 SpringMVC 层(Spring 容器层面),晚于 Filter
依赖环境 依赖 Servlet 容器,不依赖 Spring 框架 依赖 Spring 框架,受 Spring 容器管理
拦截范围 拦截所有请求(包括静态资源、HTML、CSS、接口) 只拦截 SpringMVC 处理的请求(即 Controller 接口),不拦截静态资源
可操作对象 只能操作 Servlet 的 Request、Response 对象,无法获取 Spring Bean 可以获取 Spring Bean(如 Service、Dao),可以操作 Controller 的方法参数、返回值
生命周期 由 Servlet 容器管理,随容器启动而初始化,随容器关闭而销毁 由 Spring 容器管理,遵循 Spring Bean 的生命周期
执行顺序 先执行 Filter,再执行 Interceptor 后执行 Interceptor,在 Controller 前后执行

项目实战场景

  • Filter 适用场景:跨域处理、编码设置、全局请求拦截(如禁止非法 IP 访问)、日志记录(记录所有请求的 URL、参数);

    复制代码
    // 自定义 Filter 示例(跨域过滤器)
    @WebFilter(urlPatterns = "/*")
    public class CorsFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletResponse res = (HttpServletResponse) response;
            // 设置跨域允许的域名、方法、请求头
            res.setHeader("Access-Control-Allow-Origin", "*");
            res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE");
            chain.doFilter(request, response); // 放行请求
        }
    }
  • Interceptor 适用场景:登录校验(Token 校验)、权限校验、接口日志(记录接口的执行时间、参数、返回值)、接口防重;

    复制代码
    // 自定义 Interceptor 示例(登录校验)
    @Component
    public class LoginInterceptor implements HandlerInterceptor {
        // 接口执行前拦截
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 获取 Token
            String token = request.getHeader("Token");
            if (token == null || !token.equals("valid_token")) {
                response.getWriter().write("请先登录");
                return false; // 拦截请求,不执行 Controller
            }
            return true; // 放行请求
        }
    }
    ​
    // 注册 Interceptor
    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Autowired
        private LoginInterceptor loginInterceptor;
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // 拦截所有接口,排除登录接口
            registry.addInterceptor(loginInterceptor)
                    .addPathPatterns("/**")
                    .excludePathPatterns("/login");
        }
    }

总结(面试口述)

Filter 是 Servlet 层面的拦截,管的 "更宽",所有请求都能拦,但无法操作 Spring Bean;Interceptor 是 SpringMVC 层面的拦截,只拦接口,能操作 Spring Bean,更适合业务相关的拦截(如登录、权限)。

3.全局异常处理怎么实现?

SpringBoot 提供 @RestControllerAdvice + @ExceptionHandler 注解,实现全局异常捕获,统一返回前端标准 JSON 格式,避免在每个 Controller 中重复写 try-catch,核心步骤:

  1. 新建全局异常处理类,用 @RestControllerAdvice 标记(本质是 @Component,作用于所有 Controller);

  2. @ExceptionHandler(异常类型.class) 标记方法,指定该方法处理哪种异常;

  3. 统一封装异常返回结果(如状态码、错误信息),返回给前端。

实战代码示例(可直接复制到项目)

复制代码
// 全局异常处理类
@RestControllerAdvice
public class GlobalExceptionHandler {
​
    // 1. 处理自定义异常(项目中常用,如业务异常)
    @ExceptionHandler(BusinessException.class)
    public ResultVO handleBusinessException(BusinessException e) {
        // 自定义异常,返回业务状态码和错误信息
        return ResultVO.fail(e.getCode(), e.getMessage());
    }
​
    // 2. 处理系统异常(如空指针、数组越界)
    @ExceptionHandler(RuntimeException.class)
    public ResultVO handleRuntimeException(RuntimeException e) {
        // 系统异常,返回统一状态码 500
        log.error("系统异常:", e); // 打印异常栈,便于排查
        return ResultVO.fail(500, "系统异常,请联系管理员");
    }
​
    // 3. 处理所有异常(兜底)
    @ExceptionHandler(Exception.class)
    public ResultVO handleException(Exception e) {
        log.error("未知异常:", e);
        return ResultVO.fail(500, "未知异常,请联系管理员");
    }
​
    // 统一返回结果封装类
    @Data
    public static class ResultVO {
        private Integer code; // 状态码(200成功,500失败)
        private String message; // 错误信息
        private Object data; // 响应数据
​
        // 成功方法
        public static ResultVO success(Object data) {
            ResultVO vo = new ResultVO();
            vo.setCode(200);
            vo.setMessage("success");
            vo.setData(data);
            return vo;
        }
​
        // 失败方法
        public static ResultVO fail(Integer code, String message) {
            ResultVO vo = new ResultVO();
            vo.setCode(code);
            vo.setMessage(message);
            vo.setData(null);
            return vo;
        }
    }
​
    // 自定义业务异常
    public static class BusinessException extends RuntimeException {
        private Integer code;
​
        public BusinessException(Integer code, String message) {
            super(message);
            this.code = code;
        }
​
        // getter/setter 省略
    }
}

补充

  • 可以按异常类型细分处理(如空指针、SQL 异常),返回更精准的错误信息;

  • 异常处理中要打印异常栈(log.error),便于线上问题排查;

  • 自定义异常可以携带业务状态码(如 400 参数错误、401 未登录),让前端更好地处理异常。

4.静态资源放行规则?如何自定义静态资源路径?

1)默认静态资源放行规则

SpringBoot 默认会放行 4 个目录下的静态资源,无需额外配置,直接访问即可:

  • classpath:/static/(最常用,存放 CSS、JS、图片等);

  • classpath:/public/

  • classpath:/resources/

  • classpath:/META-INF/resources/(存放 WebJar 资源,如 jQuery、Bootstrap)。

访问方式:直接通过 URL 访问静态资源,无需加目录前缀,比如:

  • 静态资源路径:src/main/resources/static/img/logo.png

  • 访问 URL:http://localhost:8080/img/logo.png

(2)自定义静态资源路径

当默认路径满足不了需求(如静态资源放在外部目录),可以通过 WebMvcConfigurer 自定义:

复制代码
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    // 自定义静态资源路径
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 1. 自定义项目内静态资源路径
        registry.addResourceHandler("/static/**") // 访问路径前缀
                .addResourceLocations("classpath:/my-static/"); // 实际资源目录
        // 2. 自定义外部静态资源路径(如服务器上的静态资源)
        registry.addResourceHandler("/external/**")
                .addResourceLocations("file:/usr/local/static/"); // file: 表示本地文件路径
    }
}

(3)补充(面试避坑)

  • 如果项目中配置了拦截器,要注意 排除静态资源的拦截,否则静态资源无法访问;

  • 静态资源可以通过 CDN 加速(生产环境常用),减少服务器压力。

四、事务管理

1.@Transactional 注解的原理?

@Transactional 是 SpringBoot 中用于声明式事务管理的核心注解,底层基于 AOP 动态代理实现,完整原理如下:

  1. 开启事务注解支持 :SpringBoot 自动配置类 TransactionAutoConfiguration,默认开启 @EnableTransactionManagement,允许使用 @Transactional 注解;

  2. 动态代理生成 :Spring 会为带有 @Transactional 注解的类 / 方法,生成一个代理类(JDK 动态代理或 CGLIB 动态代理);

  3. 事务拦截器 :代理类中会植入 TransactionInterceptor(事务拦截器),拦截目标方法的执行;

  4. 事务流程控制:

    • 方法执行前:拦截器开启事务(通过 Spring 事务管理器 PlatformTransactionManager),设置事务隔离级别、传播行为等;

    • 方法执行中:执行目标方法的业务逻辑;

    • 方法执行成功:拦截器提交事务;

    • 方法执行失败(抛出异常):拦截器回滚事务;

  5. 底层依赖:事务的最终实现依赖数据库的事务(如 MySQL 的事务),Spring 只是对数据库事务进行了封装,简化开发。

补充(面试加分)

  • 如果目标类实现了接口,用 JDK 动态代理;如果没有实现接口,用 CGLIB 动态代理;

  • 事务管理器 PlatformTransactionManager 是核心,不同的数据源对应不同的实现(如 JDBC 对应 DataSourceTransactionManager,MyBatis 也用这个)。

2.@Transactional 注解的常用属性?

@Transactional 有多个属性,用于配置事务的隔离级别、传播行为、超时时间等,常用属性如下,结合项目场景说明:

  1. value/transactionManager:指定事务管理器,多数据源场景下使用(如主从数据源,分别配置事务管理器);

  2. propagation:事务传播行为(最常用),定义多个事务之间的关系,常用值:

    • REQUIRED(默认):如果当前没有事务,就新建一个事务;如果当前有事务,就加入当前事务(适合大多数业务场景,如订单创建 + 库存扣减);

    • REQUIRES_NEW:无论当前是否有事务,都新建一个事务,新事务和原事务独立(适合日志记录、消息发送,即使主事务回滚,日志也能提交);

    • SUPPORTS:如果当前有事务,就加入;如果没有,就不使用事务(适合查询接口,可选事务);

    • NESTED:嵌套事务,在当前事务内新建一个子事务,子事务回滚不影响主事务,主事务回滚会带动子事务回滚(适合复杂业务拆分);

  3. isolation:事务隔离级别,对应 MySQL 四大隔离级别,默认使用数据库的隔离级别(MySQL 默认 RR);

    • Isolation.READ_COMMITTED:读已提交,避免脏读;

    • Isolation.REPEATABLE_READ:可重复读(默认),避免脏读、不可重复读;

  4. timeout:事务超时时间(单位:秒),默认 -1(不超时);如果事务执行时间超过设定值,自动回滚(防止长事务占用资源);

  5. readOnly:是否为只读事务,默认 false;如果是查询接口,设置为 true,优化性能(数据库会对只读事务做优化);

  6. rollbackFor:指定需要回滚的异常类型(如自定义业务异常),默认只回滚 RuntimeException 及其子类;

    • 示例:@Transactional(rollbackFor = BusinessException.class),只有抛出该异常时才回滚;
  7. noRollbackFor:指定不需要回滚的异常类型,即使抛出该异常,事务也不回滚。

3.@Transactional 什么时候失效?

@Transactional 失效的核心原因是:没有生成代理类,或者事务拦截器没有拦截到方法执行,常见 6 种场景,结合原理说明,面试不慌:

  1. 方法不是 public 修饰

    • 原理:Spring 的事务拦截器 TransactionInterceptor 只拦截 public 方法,非 public 方法(private、protected)不会被拦截,事务失效;

    • 解决方案:将方法改为 public 修饰。

  2. 内部 this 调用(未走代理)

    • 场景:在同一个 Service 类中,用 this 调用带有 @Transactional 注解的方法;

    • 原理:this 是当前类的实例,不是 Spring 生成的代理类,事务拦截器无法拦截,事务失效;

    • 示例:

      复制代码
      @Service
      public class OrderService {
          // 方法1:无事务
          public void createOrder() {
              // this 调用,事务失效
              this.doCreate();
          }
      ​
          // 方法2:有事务
          @Transactional
          public void doCreate() {
              // 业务逻辑
          }
      }
    • 解决方案:

      • 注入自身代理对象(@Autowired private OrderService thisService;,用 thisService 调用);

      • 从 Spring 容器中获取代理对象(ApplicationContext.getBean(OrderService.class));

      • 将方法拆分到不同的 Service 类中。

  3. try-catch 吃掉异常,未抛出

    • 场景:方法中用 try-catch 捕获了异常,没有重新抛出,事务无法感知异常,不会回滚;

    • 原理:Spring 事务回滚的前提是 "方法抛出未被捕获的异常",如果异常被吃掉,事务会认为方法执行成功,直接提交;

    • 解决方案:

      • 捕获异常后,重新抛出(throw new RuntimeException(e););

      • 手动回滚事务(TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();)。

  4. 多线程跨线程调用

    • 场景:在一个事务方法中,开启新线程,调用另一个事务方法;

    • 原理:新线程中的方法,不在当前事务的上下文范围内,事务无法传递,新线程中的事务失效;

    • 示例:

      复制代码
      @Transactional
      public void createOrder() {
          // 开启新线程
          new Thread(() -> {
              // 新线程中的方法,事务失效
              stockService.deductStock();
          }).start();
      }
    • 解决方案:避免多线程跨线程调用事务方法;如果必须用,使用分布式事务(如 Seata)。

  5. 事务传播行为配置错误

    • 场景:配置了 propagation = Propagation.NOT_SUPPORTED(不支持事务)、propagation = Propagation.NEVER(禁止事务);

    • 原理:这类传播行为会导致方法不使用事务,即使加了 @Transactional,也会失效;

    • 解决方案:根据业务场景,配置正确的传播行为(常用 REQUIRED、REQUIRES_NEW)。

  6. 数据库引擎不支持事务

    • 场景:使用 MySQL 的 MyISAM 引擎(不支持事务);

    • 原理:事务的最终实现依赖数据库,如果数据库引擎不支持事务,Spring 事务也无法生效;

    • 解决方案:将数据库引擎改为 InnoDB(支持事务,MySQL 5.5+ 默认)。

补充(面试加分)

  • 还有一种特殊情况:@Transactional 注解加在接口上,且目标类没有实现该接口,事务会失效(因为 JDK 动态代理基于接口,无法代理接口上的注解);

  • 排查事务失效的核心:看方法是否被代理类调用,是否有未被捕获的异常,传播行为是否正确。

4.声明式事务和编程式事务的区别?

Spring 提供两种事务管理方式:声明式事务(@Transactional)和编程式事务,核心区别如下:

维度 声明式事务(@Transactional) 编程式事务
实现方式 注解配置,无需手动写事务代码 手动编写代码(如 TransactionTemplate)
代码侵入性 低(只加注解),不侵入业务代码 高(业务代码中嵌入事务代码)
灵活性 低(只能通过注解属性配置) 高(可手动控制事务的开启、提交、回滚)
适用场景 大多数业务场景(如 CRUD、简单业务) 复杂业务场景(如多数据源、动态控制事务)
开发效率 高(无需手动处理事务流程) 低(需要手动编写事务逻辑)

编程式事务示例

复制代码
@Service
public class OrderService {
    @Autowired
    private TransactionTemplate transactionTemplate;
    @Autowired
    private OrderMapper orderMapper;
    // 编程式事务
    public void createOrder(OrderDTO orderDTO) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    // 业务逻辑
                    orderMapper.insert(orderDTO);
                    // 手动回滚(如需)
                    // status.setRollbackOnly();
                } catch (Exception e) {
                    // 异常回滚
                    status.setRollbackOnly();
                    throw new RuntimeException(e);
                }
            }
        });
    }
}

总结(面试口述)

  • 大多数场景用声明式事务,简洁、低侵入,开发效率高;

  • 复杂场景(如多数据源、动态控制事务)用编程式事务,灵活性高,能精准控制事务流程。

五、Bean 生命周期 + IOC 容器

1.Spring Boot 中 Bean 的生命周期

Spring Boot 中 Bean 的生命周期,本质和 Spring 一致,核心是 "从创建到销毁" 的全过程,分为 7 个步骤,结合注解说明:

  1. 实例化(Instantiation):Spring 容器通过反射,创建 Bean 的实例(调用无参构造方法);

  2. 属性填充(Population) :Spring 容器将配置文件中、其他 Bean 中的属性,注入到当前 Bean 中(如 @Autowired 注入依赖);

  3. BeanNameAware 接口回调 :如果 Bean 实现了 BeanNameAware 接口,Spring 会调用 setBeanName() 方法,将 Bean 的名称注入;

  4. BeanFactoryAware 接口回调 :如果 Bean 实现了 BeanFactoryAware 接口,Spring 会调用 setBeanFactory() 方法,将 BeanFactory 容器注入;

  5. 初始化(Initialization)

    • 调用 @PostConstruct 注解标记的方法(Bean 初始化前执行);

    • 如果 Bean 实现了 InitializingBean 接口,调用 afterPropertiesSet() 方法;

    • 调用 XML 配置中 init-method 指定的方法(现在很少用);

  6. Bean 就绪:Bean 初始化完成,存入 Spring 容器,供其他 Bean 调用;

  7. 销毁(Destruction)

    • 调用 @PreDestroy 注解标记的方法(Bean 销毁前执行);

    • 如果 Bean 实现了 DisposableBean 接口,调用 destroy() 方法;

    • 调用 XML 配置中 destroy-method 指定的方法;

    • 容器关闭时,Bean 被销毁,释放资源。

实战代码示例(直观理解)

复制代码
@Component
public class UserService implements BeanNameAware, InitializingBean, DisposableBean {
​
    // 依赖注入(属性填充)
    @Autowired
    private UserMapper userMapper;
​
    // 1. 实例化(无参构造)
    public UserService() {
        System.out.println("UserService 实例化");
    }
​
    // 2. 属性填充(@Autowired 注入,Spring 自动执行)
​
    // 3. BeanNameAware 回调
    @Override
    public void setBeanName(String name) {
        System.out.println("Bean 名称:" + name);
    }
​
    // 4. 初始化前:@PostConstruct
    @PostConstruct
    public void initBefore() {
        System.out.println("UserService 初始化前执行");
    }
​
    // 5. InitializingBean 回调(初始化)
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("UserService 初始化执行");
    }
​
    // 6. Bean 就绪,业务方法
    public void getUser() {
        userMapper.selectById(1L);
    }
​
    // 7. 销毁前:@PreDestroy
    @PreDestroy
    public void destroyBefore() {
        System.out.println("UserService 销毁前执行");
    }
​
    // 8. DisposableBean 回调(销毁)
    @Override
    public void destroy() throws Exception {
        System.out.println("UserService 销毁执行");
    }
}

补充(面试加分)

  • Bean 的生命周期由 Spring 容器管理,我们可以通过 @PostConstruct@PreDestroy 注解,自定义 Bean 的初始化和销毁逻辑;

  • 单例 Bean 的生命周期:容器启动时创建,容器关闭时销毁;

  • 多例 Bean 的生命周期:每次获取 Bean 时创建,使用完后由 JVM 垃圾回收(Spring 不管理多例 Bean 的销毁)。

2.Spring Boot 中 Bean 的作用域?

Bean 的作用域定义了 Bean 在 Spring 容器中的创建时机、存活时间、实例数量,Spring Boot 支持 5 种作用域,常用 4 种:

  1. singleton(单例,默认)

    • 定义:整个 Spring 容器中,只有一个 Bean 实例,容器启动时创建(非懒加载),所有请求共享该实例;

    • 适用场景:Service、Dao、Controller(无状态,线程安全);

    • 注意:如果单例 Bean 有可变成员变量,会存在线程安全问题,需加锁或用 ThreadLocal。

  2. prototype(多例)

    • 定义:每次获取 Bean 时(如 context.getBean()@Autowired),都会创建一个新的实例;

    • 适用场景:有状态的 Bean(如 Request、Session 相关的 Bean);

    • 注意:Spring 不管理多例 Bean 的销毁,由 JVM 垃圾回收。

  3. request(请求域)

    • 定义:每个 HTTP 请求,创建一个新的 Bean 实例,请求结束后,Bean 被销毁;

    • 适用场景:Web 项目中,与当前请求相关的 Bean(如请求参数封装类);

    • 注意:只适用于 Web 环境(内置 Tomcat 等容器)。

  4. session(会话域)

    • 定义:每个 HTTP Session,创建一个新的 Bean 实例,会话结束后,Bean 被销毁;

    • 适用场景:Web 项目中,与用户会话相关的 Bean(如用户登录信息);

    • 注意:只适用于 Web 环境。

  5. application(应用域)

    • 定义:整个 Web 应用生命周期内,只有一个 Bean 实例,和单例类似,但只适用于 Web 环境;

    • 适用场景:Web 应用全局共享的 Bean(如全局配置);

    • 注意:很少用,单例基本能满足需求。

如何设置 Bean 作用域?

@Scope 注解,指定作用域:

复制代码
// 多例 Bean
@Scope("prototype")
@Component
public class UserDTO {
    // 有状态的属性
    private String username;
    // getter/setter 省略
}
​
// 请求域 Bean
@Scope("request")
@Component
public class RequestParamDTO {
    // 请求参数
}
3.单例 Bean 线程安全吗?为什么?如何解决?

(1)单例 Bean 线程安全吗?

不一定安全,核心取决于 Bean 是否有 "可变成员变量":

  • 无可变成员变量(纯业务逻辑,无状态):线程安全(如 Service 中只有方法,没有成员变量);

  • 有可变成员变量(有状态):线程不安全(如 Bean 中有一个 private int count,多个线程同时修改该变量,会出现线程安全问题)。

(2)为什么不安全?

单例 Bean 是整个容器共享的,多个线程会同时访问 Bean 的成员变量,如果没有同步机制,会出现 "并发修改" 的问题(如脏读、数据覆盖)。

(3)解决方案(项目常用)

  1. 避免使用可变成员变量:尽量让单例 Bean 无状态(只写方法,不定义可变成员变量);

  2. 使用 ThreadLocal:将可变成员变量存入 ThreadLocal,实现线程隔离(每个线程有自己的变量副本);

    复制代码
    @Component
    public class UserService {
        // ThreadLocal 存储线程隔离的变量
        private ThreadLocal<String> username = new ThreadLocal<>();
    ​
        public void setUsername(String name) {
            username.set(name);
        }
    ​
        public String getUsername() {
            return username.get();
        }
    ​
        // 用完移除,防止内存泄漏
        public void removeUsername() {
            username.remove();
        }
    }
  3. 加锁同步:在修改成员变量的方法上,加 synchronized 或 ReentrantLock,保证同一时间只有一个线程修改;

    复制代码
    @Component
    public class CounterService {
        private int count = 0;
        private final Lock lock = new ReentrantLock();
    ​
        public void increment() {
            lock.lock();
            try {
                count++; // 加锁,避免并发修改
            } finally {
                lock.unlock();
            }
        }
    ​
        public int getCount() {
            return count;
        }
    }
  4. 改为多例 Bean:如果 Bean 状态复杂,可将 @Scope 改为 prototype,每次获取新实例,避免线程共享。

相关推荐
小冷coding2 小时前
【面试】结合项目整理的场景面试题,覆盖 Java 基础、锁、多线程、数据库、分布式锁 / 事务、消息中间件等核心维度
java·数据库·面试
文心快码BaiduComate2 小时前
Comate搭载GLM-5.1:长程8H,对齐Opus 4.6
前端·后端·架构
我叫黑大帅3 小时前
PHP中的官方操作数据库PDO
后端·面试·php
用户92239610327283 小时前
不用 nohup、不用 sshpass!rsync 后台传输 + 断 SSH 不中断的原生玩法
后端
青柠代码录3 小时前
【SpringBoot】过滤器
后端
元宝骑士3 小时前
MySQL联表查询优化实战:小表驱动大表的联合索引设计
后端·mysql
用户69371750013843 小时前
Android 开发,别只钻技术一亩三分地,也该学点“广度”了
android·前端·后端
张元清3 小时前
React 鼠标追踪与交互效果实战
前端·javascript·面试
gogogo出发喽3 小时前
使用Pear Admin Flask
后端·python·flask