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 个核心子注解:
-
@SpringBootConfiguration:本质是@Configuration,标记当前类是配置类,允许定义 Bean; -
@ComponentScan:自动扫描当前包及其子包下的@Component、@Service、@Controller、@Repository等注解,将其注册为 Spring Bean; -
@EnableAutoConfiguration:开启自动配置的核心注解,也是自动配置的入口。
-
(2)@EnableAutoConfiguration 的底层逻辑
-
@EnableAutoConfiguration内部导入了AutoConfigurationImportSelector类; -
AutoConfigurationImportSelector通过selectImports()方法,读取 classpath 下的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件(SpringBoot 2.7+ 版本,之前是 META-INF/spring.factories); -
该文件中配置了所有可自动配置的类(比如
DataSourceAutoConfiguration、WebMvcAutoConfiguration),SpringBoot 会加载这些配置类; -
每个自动配置类都包含 条件注解 (如
@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 个步骤:
-
执行主类的 main 方法 ,调用
SpringApplication.run(主类.class, args),传入主类和命令行参数; -
初始化 SpringApplication 实例:
-
加载项目中的资源(配置文件、类路径资源);
-
初始化监听器(ApplicationListener),用于监听启动过程中的各个事件;
-
确定应用类型(Web 应用 / 非 Web 应用),默认是 Web 应用(内置 Tomcat);
-
-
准备环境(Environment):
-
加载系统环境变量、命令行参数、配置文件(application.yml/properties);
-
合并配置,确定当前环境(dev/test/prod),优先级:命令行参数 > 外部配置 > 内部配置;
-
-
创建并初始化 ApplicationContext 容器
(Spring 核心容器):
-
根据应用类型,创建对应的容器(Web 应用创建 AnnotationConfigServletWebServerApplicationContext);
-
为容器设置环境、监听器等;
-
-
刷新容器(refresh () 方法,核心步骤):
-
加载 BeanDefinition(解析注解、扫描 Bean);
-
触发自动配置,装配自动配置类中的 Bean;
-
初始化所有单例 Bean(默认单例,非懒加载);
-
触发 Bean 生命周期的各个阶段(实例化、属性填充、初始化);
-
-
启动内置 Web 容器:
-
根据自动配置,启动内置 Tomcat(默认),绑定端口(默认 8080);
-
将 SpringMVC 的 DispatcherServlet 注册到 Tomcat 中,处理请求;
-
-
启动完成,触发回调:
-
执行 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 工作原理
-
Starter 依赖中包含了该场景所需的所有依赖(比如
spring-boot-starter-web包含 SpringMVC、Tomcat、jackson 等依赖); -
Starter 关联一个 autoconfigure(自动配置)模块,该模块中包含自动配置类(如
WebMvcAutoConfiguration); -
自动配置类中通过条件注解,按需装配 Bean;
-
SpringBoot 启动时,会扫描 autoconfigure 模块中的自动配置类,完成 Bean 装配。
(3)自定义 Starter 步骤
自定义 Starter 分为 2 个模块(规范写法),步骤清晰,可直接说:
-
创建两个 Maven 模块:
-
模块 1:xxx-spring-boot-autoconfigure(自动配置模块)------ 存放自动配置类、Bean 定义;
-
模块 2:xxx-spring-boot-starter(依赖聚合模块)------ 只做依赖引入,不写业务代码;
-
-
开发 autoconfigure 模块:
-
编写自动配置类(如
XxxAutoConfiguration),使用@Configuration标记,配合条件注解(@ConditionalOnClass、@ConditionalOnMissingBean); -
编写配置绑定类(如
XxxProperties),使用@ConfigurationProperties绑定配置文件中的参数; -
在
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中,注册自动配置类;
-
-
开发 starter 模块:
- 在 pom.xml 中引入 autoconfigure 模块的依赖,无需其他代码;
-
测试自定义 Starter:
-
新建 SpringBoot 项目,引入自定义的 starter 依赖;
-
配置相关参数,启动项目,验证自动配置是否生效(如 Bean 是否成功注入容器)。
-
补充(面试避坑)
-
自定义 Starter 命名规范:官方 Starter 命名为
spring-boot-starter-xxx,自定义 Starter 命名为xxx-spring-boot-starter(避免和官方冲突); -
核心原则:自动配置类要 "按需装配",避免冗余,允许用户自定义 Bean 覆盖自动配置。
二、配置体系
1.SpringBoot 配置文件的种类、加载顺序?
(1)配置文件种类
SpringBoot 支持 3 种配置文件,优先级从高到低:
-
application.yml:最常用,可读性强,支持分层配置、松散绑定,推荐使用; -
application.properties:传统配置文件,key=value 格式,兼容性好(老项目常用); -
application.yaml:和 yml 格式一致,只是后缀不同,使用较少。
(2)配置文件加载顺序(从高到低,高优先级覆盖低优先级)
-
命令行参数(如
java -jar xxx.jar --spring.profiles.active=prod); -
外部配置文件(如服务器上的配置文件,通过
--spring.config.location指定路径); -
项目内部 config 目录下的配置文件(
src/main/resources/config/application.yml); -
项目内部 resources 目录下的配置文件(
src/main/resources/application.yml); -
内置默认配置(SpringBoot 自带的默认配置,如 Tomcat 默认端口 8080)。
补充(面试加分)
-
配置文件可以放在不同位置,高优先级的配置会覆盖低优先级的同名配置;
-
可以通过
spring.config.location手动指定配置文件路径,适合生产环境(配置文件外置,方便修改,无需重新打包)。
2.多环境配置怎么实现?
SpringBoot 支持多环境配置,核心是 "分文件配置 + 激活指定环境",常用两种方式,推荐第一种:
方式 1:多文件配置(最常用,清晰易懂)
-
新建 3 个配置文件,分别对应不同环境:
-
application-dev.yml:开发环境(本地开发,端口 8080,连接本地数据库); -
application-test.yml:测试环境(测试服务器,端口 8081,连接测试数据库); -
application-prod.yml:生产环境(正式服务器,端口 80,连接生产数据库);
-
-
在主配置文件
application.yml中,激活指定环境:yaml
spring:
profiles:
active: dev # 激活开发环境,可改为 test/prod
- 打包 / 启动时,通过命令行参数指定环境(覆盖主配置):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 种加载外部配置的方式,优先级从高到低:
-
命令行参数
(最常用):启动时通过 --key=value指定,覆盖配置文件中的参数:bash运行
java -jar xxx.jar --spring.datasource.password=123456 --server.port=80 -
指定外部配置文件路径:启动时通过
--spring.config.location指定外部配置文件的路径:bash运行
# 加载服务器上的配置文件 java -jar xxx.jar --spring.config.location=/usr/local/config/application-prod.yml -
环境变量:将配置参数设置为服务器的环境变量,SpringBoot 会自动读取(适合敏感配置,如密码);
-
配置文件中可以通过 ${环境变量名} 引用:+
yaml
spring: datasource: password: ${DB_PASSWORD} # 读取服务器环境变量 DB_PASSWORD
-
-
配置中心(微服务常用):将配置文件放在 Nacos、Apollo 等配置中心,SpringBoot 启动时从配置中心拉取配置,支持动态刷新配置。
三、SpringMVC 核心
1.SpringBoot 内置的 Web 容器有哪些?如何切换?
(1)内置 Web 容器(4 种)
SpringBoot 内置了 4 种 Web 容器,默认使用 Tomcat:
-
Tomcat(默认):轻量级、稳定,适合大多数 Web 项目;
-
Jetty:轻量级,启动速度快,适合嵌入式项目、微服务;
-
Undertow:高性能、高并发,基于 NIO,适合高并发场景;
-
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,核心步骤:
-
新建全局异常处理类,用
@RestControllerAdvice标记(本质是@Component,作用于所有 Controller); -
用
@ExceptionHandler(异常类型.class)标记方法,指定该方法处理哪种异常; -
统一封装异常返回结果(如状态码、错误信息),返回给前端。
实战代码示例(可直接复制到项目)
// 全局异常处理类
@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 动态代理实现,完整原理如下:
-
开启事务注解支持 :SpringBoot 自动配置类
TransactionAutoConfiguration,默认开启@EnableTransactionManagement,允许使用@Transactional注解; -
动态代理生成 :Spring 会为带有
@Transactional注解的类 / 方法,生成一个代理类(JDK 动态代理或 CGLIB 动态代理); -
事务拦截器 :代理类中会植入
TransactionInterceptor(事务拦截器),拦截目标方法的执行; -
事务流程控制:
-
方法执行前:拦截器开启事务(通过 Spring 事务管理器
PlatformTransactionManager),设置事务隔离级别、传播行为等; -
方法执行中:执行目标方法的业务逻辑;
-
方法执行成功:拦截器提交事务;
-
方法执行失败(抛出异常):拦截器回滚事务;
-
-
底层依赖:事务的最终实现依赖数据库的事务(如 MySQL 的事务),Spring 只是对数据库事务进行了封装,简化开发。
补充(面试加分)
-
如果目标类实现了接口,用 JDK 动态代理;如果没有实现接口,用 CGLIB 动态代理;
-
事务管理器
PlatformTransactionManager是核心,不同的数据源对应不同的实现(如 JDBC 对应DataSourceTransactionManager,MyBatis 也用这个)。
2.@Transactional 注解的常用属性?
@Transactional 有多个属性,用于配置事务的隔离级别、传播行为、超时时间等,常用属性如下,结合项目场景说明:
-
value/transactionManager:指定事务管理器,多数据源场景下使用(如主从数据源,分别配置事务管理器); -
propagation:事务传播行为(最常用),定义多个事务之间的关系,常用值:
-
REQUIRED(默认):如果当前没有事务,就新建一个事务;如果当前有事务,就加入当前事务(适合大多数业务场景,如订单创建 + 库存扣减); -
REQUIRES_NEW:无论当前是否有事务,都新建一个事务,新事务和原事务独立(适合日志记录、消息发送,即使主事务回滚,日志也能提交); -
SUPPORTS:如果当前有事务,就加入;如果没有,就不使用事务(适合查询接口,可选事务); -
NESTED:嵌套事务,在当前事务内新建一个子事务,子事务回滚不影响主事务,主事务回滚会带动子事务回滚(适合复杂业务拆分);
-
-
isolation:事务隔离级别,对应 MySQL 四大隔离级别,默认使用数据库的隔离级别(MySQL 默认 RR);
-
Isolation.READ_COMMITTED:读已提交,避免脏读; -
Isolation.REPEATABLE_READ:可重复读(默认),避免脏读、不可重复读;
-
-
timeout:事务超时时间(单位:秒),默认 -1(不超时);如果事务执行时间超过设定值,自动回滚(防止长事务占用资源); -
readOnly:是否为只读事务,默认 false;如果是查询接口,设置为 true,优化性能(数据库会对只读事务做优化); -
rollbackFor:指定需要回滚的异常类型(如自定义业务异常),默认只回滚 RuntimeException 及其子类;
- 示例:
@Transactional(rollbackFor = BusinessException.class),只有抛出该异常时才回滚;
- 示例:
-
noRollbackFor:指定不需要回滚的异常类型,即使抛出该异常,事务也不回滚。
3.@Transactional 什么时候失效?
@Transactional 失效的核心原因是:没有生成代理类,或者事务拦截器没有拦截到方法执行,常见 6 种场景,结合原理说明,面试不慌:
-
方法不是 public 修饰:
-
原理:Spring 的事务拦截器
TransactionInterceptor只拦截 public 方法,非 public 方法(private、protected)不会被拦截,事务失效; -
解决方案:将方法改为 public 修饰。
-
-
内部 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 类中。
-
-
-
try-catch 吃掉异常,未抛出:
-
场景:方法中用 try-catch 捕获了异常,没有重新抛出,事务无法感知异常,不会回滚;
-
原理:Spring 事务回滚的前提是 "方法抛出未被捕获的异常",如果异常被吃掉,事务会认为方法执行成功,直接提交;
-
解决方案:
-
捕获异常后,重新抛出(
throw new RuntimeException(e);); -
手动回滚事务(
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();)。
-
-
-
多线程跨线程调用:
-
场景:在一个事务方法中,开启新线程,调用另一个事务方法;
-
原理:新线程中的方法,不在当前事务的上下文范围内,事务无法传递,新线程中的事务失效;
-
示例:
@Transactional public void createOrder() { // 开启新线程 new Thread(() -> { // 新线程中的方法,事务失效 stockService.deductStock(); }).start(); } -
解决方案:避免多线程跨线程调用事务方法;如果必须用,使用分布式事务(如 Seata)。
-
-
事务传播行为配置错误:
-
场景:配置了
propagation = Propagation.NOT_SUPPORTED(不支持事务)、propagation = Propagation.NEVER(禁止事务); -
原理:这类传播行为会导致方法不使用事务,即使加了
@Transactional,也会失效; -
解决方案:根据业务场景,配置正确的传播行为(常用 REQUIRED、REQUIRES_NEW)。
-
-
数据库引擎不支持事务:
-
场景:使用 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 个步骤,结合注解说明:
-
实例化(Instantiation):Spring 容器通过反射,创建 Bean 的实例(调用无参构造方法);
-
属性填充(Population) :Spring 容器将配置文件中、其他 Bean 中的属性,注入到当前 Bean 中(如
@Autowired注入依赖); -
BeanNameAware 接口回调 :如果 Bean 实现了
BeanNameAware接口,Spring 会调用setBeanName()方法,将 Bean 的名称注入; -
BeanFactoryAware 接口回调 :如果 Bean 实现了
BeanFactoryAware接口,Spring 会调用setBeanFactory()方法,将 BeanFactory 容器注入; -
初始化(Initialization):
-
调用
@PostConstruct注解标记的方法(Bean 初始化前执行); -
如果 Bean 实现了
InitializingBean接口,调用afterPropertiesSet()方法; -
调用 XML 配置中
init-method指定的方法(现在很少用);
-
-
Bean 就绪:Bean 初始化完成,存入 Spring 容器,供其他 Bean 调用;
-
销毁(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 种:
-
singleton(单例,默认):
-
定义:整个 Spring 容器中,只有一个 Bean 实例,容器启动时创建(非懒加载),所有请求共享该实例;
-
适用场景:Service、Dao、Controller(无状态,线程安全);
-
注意:如果单例 Bean 有可变成员变量,会存在线程安全问题,需加锁或用 ThreadLocal。
-
-
prototype(多例):
-
定义:每次获取 Bean 时(如
context.getBean()、@Autowired),都会创建一个新的实例; -
适用场景:有状态的 Bean(如 Request、Session 相关的 Bean);
-
注意:Spring 不管理多例 Bean 的销毁,由 JVM 垃圾回收。
-
-
request(请求域):
-
定义:每个 HTTP 请求,创建一个新的 Bean 实例,请求结束后,Bean 被销毁;
-
适用场景:Web 项目中,与当前请求相关的 Bean(如请求参数封装类);
-
注意:只适用于 Web 环境(内置 Tomcat 等容器)。
-
-
session(会话域):
-
定义:每个 HTTP Session,创建一个新的 Bean 实例,会话结束后,Bean 被销毁;
-
适用场景:Web 项目中,与用户会话相关的 Bean(如用户登录信息);
-
注意:只适用于 Web 环境。
-
-
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)解决方案(项目常用)
-
避免使用可变成员变量:尽量让单例 Bean 无状态(只写方法,不定义可变成员变量);
-
使用 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(); } } -
加锁同步:在修改成员变量的方法上,加 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; } } -
改为多例 Bean:如果 Bean 状态复杂,可将
@Scope改为prototype,每次获取新实例,避免线程共享。