11-Spring
Spring的核心特性是什么?
原始问法:
- Spring的核心特性是什么?
来源题目:
SRC-11-111-383
面试先答
Spring 的核心特性可以概括为 IOC(控制反转)、AOP(面向切面编程)、自动装配、事务管理和 MVC 框架。IOC 是最核心的设计思想,通过将对象的创建和依赖管理交给容器,实现了松耦合;AOP 通过动态代理将横切关注点(如日志、事务)从业务逻辑中分离;自动装配基于注解实现依赖的自动注入;事务管理提供声明式事务;Spring MVC 则是基于 MVC 模式的 Web 框架。这些特性共同构成了 Spring 生态的基石。
核心结论
- IOC 容器是 Spring 的核心,负责对象的生命周期和依赖管理。
- AOP 基于动态代理实现,将横切关注点与业务逻辑解耦。
- Spring 全家桶围绕核心容器扩展,涵盖 Web、数据访问、安全等领域。
1. 是什么
Spring 是一个开源的 Java 企业级开发框架,其核心特性包括:
- IOC(Inversion of Control,控制反转):将对象的创建和依赖注入交给容器管理,开发者不再手动
new对象。 - AOP(Aspect-Oriented Programming,面向切面编程):将日志、事务、权限等横切逻辑从业务代码中抽离,通过动态代理织入。
- 自动装配(Auto Wiring):基于
@Autowired、@Resource等注解自动完成依赖注入。 - 事务管理:提供声明式事务(
@Transactional),基于 AOP 实现事务的开启、提交、回滚。 - Spring MVC:基于 MVC 模式的 Web 框架,实现请求分发、视图渲染。
- 事件机制:基于观察者模式的事件驱动模型。
- 资源管理:统一管理数据库连接、文件句柄等资源的生命周期。
2. 为什么需要它
在没有 Spring 的时代,Java 企业级开发面临以下痛点:
- 紧耦合:对象之间通过
new直接依赖,修改一个类可能牵连大量改动。 - 重复代码:日志、事务等横切逻辑散布在每个方法中,代码臃肿难以维护。
- 资源管理复杂:数据库连接、Hibernate Session 等需要手动创建和释放。
- 测试困难:紧耦合导致单元测试难以 Mock 依赖。
Spring 通过 IOC 和 AOP 解决了这些核心矛盾,使开发者专注于业务逻辑。
3. 底层原理与完整流程
IOC 流程:
- 加载配置(XML 或注解),通过
BeanDefinitionReader解析为BeanDefinition。 BeanFactory收到获取 Bean 请求时,先从缓存(singletonObjects)中查找。- 缓存未命中则创建 Bean:实例化→属性填充→初始化→放入单例缓存。
- 依赖注入通过反射(
setter或构造器)完成。
AOP 流程:
- 容器启动时扫描切面类,解析切点表达式。
- 为符合切点的 Bean 创建代理对象(JDK 动态代理或 CGLIB)。
- 方法调用时,代理对象拦截调用,按顺序织入通知(Advice)。
4. 怎么使用
@Configuration
@ComponentScan("com.example")
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(...);
}
}
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
启动容器:
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
UserService service = ctx.getBean(UserService.class);
5. 适用场景
- 企业级 Java 应用的基础框架。
- 需要松耦合、可维护的大型项目。
- 需要声明式事务、AOP 横切关注点管理。
- 与 Spring Boot 结合快速构建微服务。
6. 不适用场景与替代方案
- 极小型脚本或工具类项目,Spring 启动开销不必要。
- 对启动性能极致敏感的场景可考虑使用 Picocli 等轻量框架。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 耦合性 | 松耦合,易测试 | 学习曲线陡峭 |
| 开发效率 | 自动装配提高效率 | 启动时扫描开销 |
| 灵活性 | 丰富的扩展点 | 版本兼容性问题(大版本升级) |
| 生态 | 完善的生态系统 | 过度设计风险 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| Bean 循环依赖 | 使用构造器注入或 @Lazy 延迟加载 |
| 启动缓慢 | 合理配置 @ComponentScan 范围,避免过度扫描 |
| 事务不生效 | 检查方法是否为 public,是否在同一类内部调用 |
| AOP 不生效 | 确认代理方式(JDK vs CGLIB),同类调用问题 |
9. 版本差异与实现边界
- Spring Framework 6.x:基于 Java 17+,全面支持 Spring Boot 3,移除了对 Java 8 的兼容。
- Spring Framework 5.x:兼容 Java 8+,广泛用于生产环境。
- 实现边界:Spring 不负责业务逻辑的编排调度,也不提供消息队列等中间件功能。
10. 常见追问
追问 1:BeanFactory 和 ApplicationContext 的区别? 答:BeanFactory 是底层接口,采用延迟初始化;ApplicationContext 是子接口,功能更完善(事件、国际化、自动装配),采用预初始化。
追问 2:Spring 中 Bean 的实例化方式有哪些? 答:构造器实例化(最常用)、静态工厂方法、实例工厂方法。
追问 3:Spring 如何解决循环依赖的? 答:通过三级缓存机制,提前暴露 Bean 的早期引用(通过
ObjectFactory)。
11. 易错点
❌ 错误:Spring 容器启动后所有 Bean 都会被实例化。
✅ 正确:默认单例 Bean 会预实例化,
lazy-init=true的 Bean 延迟到首次使用。❌ 错误:Spring 的 IOC 就是依赖注入。
✅ 正确:IOC 是思想,依赖注入(DI)是实现 IOC 的具体手段。
一句话总结
Spring 通过 IOC 容器管理对象生命周期和依赖,通过 AOP 分离横切关注点,以松耦合、高内聚的方式构建 Java 企业级应用。
什么是IOC?原理是什么?
原始问法:
- 什么是IOC?原理是什么?
来源题目:
SRC-11-111-384
面试先答
IOC(Inversion of Control,控制反转)是 Spring 的核心设计思想,指将对象的创建权和依赖管理权从代码中转移到容器。传统方式中开发者通过 new 主动创建对象,IOC 则将控制权反转给容器,容器根据配置自动创建和注入对象。其底层原理包括:通过反射机制动态创建 Bean 实例、通过 BeanDefinition 描述 Bean 的元信息、通过三级缓存解决循环依赖、通过 BeanPostProcessor 实现扩展点。IOC 的核心价值在于降低耦合、提高可测试性和可维护性。
核心结论
- IOC 是一种设计思想,DI(依赖注入)是其实现手段。
- BeanFactory 通过反射和配置元信息管理所有 Bean 的生命周期。
- 三级缓存机制解决了单例 Bean 的循环依赖问题。
1. 是什么
IOC 是一种对象创建和依赖管理的设计模式。核心思想是"你不要创建对象,我来创建并注入给你"。
控制反转的两层含义:
- 控制权反转:对象的创建和销毁不再由开发者代码控制,而是交给容器。
- 依赖注入(DI):容器将依赖的对象注入到需要它的类中,分为构造器注入、Setter 注入和字段注入。
IOC 容器的核心接口:
BeanFactory:Spring 最底层接口,提供 Bean 的获取和生命周期管理。ApplicationContext:BeanFactory 的增强实现,添加了事件机制、国际化、自动装配等功能。
2. 为什么需要它
| 没有 IOC 的问题 | IOC 解决的矛盾 |
|---|---|
| 紧耦合:A 类 new B 类,修改 B 需要改 A | 松耦合:A 只声明需要 B,容器负责注入 |
| 测试困难:无法独立测试 A | 易测试:可以注入 Mock 的 B |
| 生命周期管理复杂 | 容器统一管理生命周期 |
| 依赖关系不透明 | 依赖关系通过配置清晰可见 |
3. 底层原理与完整流程
核心组件:
BeanDefinition:描述 Bean 的元信息(类名、作用域、依赖关系、初始化方法等)。BeanDefinitionRegistry:注册所有BeanDefinition。BeanFactory:根据BeanDefinition创建和管理 Bean。ObjectFactory:用于提前暴露 Bean 的早期引用。
Bean 创建完整流程:
1. 定位 Bean 定义 → BeanDefinition
2. 实例化 Bean → 反射调用构造器
3. 属性填充 → 依赖注入(字段/Setter/构造器)
4. Aware 回调 → BeanNameAware、ApplicationContextAware 等
5. BeanPostProcessor 前置处理
6. 初始化 → @PostConstruct、InitializingBean、init-method
7. BeanPostProcessor 后置处理(AOP 代理生成)
8. 注册销毁回调 → DisposableBean、destroy-method
9. 放入单例缓存 → 可供后续获取
三级缓存解决循环依赖:
singletonObjects(一级):完整初始化的 Bean
earlySingletonObjects(二级):已创建但未初始化的 Bean
singletonFactories(三级):ObjectFactory,用于创建早期引用
当 A 依赖 B、B 依赖 A 时:
- 创建 A,实例化后放入三级缓存
- 发现 B,创建 B,B 从三级缓存获取 A 的早期引用
- B 初始化完成后放入一级缓存
- A 获取到 B 引用,完成初始化
4. 怎么使用
配置方式:
// 注解方式
@Configuration
public class IocConfig {
@Bean
public OrderService orderService() {
return new OrderService();
}
@Bean
public UserService userService() {
return new UserService(orderService()); // 构造器注入
}
}
// 注解自动装配
@Service
public class OrderService {
@Autowired // 字段注入(不推荐)
private UserRepository userRepository;
@Resource // 按名称注入
private OrderRepository orderRepository;
}
获取 Bean:
ApplicationContext ctx = new AnnotationConfigApplicationContext(IocConfig.class);
OrderService orderService = ctx.getBean(OrderService.class);
5. 适用场景
- 几乎所有 Spring 应用都使用 IOC。
- 需要低耦合、高可维护性的项目。
- 需要统一管理事务、AOP、事件等横切关注点。
6. 不适用场景与替代方案
- 非常简单的脚本/工具类项目(容器启动开销不必要)。
- 替代方案:Google Guice(轻量 DI)、Dagger(编译时 DI)。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 耦合 | 彻底解耦 | 间接调用增加调试难度 |
| 测试 | 易 Mock 测试 | 启动时需要扫描 |
| 配置 | 灵活的配置方式 | 配置复杂时不易排查问题 |
| 性能 | 运行时动态注入 | 反射比直接 new 稍慢(可忽略) |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| NoSuchBeanDefinitionException | 检查 Bean 是否被扫描到,@ComponentScan 范围是否覆盖 |
| 循环依赖无法解决 | 使用构造器注入避免,或使用 @Lazy |
| Bean 注入 null | 检查 @Autowired 字段是否在非托管类中(如 new 创建的类) |
9. 版本差异与实现边界
- Spring 4.x+ 推荐使用构造器注入,字段注入仅作为兼容手段。
- Spring 6.x 强制要求构造器注入,移除了对字段注入的推荐支持。
- IOC 容器不负责 Bean 内部的业务逻辑编排。
10. 常见追问
追问 1:BeanFactory 和 ApplicationContext 的区别? 答:BeanFactory 延迟初始化,ApplicationContext 预初始化并提供更多功能。
追问 2:Spring 如何自动装配? 答:通过
AutowiredAnnotationBeanPostProcessor处理@Autowired和@Value,通过反射注入依赖。追问 3:循环依赖的 Bean 一定能解决吗? 答:不一定。多例作用域的循环依赖无法解决,构造器注入的循环依赖也无法解决。
11. 易错点
❌ 错误:IOC 就是依赖注入。
✅ 正确:IOC 是控制反转的设计思想,DI 是实现该思想的具体方式。
❌ 错误:Spring 的 Bean 默认是多例的。
✅ 正确:默认是单例(
singleton),可通过@Scope("prototype")修改。
一句话总结
IOC 将对象创建和依赖管理的控制权交给容器,通过反射和配置实现松耦合、高内聚的对象协作。
什么是AOP?原理是什么?
原始问法:
- 什么是AOP?原理是什么?
来源题目:
SRC-11-111-385
面试先答
AOP(Aspect-Oriented Programming,面向切面编程)是一种将横切关注点(如日志、事务、权限)从业务逻辑中分离出来的编程范式。Spring AOP 基于动态代理实现,支持 JDK 动态代理(面向接口)和 CGLIB(面向类)两种代理方式。核心组件包括:Aspect(切面)、Pointcut(切点)、Advice(通知)、JoinPoint(连接点)。底层通过在 Bean 创建阶段生成代理对象,在方法调用时织入增强逻辑。AOP 使业务代码更纯净,横切逻辑可复用、可组合。
核心结论
- AOP 基于动态代理(JDK/CGLIB)实现方法拦截和增强。
- 核心概念:Aspect、Pointcut、Advice、JoinPoint、Advisor。
- Spring AOP 仅支持方法级别的切面,不支持字段和构造器级别。
1. 是什么
AOP 是面向对象编程的补充,它将散布在各处的横切逻辑(日志、事务、异常处理)抽离出来形成独立的切面,通过织入(Weaving)方式应用到目标对象。
核心概念:
| 概念 | 说明 | 示例 |
|---|---|---|
| Aspect(切面) | 横切关注点的模块化 | @Aspect 标注的日志切面类 |
| JoinPoint(连接点) | 程序执行中的某个点 | 方法调用、异常抛出 |
| Pointcut(切点) | 连接点的表达式 | execution(* com.example.service.*.*(..)) |
| Advice(通知) | 在切点执行的动作 | @Before、@After、@Around |
| Target(目标) | 被增强的原始对象 | UserService 实例 |
| Proxy(代理) | AOP 创建的增强对象 | 代理后的 UserService |
| Weaving(织入) | 将切面应用到目标对象的过程 | 运行时创建代理 |
2. 为什么需要它
没有 AOP 的问题:
- 代码混乱:业务代码中充斥着日志、事务等非业务逻辑。
- 难以维护:修改日志格式需要改动每个方法。
- 无法复用:横切逻辑无法像业务方法一样复用。
- 容易遗漏:在每个方法中手动添加事务注解容易出错。
3. 底层原理与完整流程
代理方式选择:
- JDK 动态代理:目标类实现接口时默认使用。基于
java.lang.reflect.Proxy,生成实现相同接口的代理类。 - CGLIB 代理:目标类未实现接口时使用。基于字节码操作(ASM),生成目标类的子类。
代理创建流程:
1. Bean 初始化完成后,AbstractAutowireCapableBeanFactory#initializeBean
2. 调用 wrapIfNecessary 方法
3. 从容器中查找所有 Advisor(切面 + 通知)
4. 判断 Bean 是否匹配切点
5. 匹配则通过 ProxyFactory 创建代理对象
6. 代理对象替换原始 Bean 放入容器
方法调用流程:
代理对象.method()
→ 拦截器链(MethodInterceptor)
→ 前置通知(@Before)
→ 目标方法执行
→ 返回通知(@AfterReturning)
→ 后置通知(@After)
→ 异常通知(@AfterThrowing)
通知类型:
| 通知 | 说明 | 触发时机 |
|---|---|---|
| @Before | 前置通知 | 目标方法执行前 |
| @AfterReturning | 返回通知 | 目标方法成功返回后 |
| @AfterThrowing | 异常通知 | 目标方法抛出异常时 |
| @After | 后置通知 | 目标方法执行后(无论成功/异常) |
| @Around | 环绕通知 | 包裹目标方法,最强大 |
4. 怎么使用
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Before("serviceLayer()")
public void beforeLog(JoinPoint jp) {
System.out.println("调用方法: " + jp.getSignature().getName());
}
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturningLog(JoinPoint jp, Object result) {
System.out.println("方法返回: " + result);
}
@Around("serviceLayer()")
public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long elapsed = System.currentTimeMillis() - start;
System.out.println("耗时: " + elapsed + "ms");
return result;
}
}
基于 API 的编程式切面:
ProxyFactory pf = new ProxyFactory(target);
pf.addAdvice(new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("前置");
Object result = invocation.proceed();
System.out.println("后置");
return result;
}
});
Object proxy = pf.getProxy();
5. 适用场景
- 日志记录:方法调用的日志追踪。
- 事务管理:
@Transactional的底层实现就是 AOP。 - 权限校验:方法级别的权限检查。
- 性能监控:方法执行耗时统计。
- 异常处理:统一的异常捕获和处理。
- 缓存:方法级别的缓存注解。
6. 不适用场景与替代方案
- 简单的横切逻辑(直接写在代码中更直观)。
- 对性能极致敏感的场景(反射代理有开销)。
- 替代方案:AspectJ(编译时/加载时织入,功能更强)。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 代码 | 业务代码纯净 | 切面逻辑分散在单独的类中 |
| 复用 | 横切逻辑可复用 | 调试困难(调用链复杂) |
| 耦合 | 解耦横切与业务 | 同类内部调用切面失效 |
| 性能 | 运行时织入灵活 | 反射代理比直接调用慢(约 10%-20%) |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 同类方法调用切面不生效 | 使用 self-injection 或 ApplicationContext.getBean() 获取代理对象 |
| 切面不执行 | 检查 @Component 和 @Aspect 注解,切点表达式是否正确 |
| 通知顺序不可控 | 使用 @Order 接口或实现 Ordered 接口排序 |
| 最终方法无法被 CGLIB 代理 | 避免将方法声明为 final,或使用 JDK 代理(实现接口) |
9. 版本差异与实现边界
- Spring 5.x 开始支持
@Order注解对切面排序。 - Spring AOP 仅支持方法级别的切点,AspectJ 支持字段、构造器等。
- Spring 6.x 中默认使用 CGLIB 代理(即使实现了接口)。
10. 常见追问
追问 1:JDK 动态代理和 CGLIB 的区别? 答:JDK 代理基于接口,CGLIB 基于类生成子类;JDK 代理需要实现 InvocationHandler,CGLIB 实现 MethodInterceptor。
追问 2:Spring AOP 和 AspectJ 的区别? 答:Spring AOP 基于运行时代理,AspectJ 支持编译时和加载时织入;Spring AOP 仅支持方法级切面,AspectJ 支持更丰富的切点。
追问 3:如何实现一个自定义的 AOP 切面? 答:实现
MethodInterceptor接口,在invoke方法中实现横切逻辑,通过ProxyFactory创建代理。
11. 易错点
❌ 错误:Spring AOP 可以拦截字段访问。
✅ 正确:Spring AOP 仅支持方法级别的连接点。
❌ 错误:
@Autowired的字段即使在同类内部调用也能触发 AOP。✅ 正确:同类内部调用走的是原始对象的引用,不走代理对象。
一句话总结
AOP 通过动态代理将横切逻辑从业务代码中分离,使关注点分离成为可能,Spring 基于 JDK/CGLIB 代理实现运行时织入。
Spring Bean的生命周期是什么?
原始问法:
- Spring Bean的生命周期是什么?
来源题目:
SRC-11-111-386
面试先答
Spring Bean 的生命周期分为四个主要阶段:实例化→属性填充→初始化→销毁。详细过程包括:1. 实例化 Bean(通过反射调用构造器);2. 属性填充(依赖注入);3. Aware 回调(BeanNameAware、BeanFactoryAware 等);4. BeanPostProcessor 前置处理;5. 初始化(@PostConstruct、InitializingBean、自定义 init-method);6. BeanPostProcessor 后置处理(AOP 代理生成);7. 使用 Bean;8. 销毁(@PreDestroy、DisposableBean、自定义 destroy-method)。理解生命周期有助于解决循环依赖、初始化顺序等问题。
核心结论
- Bean 生命周期分为:实例化→属性填充→初始化→销毁四个阶段。
- 扩展点包括
Aware接口、BeanPostProcessor、InitializingBean、DisposableBean。 - AOP 代理在
BeanPostProcessor后置处理阶段生成。
1. 是什么
Bean 生命周期是指 Spring 容器从创建一个 Bean 到销毁它的完整过程。Spring 提供了丰富的扩展点,允许开发者在生命周期的各个阶段介入。
2. 为什么需要它
- 理解生命周期有助于正确初始化资源(如数据库连接池)。
- 通过生命周期钩子可以执行自定义逻辑(如缓存预热、配置加载)。
- 正确管理 Bean 的销毁过程防止资源泄漏。
- 排查 Bean 创建失败等问题需要深入理解生命周期。
3. 底层原理与完整流程
完整生命周期流程:
1. 容器启动,加载 BeanDefinition
↓
2. 实例化 Bean(反射调用构造器,支持构造器参数解析)
↓
3. 属性填充(依赖注入:@Autowired、Setter、构造器)
↓
4. Aware 回调
├─ BeanNameAware#setBeanName
├─ BeanFactoryAware#setBeanFactory
├─ BeanClassLoaderAware#setBeanClassLoader
└─ ApplicationContextAware#setApplicationContext
↓
5. BeanPostProcessor 前置处理
└─ postProcessBeforeInitialization
↓
6. 初始化
├─ @PostConstruct 注解方法
├─ InitializingBean#afterPropertiesSet
└─ init-method(XML 配置或 @Bean(initMethod))
↓
7. BeanPostProcessor 后置处理
└─ postProcessAfterInitialization(AOP 代理在此生成)
↓
8. Bean 就绪,可被应用使用
↓
9. 容器关闭时销毁
├─ @PreDestroy 注解方法
├─ DisposableBean#destroy
└─ destroy-method(XML 配置或 @Bean(destroyMethod))
关键代码路径:
// AbstractAutowireCapableBeanFactory#createBean
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 1. 实例化
Object bean = doCreateBean(beanName, mbd, args);
// 2. 属性填充
populateBean(beanName, mbd, instanceWrapper);
// 3. 初始化
Object exposedObject = initializeBean(beanName, exposedObject, mbd);
// 4. 注册销毁回调
registerDisposableBeanIfNecessary(beanName, bean, mbd);
return exposedObject;
}
4. 怎么使用
使用生命周期扩展点:
@Component
public class MyBean implements BeanNameAware, InitializingBean, DisposableBean {
private String beanName;
@Override
public void setBeanName(String name) {
this.beanName = name;
System.out.println("Bean 名称: " + name);
}
@PostConstruct
public void postConstruct() {
System.out.println("@PostConstruct: Bean 创建后");
}
@Override
public void afterPropertiesSet() {
System.out.println("InitializingBean: 属性设置完成");
}
@Override
public void destroy() {
System.out.println("DisposableBean: Bean 销毁");
}
@PreDestroy
public void preDestroy() {
System.out.println("@PreDestroy: Bean 销毁前");
}
}
使用自定义初始化/销毁方法:
@Configuration
public class BeanConfig {
@Bean(initMethod = "init", destroyMethod = "cleanup")
public MyService myService() {
return new MyService();
}
}
public class MyService {
public void init() { /* 初始化逻辑 */ }
public void cleanup() { /* 清理逻辑 */ }
}
5. 适用场景
- 需要在 Bean 创建后执行初始化操作(缓存预热、配置加载)。
- 需要在 Bean 销毁前释放资源(关闭数据库连接、停止线程池)。
- 需要自定义 Bean 的创建逻辑(通过
BeanPostProcessor)。
6. 不适用场景与替代方案
- 简单的 POJO 不需要生命周期管理。
- 避免过度使用 Aware 接口,它们会耦合 Spring API。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 扩展性 | 丰富的扩展点 | 生命周期阶段多,学习成本高 |
| 规范性 | 统一的生命周期管理 | 部分扩展点耦合 Spring API |
| 灵活性 | 多种扩展方式可选 | 执行顺序需要注意 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
@PostConstruct 不执行 |
确认 Bean 被 Spring 管理(加了 @Component 等注解) |
| 初始化方法执行两次 | 检查是否配置了多种初始化方式(@PostConstruct + InitializingBean + init-method) |
| 循环依赖导致创建失败 | 使用构造器注入或 @Lazy 解决 |
9. 版本差异与实现边界
- Spring 2.5+ 引入
@PostConstruct支持。 - Spring 4.x+ 推荐使用
@PostConstruct替代InitializingBean。 - 原型 Bean 的销毁回调需要通过
registerDestructionCallback手动注册。
10. 常见追问
追问 1:
@PostConstruct和InitializingBean的执行顺序? 答:@PostConstruct→InitializingBean#afterPropertiesSet→init-method。追问 2:BeanPostProcessor 的作用是什么? 答:在 Bean 初始化前后执行自定义逻辑,AOP 代理就是通过它生成的。
追问 3:如何在 Bean 销毁时执行自定义逻辑? 答:使用
@PreDestroy、实现DisposableBean接口或配置destroy-method。
11. 易错点
❌ 错误:
@PostConstruct在构造器执行前调用。✅ 正确:构造器执行→属性填充→
@PostConstruct执行。❌ 错误:原型 Bean 销毁时会自动调用
@PreDestroy。✅ 正确:原型 Bean 销毁回调需手动注册。
一句话总结
Spring Bean 生命周期历经实例化、属性填充、初始化、销毁四个阶段,通过丰富的扩展点让开发者可以在任意阶段介入 Bean 的管理。
Spring Bean的作用域有哪些?默认是哪种?
原始问法:
- Spring Bean的作用域有哪些?默认是哪种?
来源题目:
SRC-11-111-387
面试先答
Spring Bean 的作用域定义了 Bean 实例的创建和使用范围,共有六种:singleton(单例,默认)、prototype(原型)、request(HTTP 请求)、session(HTTP 会话)、application(Servlet 上下文)、websocket(WebSocket 会话)。默认是 singleton,即容器中只有一个实例。不同作用域适用于不同场景:singleton 适用于无状态服务,prototype 适用于有状态对象,request/session 适用于 Web 应用。Spring 2.5+ 还引入了 @Scope 注解用于声明作用域。
核心结论
- Spring 有 6 种作用域:singleton、prototype、request、session、application、websocket。
- 默认作用域是 singleton,容器启动时创建唯一实例。
- prototype 作用域下,每次获取 Bean 都创建新实例,且容器不负责其销毁。
1. 是什么
作用域决定了 Bean 实例的生命周期和数量。Spring 支持以下作用域:
| 作用域 | 说明 | 创建时机 | 销毁时机 |
|---|---|---|---|
| singleton | 单例(默认) | 容器启动时 | 容器关闭时 |
| prototype | 原型 | 每次 getBean |
容器不管理 |
| request | HTTP 请求 | 每次 HTTP 请求 | 请求结束 |
| session | HTTP 会话 | 会话开始 | 会话结束 |
| application | Servlet 上下文 | 应用启动 | 应用关闭 |
| websocket | WebSocket 会话 | 会话连接 | 会话断开 |
2. 为什么需要它
- singleton:避免重复创建开销,适用于无状态的服务类。
- prototype:适用于需要独立实例的有状态对象。
- request/session:Web 应用中需要与请求/会话绑定的状态。
- application:全局共享的配置或缓存数据。
3. 底层原理与完整流程
singleton 实现:
// DefaultSingletonBeanRegistry
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
public Object getSingleton(String beanName) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
// 从三级缓存获取
}
}
return singletonObject;
}
prototype 实现:
// AbstractBeanFactory#doGetBean
if (isPrototype(beanName)) {
// 每次创建新实例,不缓存
Object bean = createBean(beanName, mbd, args);
// 不注册到单例缓存
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
Web 作用域实现:
Spring 通过 Scope 接口实现 Web 作用域,RequestScope、SessionScope 等实现了该接口,将 Bean 存储在 HttpServletRequest、HttpSession 中。
4. 怎么使用
// 默认 singleton,无需额外配置
@Service
public class UserService { ... }
// prototype 作用域
@Service
@Scope("prototype")
public class ShoppingCart { ... }
// request 作用域(Web 环境)
@Component
@Scope("request")
public class RequestData { ... }
// session 作用域
@Component
@Scope("session")
public class UserSession { ... }
// 自定义作用域
@Scope("myScope")
@Component
public class CustomScopedBean { ... }
XML 配置:
<bean id="userService" class="com.example.UserService" scope="prototype"/>
5. 适用场景
- singleton:Service、Repository、Controller 等无状态服务。
- prototype:需要独立状态的对象,如
@Transactional的新事务。 - request:请求级别的数据传递。
- session:用户会话级别的状态管理。
6. 不适用场景与替代方案
- 有状态的 Service 类不应使用 singleton,会导致线程安全问题。
- 原型 Bean 需要手动销毁,容器不负责清理。
7. 优缺点与技术取舍
| 作用域 | 优点 | 缺点 |
|---|---|---|
| singleton | 节省内存、启动快 | 线程安全问题、有状态数据污染 |
| prototype | 线程安全、独立实例 | 创建开销大、容器不管理生命周期 |
| request/session | Web 状态自然管理 | 仅限 Web 环境 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| singleton 中注入 prototype Bean | 使用 @Lookup 注解或 ObjectProvider 延迟获取 |
| request 作用域在非 Web 环境报错 | 确保 Spring MVC 或 Web 环境已正确配置 |
@Lookup 解决 singleton 注入 prototype:
@Service
public class SingletonService {
@Lookup
public PrototypeBean getPrototypeBean() {
// Spring 会覆盖此方法,返回新的 prototype 实例
return null;
}
}
9. 版本差异与实现边界
- Spring 2.5+ 引入
@Scope注解。 - Spring 3.0+ 支持
@Lookup方法查找原型 Bean。 - Spring 6.x 新增
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)支持 CGLIB 代理的作用域。
10. 常见追问
追问 1:singleton 和 prototype 的选择标准? 答:无状态 Bean 用 singleton,有状态 Bean 用 prototype;绝大多数 Service 用 singleton。
追问 2:如何在 singleton 中使用 prototype? 答:使用
@Lookup注解、ObjectProvider或ApplicationContext.getBean()。
11. 易错点
❌ 错误:prototype Bean 销毁时容器会自动调用
@PreDestroy。✅ 正确:prototype Bean 的销毁回调需手动注册,容器不负责其生命周期。
❌ 错误:singleton Bean 一定线程安全。
✅ 正确:singleton Bean 是否线程安全取决于其内部状态,无状态 Bean 是线程安全的。
一句话总结
Spring Bean 作用域从 singleton 到 prototype 再到 Web 相关的 request/session/application/websocket,默认单例,选择依据是 Bean 的状态特性和使用场景。
Spring Bean是线程安全的吗?如何保证线程安全?
原始问法:
- Spring Bean是线程安全的吗?如何保证线程安全?
来源题目:
SRC-11-111-388
面试先答
Spring Bean 的线程安全性取决于其作用域和内部状态。默认的 singleton Bean 不是线程安全的,因为多个线程会共享同一个实例。如果 singleton Bean 内部没有可变状态(无状态),则天然线程安全;如果包含可变状态,就需要同步措施。prototype Bean 每次获取都创建新实例,线程间互不影响。常见的线程安全方案包括:使用无状态设计、对共享状态加锁(synchronized/Lock)、使用 ThreadLocal、使用不可变对象、通过 @Scope("prototype") 改变作用域。
核心结论
- singleton Bean 默认不是线程安全的(因其内部状态可能被多线程共享)。
- 无状态的 singleton Bean 天然线程安全。
- prototype Bean 因每次获取都创建新实例,线程间互不影响。
1. 是什么
Spring Bean 的线程安全性问题是指:当多个线程同时访问同一个 Bean 实例时,是否会出现数据竞争或状态不一致。
关键因素:
- 作用域:singleton(共享实例)vs prototype(独立实例)。
- 内部状态:有状态(存在可变成员变量)vs 无状态(无可变成员变量)。
2. 为什么需要它
- 网站的 Controller、Service 被大量并发请求访问。
- 如果 Bean 存在可变状态,可能导致数据错乱、重复提交等问题。
- 理解 Spring Bean 的线程安全模型是构建高并发应用的基础。
3. 底层原理与完整流程
singleton Bean 线程安全分析:
@Service
public class OrderService {
private int counter = 0; // 危险:可变状态
public void createOrder() {
counter++; // 非原子操作,多线程下会丢失更新
}
}
线程安全的 singleton Bean:
@Service
public class OrderService {
// 无可变状态,所有方法都是计算逻辑
public void createOrder(Order order) {
// 方法内部使用局部变量,天然线程安全
}
}
prototype Bean 天然线程安全:
@Service
@Scope("prototype")
public class OrderService {
private int counter = 0; // 安全:每个线程获取独立实例
}
4. 怎么使用
方案一:无状态设计(推荐)
@Service
public class UserService {
// 不使用成员变量存储请求状态
public String getUserName(Long userId) {
User user = userRepository.findById(userId);
return user.getName();
}
}
方案二:synchronized 同步锁
@Service
public class CounterService {
private int counter = 0;
public synchronized void increment() {
counter++;
}
}
方案三:ThreadLocal
@Service
public class UserContext {
private final ThreadLocal<User> currentUser = new ThreadLocal<>();
public void setCurrentUser(User user) {
currentUser.set(user);
}
public User getCurrentUser() {
return currentUser.get();
}
}
方案四:AtomicInteger 原子类
@Service
public class CounterService {
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet();
}
}
方案五:使用 prototype 作用域
@Service
@Scope("prototype")
public class OrderService {
private Order currentOrder; // 每个线程独立
}
5. 适用场景
- 无状态 singleton:绝大多数 Service、Repository、Controller。
- ThreadLocal:保存线程上下文(如用户信息)。
- 原子类:计数器、序列号等简单共享状态。
- prototype:需要在 Bean 中保存请求状态的场景。
6. 不适用场景与替代方案
- 避免在 singleton Bean 中使用成员变量存储可变状态。
synchronized会降低并发性能,不适合高并发场景。- prototype 会增加对象创建开销。
7. 优缺点与技术取舍
| 方案 | 优点 | 缺点 |
|---|---|---|
| 无状态设计 | 简单高效 | 不适合需要保存状态的场景 |
| synchronized | 简单可靠 | 性能下降、可能死锁 |
| ThreadLocal | 线程隔离 | 内存泄漏风险(需及时 remove) |
| prototype | 天然隔离 | 对象创建开销大 |
| 原子类 | 无锁高效 | 仅适用于单一变量操作 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| ThreadLocal 内存泄漏 | 使用后调用 remove() 方法 |
| singleton 中需要保存用户状态 | 使用 ThreadLocal 或改变为 request/session 作用域 |
| 多个变量需要一致性 | 使用 synchronized 或 ReentrantLock |
9. 版本差异与实现边界
- Spring 5.x+ 推荐使用不可变对象或局部变量避免线程安全问题。
- Spring WebFlux 中推荐使用无状态设计和响应式类型。
10. 常见追问
追问 1:Controller 中可以使用成员变量吗? 答:Controller 是 singleton,不能使用成员变量存储请求状态,否则会导致线程安全问题。
追问 2:Spring 如何保证事务的线程安全? 答:通过
ThreadLocal绑定数据库连接和事务状态,保证同一线程内的事务一致性。
11. 易错点
❌ 错误:Spring singleton Bean 默认线程安全。
✅ 正确:singleton Bean 是否线程安全取决于其内部状态,无状态 Bean 才是线程安全的。
❌ 错误:prototype Bean 会被 Spring 自动回收。
✅ 正确:Spring 不负责 prototype Bean 的生命周期管理,需手动回收。
一句话总结
Spring singleton Bean 的线程安全取决于其内部状态,应通过无状态设计、同步机制或 ThreadLocal 等方式保证并发安全。
@Autowired和@Resource的区别是什么?
原始问法:
- @Autowired和@Resource的区别是什么?
来源题目:
SRC-11-111-389
面试先答
@Autowired 和 @Resource 都是 Spring 中用于依赖注入的注解,核心区别在于:@Autowired 是 Spring 提供的注解,默认按类型注入;@Resource 是 JSR-250 标准注解(javax.annotation.Resource),默认按名称注入。@Autowired 支持 @Qualifier 指定名称,@Resource 支持 name 和 type 属性指定。@Autowired 支持构造器注入、Setter 注入和字段注入,@Resource 主要用于字段和 Setter 注入。Spring 推荐使用 @Autowired 配合 @Qualifier,因为它功能更强大、与 Spring 生态集成更好。
核心结论
@Autowired按类型注入(Spring 注解),@Resource按名称注入(JSR-250 标准)。@Autowired可配合@Qualifier实现按名称注入,@Resource可通过type属性按类型注入。- Spring 推荐
@Autowired,Jakarta EE 推荐@Resource。
1. 是什么
@Autowired:
- Spring Framework 提供的注解。
- 默认按类型(
byType)注入。 - 支持字段注入、Setter 注入、构造器注入。
- 可配合
@Qualifier实现按名称注入。 - 支持
required属性,默认为true(找不到则报错)。
@Resource:
- JSR-250 标准注解(
javax.annotation.Resource)。 - 默认按名称(
byName)注入。 - 支持字段注入和 Setter 注入(不支持构造器注入)。
- 可通过
name属性指定名称,type属性指定类型。 - 不符合 Spring 的构造器注入推荐趋势。
2. 为什么需要它
@Autowired是 Spring 原生注解,与 Spring 的 IOC 容器深度集成,支持更灵活的注入方式。@Resource是 Java 标准注解,便于在其他 DI 框架(如 Google Guice)中迁移。- 在存在多个同类型 Bean 时,需要额外信息区分注入哪个。
3. 底层原理与完整流程
@Autowired 处理流程:
1. AutowiredAnnotationBeanPostProcessor 扫描 @Autowired 注解
2. 调用 BeanFactory#resolveDependency 解析依赖
3. 按类型查找候选 Bean
4. 如果只有一个匹配 → 直接注入
5. 如果有多个匹配 → 查找 @Qualifier 或字段名匹配
6. 如果没有匹配且 required=true → 抛出 NoSuchBeanDefinitionException
@Resource 处理流程:
1. CommonAnnotationBeanPostProcessor 扫描 @Resource 注解
2. 解析 name 属性(默认使用字段名/Setter 方法名)
3. 按名称查找 Bean
4. 如果按名称未找到 → 回退到按类型查找
5. 如果指定了 type 属性 → 按类型查找
4. 怎么使用
// @Autowired 默认按类型注入
@Service
public class UserService {
// 字段注入
@Autowired
private UserRepository userRepository;
// 配合 @Qualifier 按名称注入
@Autowired
@Qualifier("userRepositoryImpl")
private UserRepository userRepository;
// 构造器注入(推荐)
public UserService(@Qualifier("userRepositoryImpl") UserRepository userRepository) {
this.userRepository = userRepository;
}
// required = false 允许为空
@Autowired(required = false)
private AuditService auditService;
}
// @Resource 默认按名称注入
@Service
public class OrderService {
// 按名称注入(默认使用字段名 orderRepository)
@Resource
private OrderRepository orderRepository;
// 指定名称
@Resource(name = "orderRepositoryImpl")
private OrderRepository orderRepository;
// 按类型注入
@Resource(type = OrderRepository.class)
private OrderRepository orderRepository;
}
5. 适用场景
- @Autowired:Spring 项目的首选,功能强大,支持构造器注入。
- @Resource:需要遵循 Java EE/Jakarta EE 标准或跨框架迁移的场景。
6. 不适用场景与替代方案
- 避免字段注入,推荐构造器注入(利于测试和不可变性)。
- 避免在 Bean 初始化前使用注入的依赖。
7. 优缺点与技术取舍
| 维度 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 注解 | JSR-250 标准 |
| 默认注入方式 | 按类型(byType) | 按名称(byName) |
| 构造器注入 | 支持 | 不支持 |
| 多 Bean 处理 | @Qualifier | name 属性 |
| required 属性 | 支持 | 不支持 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 存在多个同类型 Bean | 使用 @Qualifier 指定或通过 @Primary 标注首选 |
| 注入 null | 设置 required = false 或检查 Bean 是否存在 |
| 注入 Bean 循环依赖 | 使用构造器注入或 @Lazy 延迟加载 |
9. 版本差异与实现边界
- Spring 4.3+ 支持单构造器的
@Autowired可省略。 - Spring 6.x 推荐构造器注入,字段注入仅作为兼容手段。
@Resource在 Jakarta EE 9 中迁移到jakarta.annotation.Resource。
10. 常见追问
追问 1:
@Primary注解的作用? 答:当存在多个同类型 Bean 时,标注@Primary的 Bean 会被优先注入。追问 2:如何注入所有同类型的 Bean? 答:使用
@Autowired注入List<BeanType>或Map<String, BeanType>。
11. 易错点
❌ 错误:
@Resource默认按类型注入。✅ 正确:
@Resource默认按名称注入,@Autowired默认按类型注入。❌ 错误:
@Autowired不支持构造器注入。✅ 正确:
@Autowired完全支持构造器注入,且是 Spring 推荐的注入方式。
一句话总结
@Autowired 按类型注入、功能强大,@Resource 按名称注入、符合标准,两者都是依赖注入的有效手段,Spring 推荐使用构造器注入的 @Autowired。
Spring如何解决循环依赖?
原始问法:
- Spring如何解决循环依赖?
来源题目:
SRC-11-111-390
面试先答
Spring 通过三级缓存机制解决单例 Bean 的循环依赖问题。循环依赖是指 A 依赖 B,B 又依赖 A 的场景。三级缓存包括:一级 singletonObjects(完整初始化的 Bean)、二级 earlySingletonObjects(已创建但未初始化的 Bean)、三级 singletonFactories(创建早期引用的 ObjectFactory)。当创建 A 需要 B 时,A 先将自己的 ObjectFactory 放入三级缓存;创建 B 需要 A 时,从三级缓存获取 A 的早期引用(通过 ObjectFactory.getObject() 获取未初始化的 A 实例);B 完成后 A 再完成初始化。注意:构造器注入的循环依赖无法解决,多例 Bean 的循环依赖也无法解决。
核心结论
- 三级缓存是 Spring 解决单例 Bean 循环依赖的核心机制。
- 构造器注入和多例 Bean 的循环依赖无法通过三级缓存解决。
- 避免循环依赖的最佳实践是使用构造器注入或重新设计模块结构。
1. 是什么
循环依赖是指两个或多个 Bean 之间相互依赖,形成闭环。例如 A → B → A。
循环依赖的三种情况:
| 类型 | 是否可解决 | 说明 |
|---|---|---|
| 单例 → 单例(Setter 注入) | ✅ 可解决 | 通过三级缓存 |
| 单例 → 原型 | ❌ 不可解决 | 每次获取原型都创建新实例 |
| 构造器注入 | ❌ 不可解决 | 实例化阶段就需要依赖,无法提前暴露 |
2. 为什么需要它
- 没有循环依赖解决机制,Spring 容器在启动时会因
BeanCurrentlyInCreationException而失败。 - 合理的模块设计应避免循环依赖,但实际业务中难免出现。
- Spring 的三级缓存机制提供了优雅的自动解决方案。
3. 底层原理与完整流程
三级缓存结构:
// DefaultSingletonBeanRegistry
// 一级缓存:完整初始化的单例 Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:早期引用(已创建但未初始化的 Bean)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:ObjectFactory,用于创建早期引用
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
循环依赖解决流程(A → B → A):
1. 创建 A:
a. 实例化 A(调用构造器,得到 A 的实例)
b. 将 A 的 ObjectFactory 放入三级缓存
c. 开始属性填充 → 发现需要 B
2. 创建 B:
a. 实例化 B
b. 将 B 的 ObjectFactory 放入三级缓存
c. 开始属性填充 → 发现需要 A
d. 从三级缓存查找 A → 找到 ObjectFactory
e. 调用 ObjectFactory.getObject() → 返回 A 的早期引用
f. B 属性填充完成
g. B 初始化完成
h. 将 B 从三级缓存移至一级缓存
3. 回到 A:
a. 获取 B 的引用(已完成)
b. A 属性填充完成
c. A 初始化完成
d. 将 A 从三级缓存移至一级缓存
关键代码:
// AbstractAutowireCapableBeanFactory#doCreateBean
// 提前暴露 ObjectFactory 到三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// DefaultSingletonBeanRegistry#getSingleton
public Object getSingleton(String beanName) {
// 先从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 再从二级缓存获取
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 最后从三级缓存获取
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 升级:从三级移到二级
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
return singletonObject;
}
4. 怎么使用
// Spring 自动处理循环依赖,无需额外配置
@Service
public class ServiceA {
private final ServiceB serviceB;
@Autowired // Setter 注入,支持循环依赖解决
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
@Autowired
public void setServiceA(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
不支持的情况:
// 构造器注入导致的循环依赖无法解决
@Service
public class ServiceA {
private final ServiceB serviceB;
@Autowired // 构造器注入 → 无法解决循环依赖
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
5. 适用场景
- 单例 Bean 之间的 Setter/字段注入循环依赖。
- 合理设计的模块间偶尔出现的循环引用。
6. 不适用场景与替代方案
- 构造器注入的循环依赖(应重构为 Setter 注入或消除循环)。
- 多例 Bean 的循环依赖(改变为单例或重新设计)。
- 最佳实践:通过
@Lazy延迟加载或重新拆分模块来消除循环依赖。
使用 @Lazy 解决:
@Service
public class ServiceA {
@Lazy // 延迟加载,打破循环
@Autowired
private ServiceB serviceB;
}
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 自动化 | 透明解决循环依赖 | 可能掩盖设计问题 |
| 性能 | 三级缓存查找高效 | 构造器注入不支持 |
| 设计 | 允许合理的循环引用 | 应尽量避免循环依赖 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
BeanCurrentlyInCreationException |
检查是否为构造器注入的循环依赖,改用 Setter 注入或 @Lazy |
| 多例 Bean 循环依赖 | 改为单例或重新设计依赖关系 |
| AOP 代理的循环依赖 | 使用三级缓存提前暴露代理引用 |
9. 版本差异与实现边界
- Spring 4.3+ 对循环依赖的检测更加严格,建议消除循环依赖。
- Spring 6.x 仍支持三级缓存机制,但推荐使用构造器注入避免循环。
10. 常见追问
追问 1:为什么构造器注入不能解决循环依赖? 答:构造器注入在实例化阶段就需要依赖,此时 Bean 尚未创建,无法提前暴露引用。
追问 2:三级缓存的升级过程? 答:首次从三级缓存获取时,将
ObjectFactory的结果放入二级缓存,提升后续查找效率。追问 3:如何检测循环依赖? 答:Spring 通过
singletonCurrentlyInCreation集合记录正在创建的 Bean,检测到重复即判定为循环依赖。
11. 易错点
❌ 错误:Spring 能解决所有循环依赖。
✅ 正确:仅单例 Bean 的 Setter/字段注入循环依赖可解决。
❌ 错误:三级缓存中的 Bean 是完整的。
✅ 正确:三级缓存存放的是
ObjectFactory,调用getObject()得到的是未初始化的早期引用。
一句话总结
Spring 通过三级缓存机制(完整 Bean → 早期引用 → ObjectFactory)优雅地解决了单例 Bean 的 Setter 注入循环依赖问题。
Spring事务的传播行为有哪些?
原始问法:
- Spring事务的传播行为有哪些?
来源题目:
SRC-11-111-391
面试先答
Spring 事务的传播行为定义了当一个事务方法被另一个事务方法调用时,事务应如何传播。共有 7 种传播行为:REQUIRED(默认,加入当前事务)、REQUIRES_NEW(新建事务,挂起当前)、SUPPORTS(支持当前事务,无则非事务执行)、MANDATORY(强制加入,无则抛异常)、NESTED(嵌套事务,保存点)、NEVER(非事务执行,有则抛异常)、NOT_SUPPORTED(非事务执行,挂起当前)。正确选择传播行为对保证数据一致性至关重要。
核心结论
- 7 种事务传播行为定义了方法间事务的协作方式。
- REQUIRED 是默认的传播行为,适用于大多数场景。
- REQUIRES_NEW 和 NESTED 是面试中最常考的两种特殊传播行为。
1. 是什么
事务传播行为是 Spring 框架对事务管理的核心机制,定义了当一个事务方法被另一个事务方法调用时,事务应该如何处理。
7 种传播行为详解:
| 传播行为 | 说明 | 当前事务存在时 | 当前事务不存在时 |
|---|---|---|---|
| REQUIRED | 加入当前事务 | 加入当前事务 | 创建新事务 |
| REQUIRES_NEW | 新建独立事务 | 挂起当前,新建 | 创建新事务 |
| SUPPORTS | 支持当前事务 | 加入当前事务 | 非事务执行 |
| MANDATORY | 强制事务 | 加入当前事务 | 抛出异常 |
| NESTED | 嵌套事务 | 创建嵌套事务(保存点) | 创建新事务 |
| NEVER | 非事务执行 | 抛出异常 | 非事务执行 |
| NOT_SUPPORTED | 非事务执行 | 挂起当前事务 | 非事务执行 |
2. 为什么需要它
- 复杂业务中涉及多个方法调用,每个方法可能需要不同的事务策略。
- 例如:主流程需要事务,但某些操作(如日志记录)不应影响主流程的事务。
- 正确的传播行为能保证数据的一致性和隔离性。
3. 底层原理与完整流程
事务传播处理流程:
TransactionInterceptor#invoke
→ TransactionAspectSupport#invokeWithinTransaction
→ 根据传播行为决定事务策略
├─ REQUIRED: 如果当前有事务则加入,否则新建
├─ REQUIRES_NEW: 挂起当前事务,新建独立事务
├─ NESTED: 当前事务内创建保存点
└─ 其他: 类似逻辑
→ 获取数据库连接,设置 autoCommit=false
→ 执行业务方法
→ 根据结果决定提交或回滚
REQUIRES_NEW 实现流程:
// TransactionAspectSupport
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
// 1. 挂起当前事务
SuspendedResourcesHolder suspendedResources = suspend(tx);
// 2. 创建新事务
tx = startTransaction(definition);
// 3. 执行业务
// 4. 恢复原事务
resume(tx, suspendedResources);
}
NESTED 实现流程:
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
// 1. 创建保存点
Savepoint savepoint = tx.createSavepoint();
// 2. 执行业务
// 3. 成功则释放保存点
tx.releaseSavepoint(savepoint);
// 4. 失败则回滚到保存点
tx.rollbackToSavepoint(savepoint);
}
4. 怎么使用
@Service
public class OrderService {
// 默认 REQUIRED:加入当前事务,无则新建
@Transactional
public void createOrder(Order order) { ... }
// REQUIRES_NEW:总是新建事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeLog(Log log) { ... }
// NESTED:嵌套事务
@Transactional(propagation = Propagation.NESTED)
public void updateStock(Stock stock) { ... }
// SUPPORTS:有事务则加入,无则非事务
@Transactional(propagation = Propagation.SUPPORTS)
public void queryOrder(Long id) { ... }
// MANDATORY:必须在事务中调用
@Transactional(propagation = Propagation.MANDATORY)
public void processPayment(Payment payment) { ... }
// NEVER:不能在事务中调用
@Transactional(propagation = Propagation.NEVER)
public void sendNotification(Notification notification) { ... }
// NOT_SUPPORTED:非事务执行
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void exportData() { ... }
}
5. 适用场景
- REQUIRED:绝大多数业务方法的默认选择。
- REQUIRES_NEW:需要独立提交/回滚的操作(如日志记录、审计)。
- NESTED:需要部分回滚的复杂业务流程。
- MANDATORY:强制要求在事务环境中执行(如核心金融操作)。
6. 不适用场景与替代方案
- 避免在嵌套方法中使用
REQUIRES_NEW,会导致事务边界复杂。 NESTED需要数据库支持保存点(JDBC 3.0+)。- 避免使用
NEVER和NOT_SUPPORTED,除非非常确定需要非事务执行。
7. 优缺点与技术取舍
| 传播行为 | 优点 | 缺点 |
|---|---|---|
| REQUIRED | 简单、默认选择 | 子方法失败会影响主事务 |
| REQUIRES_NEW | 独立事务,互不影响 | 资源消耗大、性能开销 |
| NESTED | 支持部分回滚 | 依赖数据库保存点支持 |
| MANDATORY | 强制事务保护 | 调用方必须提供事务 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 事务不生效 | 检查方法是否为 public、是否在同类内部调用、是否有异常 |
| 嵌套事务回滚导致全部回滚 | 使用 REQUIRES_NEW 或 NESTED 隔离事务 |
UnexpectedRollbackException |
主事务提交时发现已被标记为回滚,检查子事务的 rollback 状态 |
9. 版本差异与实现边界
- Spring 6.x 对事务传播的行为与之前版本一致。
- NESTED 传播行为在 JDBC 3.0 之前需要驱动支持。
10. 常见追问
追问 1:REQUIRES_NEW 和 NESTED 的区别? 答:REQUIRES_NEW 是完全独立的事务,NESTED 是当前事务内的保存点;REQUIRES_NEW 的回滚不影响外层,NESTED 的回滚可以回到保存点。
追问 2:事务的隔离级别有哪些? 答:READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE,默认使用数据库的隔离级别。
11. 易错点
- ❌ 错误:REQUIRED 传播行为下子方法异常不会影响主事务。
- ✅ 正确:REQUIRED 下子方法异常会触发主事务回滚,因为它们共享同一事务。
一句话总结
Spring 事务传播行为定义了方法间事务的协作方式,REQUIRED 为默认,REQUIRES_NEW 和 NESTED 提供了事务隔离机制。
@Transactional注解的原理是什么?失效场景有哪些?
原始问法:
- @Transactional注解的原理是什么?失效场景有哪些?
来源题目:
SRC-11-111-392
面试先答
@Transactional 的原理基于 Spring AOP 实现。容器启动时,TransactionInterceptor 拦截标注了 @Transactional 的方法,在方法执行前开启事务(获取数据库连接、设置自动提交为 false),方法执行后根据结果决定提交或回滚。@Transactional 失效的常见场景包括:方法非 public、同类内部调用、异常被捕获未抛出、默认只回滚 RuntimeException、MyISAM 引擎不支持事务等。
核心结论
@Transactional基于 AOP 代理实现,核心是TransactionInterceptor。- 默认只回滚
RuntimeException和Error,不回滚受检异常。 - 常见失效场景:非 public、同类调用、异常未抛出、引擎不支持。
1. 是什么
@Transactional 是 Spring 提供的声明式事务管理注解,可标注在方法或类上,表示该方法/类的所有方法需要事务支持。
属性说明:
| 属性 | 说明 | 默认值 |
|---|---|---|
| propagation | 事务传播行为 | REQUIRED |
| isolation | 事务隔离级别 | DEFAULT |
| timeout | 事务超时时间 | -1(不超时) |
| readOnly | 是否只读 | false |
| rollbackFor | 指定回滚的异常类 | RuntimeException |
| noRollbackFor | 指定不回滚的异常类 | 无 |
2. 为什么需要它
- 声明式事务使业务代码与事务逻辑解耦。
- 避免手动管理事务的繁琐代码(
begin、commit、rollback)。 - 统一事务管理策略,便于维护。
3. 底层原理与完整流程
事务开启流程:
1. TransactionInterceptor#invoke 拦截方法调用
2. TransactionAspectSupport#invokeWithinTransaction
3. 根据传播行为决定是否创建事务
4. DataSourceTransactionManager#doBegin
a. 获取数据库连接
b. 设置 connection.setAutoCommit(false)
c. 设置隔离级别
d. 将连接绑定到当前线程(ThreadLocal)
5. 执行业务方法
事务提交/回滚流程:
1. 方法正常返回 → TransactionAspectSupport#commit
→ DataSourceTransactionManager#doCommit
→ connection.commit()
→ 释放连接
2. 方法抛出异常 → TransactionAspectSupport#rollback
→ DataSourceTransactionManager#doRollback
→ connection.rollback()
→ 释放连接
4. 怎么使用
@Service
public class OrderService {
// 最简方式:使用默认配置
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
inventoryService.deductStock(order.getProductId());
}
// 自定义配置
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
timeout = 30,
rollbackFor = Exception.class,
readOnly = false
)
public void updateOrder(Order order) { ... }
// 类级别注解:所有方法默认开启事务
@Transactional
@Service
public class UserService { ... }
// 只读事务优化
@Transactional(readOnly = true)
public User getUser(Long id) { ... }
}
5. 适用场景
- 绝大多数写操作方法。
- 需要保证数据一致性的业务操作。
- 跨多个表操作需要原子性的场景。
6. 不适用场景与替代方案
- 只读查询操作可使用
readOnly = true优化。 - 不需要事务的简单操作避免滥用
@Transactional。 - 分布式事务场景使用 Seata 等框架替代。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 代码 | 声明式、简洁 | 存在多种失效场景 |
| 一致性 | 统一事务管理 | 默认只回滚 RuntimeException |
| 性能 | 合理配置可优化 | 事务范围过大影响性能 |
8. 常见失效场景及解决方案
| 失效场景 | 原因 | 解决方案 |
|---|---|---|
| 方法非 public | AOP 代理限制 | 将方法改为 public |
| 同类内部调用 | 未经过代理对象 | 通过注入自身或 ApplicationContext 获取代理 |
| 异常被 try-catch 捕获 | 异常未抛到代理层 | 捕获后重新抛出或手动回滚 |
| 默认只回滚 RuntimeException | Spring 默认策略 | 使用 rollbackFor = Exception.class |
| 数据库引擎不支持 | MyISAM 不支持事务 | 切换到 InnoDB 引擎 |
| 未被 Spring 管理 | 非 Spring Bean | 添加 @Component 等注解 |
同类内部调用解决方案:
@Service
public class OrderService {
@Autowired
private ApplicationContext context;
public void methodA() {
// 通过代理对象调用,事务生效
context.getBean(OrderService.class).methodB();
}
@Transactional
public void methodB() {
// 事务生效
}
}
捕获异常后手动回滚:
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
try {
// 业务逻辑
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new RuntimeException(e);
}
}
9. 版本差异与实现边界
- Spring 4.2+ 支持
@Transactional的rollbackForClassName属性。 - Spring 6.x 中
@Transactional的超时默认值为 -1(不超时)。 - Spring 不支持分布式事务(需使用 Spring Cloud Seata 等)。
10. 常见追问
追问 1:
@Transactional和编程式事务的区别? 答:@Transactional是声明式事务,基于 AOP;编程式事务使用TransactionTemplate,更灵活但代码侵入。追问 2:如何实现分布式事务? 答:使用 Seata、TCC、Saga 等分布式事务框架,Spring 的
@Transactional仅支持单数据源事务。追问 3:事务超时怎么配置? 答:
@Transactional(timeout = 30)单位为秒,超时后自动回滚。
11. 易错点
❌ 错误:
@Transactional可以作用在 private 方法上。✅ 正确:AOP 代理只能拦截 public 方法,private 方法上的注解无效。
❌ 错误:
@Transactional默认回滚所有异常。✅ 正确:默认只回滚
RuntimeException和Error,受检异常需配置rollbackFor。❌ 错误:同类内部调用
@Transactional方法事务仍然生效。✅ 正确:同类内部调用走原始对象引用,不经过代理,事务不生效。
一句话总结
@Transactional 基于 AOP 实现声明式事务管理,通过 TransactionInterceptor 在方法前后开启和提交/回滚事务,注意避免非 public、同类调用、异常捕获等失效场景。
Spring Boot自动装配原理是什么?Spring Boot Starter是什么?
原始问法:
- Spring Boot自动装配原理是什么?
- Spring Boot Starter是什么?
来源题目:
SRC-11-112-393,SRC-11-112-394
面试先答
Spring Boot 自动装配的核心是 @EnableAutoConfiguration 注解,它通过 ImportSelector 机制从 spring.factories 文件中加载预定义的配置类,根据类路径上的依赖自动创建相应的 Bean。Starter 是一种特殊的依赖模块,它封装了特定功能的自动配置、默认依赖和配置属性。自动装配流程:@SpringBootApplication → @EnableAutoConfiguration → AutoConfigurationImportSelector → 读取 spring.factories → 根据条件注解筛选配置类 → 注册 Bean。核心条件注解包括 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty。
核心结论
- 自动装配核心:
@EnableAutoConfiguration通过ImportSelector加载配置类。 - 条件装配:通过
@Conditional系列注解实现按需加载。 - Starter 机制:封装自动配置和依赖的模块化方案。
1. 是什么
Spring Boot 自动装配(Auto-Configuration)是指根据类路径中的依赖、配置文件等条件,自动配置 Spring 应用的 Bean 和功能。开发者无需手动编写大量配置代码,引入 Starter 依赖即可获得开箱即用的功能。
核心组件:
| 组件 | 说明 |
|---|---|
@SpringBootApplication |
启动类注解,包含自动装配 |
@EnableAutoConfiguration |
启用自动装配的核心注解 |
AutoConfigurationImportSelector |
加载配置类的选择器 |
spring.factories |
配置类注册文件 |
@Conditional |
条件装配注解的元注解 |
Starter |
封装自动配置的依赖模块 |
2. 为什么需要它
- 传统 Spring 项目需要大量 XML 配置或 Java Config 配置。
- 每个功能模块(如数据库、缓存)都需要手动配置数据源、连接池等。
- 自动装配通过约定优于配置的方式,大幅减少配置工作量。
- Starter 机制使项目依赖管理更清晰。
3. 底层原理与完整流程
自动装配完整流程:
1. SpringApplication.run(Application.class)
2. 创建 ApplicationContext
3. 刷新上下文 → invokeBeanFactoryPostProcessors
4. 处理 @EnableAutoConfiguration 注解
5. @EnableAutoConfiguration 的 @Import 导入 AutoConfigurationImportSelector
6. AutoConfigurationImportSelector#selectImports
a. 读取 META-INF/spring.factories 中的 EnableAutoConfiguration 配置
b. 得到候选配置类列表
c. 过滤掉 spring.autoconfigure.exclude 中排除的配置
7. 加载配置类,解析 @Bean 方法
8. 根据 @Conditional 系列注解进行条件判断
9. 符合条件的 @Bean 注册到容器
核心条件注解:
| 注解 | 说明 | 示例 |
|---|---|---|
@ConditionalOnClass |
类路径存在指定类时生效 | @ConditionalOnClass({DataSource.class}) |
@ConditionalOnMissingBean |
容器不存在指定 Bean 时生效 | @ConditionalOnMissingBean(name = "dataSource") |
@ConditionalOnProperty |
配置文件属性符合条件时生效 | @ConditionalOnProperty(prefix = "spring.datasource") |
@ConditionalOnWebApplication |
Web 应用环境时生效 | @ConditionalOnWebApplication(type = SERVLET) |
@ConditionalOnBean |
容器存在指定 Bean 时生效 | @ConditionalOnBean(DataSource.class) |
@ConditionalOnExpression |
SpEL 表达式为 true 时生效 | @ConditionalOnExpression("${app.enabled:true}") |
spring.factories 文件示例:
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
自定义 Starter 结构:
my-spring-boot-starter/
├── src/main/java/
│ └── com/example/myapp/
│ ├── MyAppAutoConfiguration.java # 自动配置类
│ └── MyAppProperties.java # 属性绑定类
├── src/main/resources/
│ ├── META-INF/
│ │ └── spring.factories # 注册自动配置
│ └── META-INF/additional-spring-configuration-metadata.json
└── pom.xml
4. 怎么使用
使用自动装配:
// 启动类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
自定义配置覆盖自动装配:
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/example
username: root
password: password
redis:
host: localhost
port: 6379
排除特定自动配置:
// 方式一:启动类注解排除
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class Application { ... }
// 方式二:配置文件排除
spring.autoconfigure.exclude=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
创建自定义 Starter:
// 1. 属性绑定类
@ConfigurationProperties(prefix = "myapp")
public class MyAppProperties {
private String name = "default";
private int timeout = 5000;
// getter/setter
}
// 2. 自动配置类
@Configuration
@EnableConfigurationProperties(MyAppProperties.class)
@ConditionalOnClass(MyService.class)
@ConditionalOnMissingBean(MyService.class)
@EnableConfigurationProperties(MyAppProperties.class)
public class MyAppAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyAppProperties properties) {
return new MyService(properties.getName(), properties.getTimeout());
}
}
// 3. 注册到 spring.factories
// org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
// com.example.myapp.MyAppAutoConfiguration
5. 适用场景
- 快速搭建 Spring 应用(通过 Starter 一键引入依赖)。
- 减少样板代码,提升开发效率。
- 统一技术栈配置,保证团队一致性。
- 第三方框架快速集成 Spring Boot。
6. 不适用场景与替代方案
- 对配置有极致定制需求的场景(可能需要手动配置)。
- 微服务架构中多个 Starter 可能导致依赖冲突。
- 替代方案:手动 Spring Java Config(更灵活但配置量大)。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 效率 | 开箱即用,减少配置 | 自动配置不透明,调试困难 |
| 一致性 | 统一配置标准 | 版本兼容性问题 |
| 灵活性 | 可通过属性覆盖 | 复杂场景覆盖困难 |
| 启动速度 | 合理配置启动快 | 扫描和条件判断有开销 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 自动配置不生效 | 检查依赖是否引入、配置类是否被扫描、条件是否满足 |
| 自动配置与自定义 Bean 冲突 | 使用 @ConditionalOnMissingBean 让自定义 Bean 优先 |
| 启动时自动配置报错 | 查看 ConditionEvaluationReport 分析条件评估结果 |
| 排除不需要的自动配置 | 使用 exclude 或 spring.autoconfigure.exclude |
查看条件评估报告:
// 启动时打印条件评估报告
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 在日志级别中开启
// logging.level.org.springframework.boot.autoconfigure=DEBUG
9. 版本差异与实现边界
- Spring Boot 2.x:支持
spring.factories机制。 - Spring Boot 3.x:新增
AutoConfiguration.imports文件机制,推荐使用spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。 - Spring Boot 3.x 基于 Spring Framework 6.x,要求 Java 17+。
10. 常见追问
追问 1:
@SpringBootApplication由哪些注解组成? 答:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan、@ConfigurationPropertiesScan。追问 2:如何查看哪些自动配置被加载了? 答:启动时添加
--debug参数或设置debug=true,控制台会输出条件评估报告。追问 3:
spring.factories和AutoConfiguration.imports的区别? 答:spring.factories是通用机制,AutoConfiguration.imports是 Spring Boot 3.x 专门用于自动配置的机制,性能更优。
11. 易错点
❌ 错误:引入 Starter 后无需任何配置即可使用。
✅ 正确:Starter 提供默认配置,但通常需要通过
application.yml/application.properties覆盖关键属性。❌ 错误:自动配置的 Bean 无法被自定义 Bean 覆盖。
✅ 正确:使用
@ConditionalOnMissingBean可让自定义 Bean 优先于自动配置。❌ 错误:
@SpringBootApplication只能标注在启动类上。✅ 正确:实际上是一个组合注解,可标注在任何配置类上,但通常标注在启动类。
一句话总结
Spring Boot 自动装配通过 @EnableAutoConfiguration 触发,从 spring.factories 加载配置类,基于条件注解按需注册 Bean,Starter 封装了特定功能的完整依赖和配置。
Spring Boot常用的注解有哪些?
原始问法:
- Spring Boot常用的注解有哪些?
来源题目:
SRC-11-112-395
面试先答
Spring Boot 常用注解分为几大类:启动类注解(@SpringBootApplication)、配置注解(@Configuration、@Bean、@ConfigurationProperties)、依赖注入注解(@Autowired、@Resource)、Web 注解(@RestController、@RequestMapping、@GetMapping、@PostMapping)、数据库注解(@Transactional、@Mapper)、条件装配注解(@ConditionalOnClass、@ConditionalOnMissingBean)。@SpringBootApplication 是核心入口注解,自动装配和 Starter 是其核心机制。
核心结论
- Spring Boot 注解分为启动类、配置、注入、Web、数据库、条件六大类。
@SpringBootApplication是启动入口,包含三个核心注解。@ConfigurationProperties是 Spring Boot 特有的类型安全配置绑定注解。
1. 是什么
Spring Boot 注解是对 Spring Framework 注解体系的扩展和补充,简化了 Spring 应用的配置和开发。
注解分类汇总:
| 类别 | 注解 | 说明 |
|---|---|---|
| 启动类 | @SpringBootApplication |
启动类注解,启用自动装配和组件扫描 |
@EnableAutoConfiguration |
单独启用自动装配 | |
| 配置类 | @Configuration |
标注配置类,替代 XML |
@Bean |
标注 Bean 工厂方法 | |
@ConfigurationProperties |
类型安全的配置属性绑定 | |
@Value |
注入单个配置属性 | |
| 依赖注入 | @Autowired |
按类型注入 |
@Resource |
按名称注入 | |
@Qualifier |
指定注入的 Bean 名称 | |
@Primary |
标注首选 Bean | |
| 组件扫描 | @Component |
通用组件 |
@Service |
业务服务层 | |
@Repository |
数据访问层 | |
@Controller |
Web 控制器 | |
@RestController |
REST 控制器(= @Controller + @ResponseBody) | |
| Web | @RequestMapping |
通用请求映射 |
@GetMapping |
GET 请求映射 | |
@PostMapping |
POST 请求映射 | |
@PutMapping |
PUT 请求映射 | |
@DeleteMapping |
DELETE 请求映射 | |
@RequestParam |
请求参数绑定 | |
@PathVariable |
路径变量绑定 | |
@RequestBody |
请求体绑定 | |
@ResponseBody |
响应体序列化 | |
| 条件装配 | @ConditionalOnClass |
类存在时装配 |
@ConditionalOnMissingBean |
Bean 不存在时装配 | |
@ConditionalOnProperty |
属性符合条件时装配 | |
| 数据库 | @Transactional |
声明式事务 |
@Mapper |
MyBatis Mapper 接口 | |
@Repository |
数据访问组件 |
2. 为什么需要它
- 注解比 XML 配置更直观、更易维护。
- 类型安全的配置绑定避免了字符串硬编码。
- 条件装配实现了按需加载,减少不必要的 Bean。
- 注解驱动的开发模式提高了开发效率。
3. 底层原理与完整流程
注解处理流程:
1. 容器启动时扫描指定包(@ComponentScan)
2. 发现所有标注了注解的类
3. 根据注解类型进行不同处理:
a. @Component/@Service/@Repository → 注册为 Bean
b. @Configuration → 解析 @Bean 方法
c. @RestController → 注册为 Web 控制器
d. @Transactional → 创建 AOP 代理
4. @ConfigurationProperties → 绑定配置属性到 Bean 字段
5. @Conditional* → 条件判断后决定是否注册
@ConfigurationProperties 绑定流程:
// 配置文件
// application.yml: myapp.name=test, myapp.timeout=5000
@ConfigurationProperties(prefix = "myapp")
public class MyAppConfig {
private String name; // 自动绑定 myapp.name
private int timeout; // 自动绑定 myapp.timeout
// getter/setter
}
4. 怎么使用
典型 Spring Boot 应用示例:
// 启动类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 配置类
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
// 配置属性类
@ConfigurationProperties(prefix = "app")
@Component
public class AppProperties {
private String name;
private int port;
}
// Service 层
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// Controller 层
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUser(id);
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
}
// 数据访问层
@Repository
public class UserRepositoryImpl implements UserRepository { ... }
// 事务管理
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) { ... }
}
5. 适用场景
- 所有 Spring Boot 应用都使用这些注解。
- RESTful API 开发。
- 微服务架构下的服务注册与发现。
- 数据访问层的快速开发。
6. 不适用场景与替代方案
- 避免滥用注解(如在非 Spring 托管类上使用
@Autowired)。 - 避免在 Controller 中编写业务逻辑。
- 替代方案:Kotlin DSL、Groovy DSL(但 Spring Boot 主要支持 Java)。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 可读性 | 声明式、直观 | 注解过多影响阅读 |
| 类型安全 | @ConfigurationProperties 类型安全 |
@Value 不类型安全 |
| 开发效率 | 快速开发 | 复杂场景调试困难 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
@ConfigurationProperties 不生效 |
添加 @Component 或 @EnableConfigurationProperties |
@Value 注入 null |
检查配置文件属性是否存在,使用 :default 提供默认值 |
@ConditionalOnClass 不生效 |
确认指定类在类路径中存在 |
@Bean 方法重复调用 |
使用 @Configuration 类的 CGLIB 代理保证单例 |
9. 版本差异与实现边界
- Spring Boot 2.x:推荐使用
@ConfigurationProperties替代@Value。 - Spring Boot 3.x:支持
@ConfigurationProperties记录类型(Record)绑定。 - Spring Framework 6.x:
@Autowired字段注入不再推荐,仅支持构造器注入。
10. 常见追问
追问 1:
@Configuration和@Component的区别? 答:@Configuration用于配置类,@Component用于业务组件;@Configuration类的@Bean方法会被 CGLIB 代理,保证单例。追问 2:
@RestController和@Controller的区别? 答:@RestController=@Controller+@ResponseBody,方法默认直接返回 JSON 而非视图。追问 3:
@Value和@ConfigurationProperties的区别? 答:@Value注入单个属性,不类型安全;@ConfigurationProperties绑定一组属性,类型安全,支持校验。
11. 易错点
❌ 错误:
@Autowired可以注入 null。✅ 正确:默认
required=true,找不到 Bean 时抛异常;需允许 null 时设置required=false。❌ 错误:
@Bean方法每次调用都创建新实例。✅ 正确:
@Configuration类被 CGLIB 代理,@Bean方法被拦截,保证单例。❌ 错误:
@ConfigurationProperties可以直接在任何类上使用。✅ 正确:需要配合
@Component或@EnableConfigurationProperties使用。
一句话总结
Spring Boot 常用注解覆盖了启动、配置、注入、Web、数据库、条件装配六大类,以声明式方式简化开发,@SpringBootApplication 是核心入口。
Spring MVC的工作流程是什么?
原始问法:
- Spring MVC的工作流程是什么?
来源题目:
SRC-11-113-396
面试先答
Spring MVC 的核心是 DispatcherServlet(前端控制器),请求处理流程为:1. 用户请求到达 Tomcat;2. Tomcat 将请求交给 DispatcherServlet;3. DispatcherServlet 调用 HandlerMapping 查找对应的 Handler(Controller 方法);4. 找到后通过 HandlerAdapter 执行;5. 执行前经过拦截器(Interceptor)的 preHandle;6. 执行 Controller 方法,返回 ModelAndView;7. 通过 ViewResolver 解析视图;8. 渲染视图返回给用户。使用 @RestController 时直接将返回值序列化为 JSON,跳过视图解析。
核心结论
DispatcherServlet是 Spring MVC 的前端控制器,统一接收和分发请求。- 核心组件:
HandlerMapping(映射查找)、HandlerAdapter(适配器执行)、ViewResolver(视图解析)。 @RestController直接返回 JSON,不经过视图渲染。
1. 是什么
Spring MVC 是基于 MVC(Model-View-Controller)模式的 Web 框架,核心是 DispatcherServlet 作为前端控制器,将请求分发给相应的处理器。
MVC 三层含义:
| 层次 | 说明 | Spring MVC 对应 |
|---|---|---|
| Model | 数据模型 | POJO/DTO |
| View | 视图渲染 | JSP/Thymeleaf/JSON |
| Controller | 控制器 | @Controller/@RestController |
核心组件:
| 组件 | 说明 |
|---|---|
DispatcherServlet |
前端控制器,核心入口 |
HandlerMapping |
请求到处理器的映射 |
HandlerAdapter |
适配器,执行处理器 |
HandlerInterceptor |
拦截器,请求前后处理 |
ViewResolver |
视图解析器 |
View |
视图渲染 |
Model |
数据模型载体 |
2. 为什么需要它
- 统一的请求分发机制,简化 Web 开发。
- 支持多种视图技术和数据格式。
- 提供拦截器机制实现横切关注点(日志、权限)。
- 与 Spring IOC 容器无缝集成。
3. 底层原理与完整流程
完整请求处理流程:
1. 用户发送 HTTP 请求
↓
2. Tomcat 接收请求 → 映射到 DispatcherServlet
↓
3. DispatcherServlet#doDispatch
a. 调用 getHandler() 查找 Handler
→ HandlerMapping 根据 URL 查找 HandlerExecutionChain
→ HandlerExecutionChain 包含 Handler + 拦截器列表
b. 调用 getHandlerAdapter() 获取适配器
→ 支持多种 Handler:Controller 接口、注解式、HttpRequestHandler
c. 执行拦截器 preHandle()(按顺序)
→ 任一返回 false 则中断请求
d. HandlerAdapter#handle() 执行 Handler
→ 解析请求参数 → 调用 Controller 方法 → 返回 ModelAndView
e. 执行拦截器 postHandle()(按逆序)
f. 渲染视图
→ ViewResolver 解析视图名 → View#render()
g. 执行拦截器 afterCompletion()(按逆序)
↓
4. 返回 HTTP 响应给客户端
DispatcherServlet 核心代码:
// DispatcherServlet#doDispatch
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
// 1. 查找 Handler
HandlerExecutionChain mappedHandler = getHandler(request);
// 2. 获取适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 执行拦截器 preHandle
if (!mappedHandler.applyPreHandle(request, response)) return;
// 4. 执行 Handler
ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());
// 5. 执行拦截器 postHandle
mappedHandler.applyPostHandle(request, response, mv);
// 6. 渲染视图
processDispatchResult(request, response, mappedHandler, mv, null);
}
@RestController 流程差异:
@RestController 方法返回值
→ RequestResponseBodyMethodProcessor
→ 使用 HttpMessageConverter 将返回值序列化为 JSON
→ 直接写入 HttpServletResponse
→ 跳过 ViewResolver 和视图渲染
4. 怎么使用
// 启动类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// REST 控制器
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUser(id);
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody UserDTO dto) {
User user = userService.createUser(dto);
return ResponseEntity.ok(user);
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody UserDTO dto) {
return userService.updateUser(id, dto);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
配置拦截器:
@Component
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
System.out.println("请求路径: " + request.getRequestURI());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
System.out.println("请求处理完成");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
System.out.println("请求结束");
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/error");
}
}
5. 适用场景
- 传统 Web 应用(JSP 视图渲染)。
- RESTful API 服务(前后端分离)。
- 移动端后端服务。
- 微服务架构中的 API 网关。
6. 不适用场景与替代方案
- 非 Web 应用(使用 Spring IOC/数据访问即可)。
- 替代方案:Spring WebFlux(响应式编程)、Javalin、Vert.x。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 架构 | 清晰的 MVC 分层 | 学习曲线陡峭 |
| 扩展性 | 丰富的扩展点(拦截器、转换器) | 配置复杂 |
| 生态 | 与 Spring 生态无缝集成 | 视图技术更新快 |
| 性能 | 性能优秀 | 复杂的参数绑定有开销 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 404 找不到接口 | 检查 @RequestMapping 路径、@SpringBootApplication 扫描范围 |
| 参数绑定失败 | 检查 @RequestParam/@PathVariable/@RequestBody 使用是否正确 |
| 415 Unsupported Media Type | 检查 Content-Type 请求头,确保为 application/json |
| 拦截器不执行 | 确认拦截器注册、路径配置是否正确 |
| CORS 跨域问题 | 使用 @CrossOrigin 或全局配置 WebMvcConfigurer#addCorsMappings |
全局异常处理:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<?> handleBusinessException(BusinessException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Internal Server Error"));
}
}
9. 版本差异与实现边界
- Spring MVC 5.x 支持响应式异步处理。
- Spring Boot 3.x 基于 Spring MVC 6.x,要求 Servlet 5.0+。
- Spring MVC 不负责安全(由 Spring Security 处理)和数据校验(由 Jakarta Validation 处理)。
10. 常见追问
追问 1:
DispatcherServlet和Servlet的关系? 答:DispatcherServlet继承自HttpServlet,是 Spring MVC 的前端控制器。追问 2:
HandlerMapping和HandlerAdapter的区别? 答:HandlerMapping负责查找请求对应的 Handler,HandlerAdapter负责适配并执行 Handler。追问 3:Spring MVC 如何实现 RESTful? 答:通过
@RequestMapping、@GetMapping、@PostMapping等注解映射 HTTP 方法和路径。
11. 易错点
❌ 错误:
@RestController需要配置视图解析器。✅ 正确:
@RestController直接返回 JSON,不需要视图解析器。❌ 错误:拦截器的
preHandle返回false表示继续执行。✅ 正确:
preHandle返回true表示继续,false表示中断。❌ 错误:
@PathVariable参数可以为 null。✅ 正确:默认不能为 null,需要设置
required = false。
一句话总结
Spring MVC 以 DispatcherServlet 为核心,通过 HandlerMapping 查找、HandlerAdapter 执行、ViewResolver 渲染,形成清晰的请求处理流水线。
Tomcat的Connector组件的核心职责是什么?
原始问法:
- Tomcat的Connector组件的核心职责是什么?
来源题目:
SRC-11-113-397
面试先答
Tomcat 的 Connector 组件是 Tomcat 的网络通信层,核心职责是接收客户端请求并将请求传递给 Engine 处理,然后将处理结果返回给客户端。它负责监听端口、解析 HTTP 协议、管理连接线程。Connector 采用 NIO(非阻塞 IO)模型,由 ProtocolHandler、Endpoint、Adapter 三个核心组件组成。ProtocolHandler 处理底层网络协议,Endpoint 管理连接和线程池,Adapter 将请求转换为 Servlet 请求并交给 Engine。
核心结论
- Connector 是 Tomcat 的网络入口,负责接收请求和返回响应。
- 核心组件:ProtocolHandler(协议处理)、Endpoint(端点管理)、Adapter(适配器)。
- NIO 模型下使用 Poller 线程管理连接,Worker 线程处理请求。
1. 是什么
Connector 是 Tomcat 的连接器组件,负责监听指定端口、接收客户端 TCP 连接、解析 HTTP/HTTPS 协议、将请求转发给 Engine(Servlet 容器)处理,并将响应写回客户端。
核心组件:
| 组件 | 说明 | 子组件 |
|---|---|---|
| ProtocolHandler | 协议处理器 | Endpoint + Adapter |
| Endpoint | 端点(网络层) | 监听端口、管理连接 |
| Adapter | 适配器 | 将请求转为 Servlet 请求 |
ProtocolHandler 实现:
| 实现类 | 说明 | 协议 |
|---|---|---|
| Http11Protocol | HTTP/1.1(默认) | HTTP/1.1 |
| Http2Protocol | HTTP/2 | HTTP/2 |
| AjpProtocol | AJP 协议 | AJP/1.3 |
Endpoint 实现:
| 实现类 | IO 模型 | 说明 |
|---|---|---|
| JIoEndpoint | 阻塞 IO(BIO) | JDK 1.4+,已废弃 |
| NioEndpoint | 非阻塞 IO(NIO) | JDK 1.4+,默认使用 |
| Nio2Endpoint | 异步 IO(AIO/NIO.2) | JDK 7+ |
2. 为什么需要它
- Tomcat 作为 Web 服务器,必须能接收和处理网络请求。
- Connector 隔离了网络通信和 Servlet 处理逻辑。
- 支持多种协议(HTTP/1.1、HTTP/2、AJP)和多种 IO 模型。
- 提供连接管理、线程池、SSL 配置等能力。
3. 底层原理与完整流程
NIO 模型请求处理流程:
1. 客户端建立 TCP 连接
↓
2. Acceptor 线程接收连接
a. ServerSocketChannel#accept() 接收新连接
b. 将连接注册到 Poller
↓
3. Poller 线程轮询连接事件
a. Selector#select() 等待事件
b. 可读事件 → 读取请求数据
c. 可写事件 → 写入响应数据
d. 读取完成 → 提交到 Worker 线程
↓
4. Worker 线程处理请求
a. 解析 HTTP 请求行和请求头
b. 通过 Adapter 转换为 Servlet Request
c. 交给 Engine → Host → Context → Wrapper
d. 执行 Servlet(Spring MVC 的 DispatcherServlet)
e. 获取 Servlet Response
↓
5. 响应写回
a. Adapter 转换为 HTTP 响应
b. Poller 线程将响应写回客户端
↓
6. 关闭或保持连接
Connector 配置结构:
<!-- server.xml -->
<Connector port="8080"
protocol="HTTP/1.1"
maxThreads="200"
minSpareThreads="25"
maxSpareThreads="75"
acceptCount="100"
connectionTimeout="20000"
redirectPort="8443" />
核心参数:
| 参数 | 说明 | 默认值 |
|---|---|---|
port |
监听端口 | 8080 |
maxThreads |
最大工作线程数 | 200 |
minSpareThreads |
最小空闲线程数 | 25 |
maxSpareThreads |
最大空闲线程数 | 75 |
acceptCount |
等待队列长度 | 100 |
connectionTimeout |
连接超时时间(ms) | 20000 |
4. 怎么使用
Spring Boot 内嵌 Tomcat 配置:
# application.yml
server:
port: 8080
tomcat:
threads:
max: 200
min-spare: 25
accept-count: 100
connection-timeout: 20000
max-connections: 8192
min-connections: 10
basedir: /tmp/tomcat
编程式配置:
@Configuration
public class TomcatConfig {
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
return factory -> {
factory.addConnectorCustomizers(connector -> {
connector.setMaxThreads(200);
connector.setMinSpareThreads(25);
connector.setAcceptCount(100);
connector.setConnectionTimeout(20000);
});
};
}
}
独立 Tomcat 配置:
<!-- server.xml Connector 配置 -->
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="200"
minSpareThreads="25"
maxSpareThreads="75"
acceptCount="100"
connectionTimeout="20000"
disableUploadTimeout="true" />
5. 适用场景
- Spring Boot 内嵌 Tomcat:适合微服务、快速开发。
- 独立 Tomcat:适合传统企业应用、需要精细配置的场景。
- 与 Apache/Nginx 配合:处理静态资源,Tomcat 处理动态请求。
6. 不适用场景与替代方案
- 非 HTTP 服务场景(使用 Netty 等网络框架)。
- 极致性能场景(考虑 Undertow、Jetty 等 alternatives)。
- 大量长连接 WebSocket(需要专门的网络框架)。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 稳定性 | 成熟稳定,生产级 | 配置复杂 |
| 性能 | NIO 模型性能优秀 | BIO 模型已废弃 |
| 生态 | 广泛支持的 Servlet 容器 | 启动速度略慢于 Undertow |
| 资源 | 线程池管理完善 | 高并发下可能成为瓶颈 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 端口被占用 | 修改 server.port 或释放占用端口的进程 |
| 请求超时 | 调整 connectionTimeout 和 asyncTimeout |
| 大文件上传失败 | 配置 max-swallow-size 和 max-http-form-post-size |
| 线程耗尽 | 调大 maxThreads 或优化代码逻辑 |
| NIO 与 BIO 切换 | Spring Boot 3.x 默认使用 NIO,无需手动切换 |
9. 版本差异与实现边界
- Tomcat 9.x:默认使用 NIO,支持 HTTP/2。
- Tomcat 10.x:基于 Jakarta EE 10,
javax→jakarta命名空间。 - Spring Boot 3.x 默认内嵌 Tomcat 10.x。
10. 常见追问
追问 1:Tomcat 的 Connector 和 Engine 是如何协作的? 答:Connector 接收请求后,通过 Adapter(
CoyoteAdapter)将请求转换为 Servlet 请求,交给 Engine 的 Pipeline 处理。追问 2:NIO 和 BIO 的区别? 答:BIO 每个连接占用一个线程,NIO 使用 Selector 多路复用,少量线程管理大量连接。
追问 3:Tomcat 如何实现请求的线程隔离? 答:每个请求由 Worker 线程独立处理,保证请求间的隔离性。
11. 易错点
❌ 错误:Tomcat Connector 只负责 HTTP 请求接收。
✅ 正确:Connector 负责整个请求处理流程:接收→解析→转发→响应。
❌ 错误:Spring Boot 默认使用 BIO 模型。
✅ 正确:Spring Boot 2.x+ 默认使用 NIO 模型(
Http11NioProtocol)。❌ 错误:
maxThreads越大性能越好。✅ 正确:过大会增加上下文切换开销,需要根据 CPU 核数合理配置。
一句话总结
Tomcat Connector 是网络通信的枢纽,通过 ProtocolHandler 和 Endpoint 管理网络连接,通过 Adapter 桥接 Servlet 容器,实现从 TCP 字节流到 HTTP 请求再到 Servlet 响应的完整转换。
MyBatis中#{}和${}的区别是什么?
原始问法:
- MyBatis中#{}和${}的区别是什么?
来源题目:
SRC-11-114-398
面试先答
#{} 和 ${} 是 MyBatis 中两种参数占位符,核心区别在于安全性和适用性:#{} 使用预编译语句(PreparedStatement),参数通过占位符 ? 传入,自动进行类型转换和 SQL 注入防护;${} 直接将参数值拼接到 SQL 中,不进行预编译,存在 SQL 注入风险。#{} 适用于大多数参数传递场景,${} 仅在需要动态指定表名、列名、排序方式等无法使用预编译的场景下使用,且必须配合白名单校验。
核心结论
#{}:预编译、类型安全、防止 SQL 注入,优先使用。${}:直接拼接、有注入风险,仅用于动态表名/列名。- 永远不要将用户输入直接传给
${}。
1. 是什么
MyBatis 中 #{} 和 ${} 是两种参数占位符,用于在 SQL 语句中传递参数。
#{}`(预编译参数):
- MyBatis 将
#{}替换为 JDBC 的?占位符。 - 使用
PreparedStatement设置参数,自动进行类型转换。 - 参数值作为字面量传入,不会改变 SQL 结构。
- 天然防止 SQL 注入。
${}`(直接拼接参数):
- MyBatis 将
${}直接替换为参数值。 - 使用
Statement执行(非预编译)。 - 参数值直接嵌入 SQL 字符串中。
- 存在 SQL 注入风险。
2. 为什么需要它
- SQL 中有些场景不能使用预编译(如表名、列名、排序关键字)。
#{}处理值,${}处理 SQL 结构部分。- 理解两者区别是编写安全 MyBatis 映射的基础。
3. 底层原理与完整流程
#{} 处理流程:
1. SQL 模板:SELECT * FROM user WHERE id = #{id}
2. MyBatis 解析为:SELECT * FROM user WHERE id = ?
3. 使用 PreparedStatement
4. ps.setInt(1, id) 设置参数
5. 数据库执行预编译 SQL
${} 处理流程:
1. SQL 模板:SELECT * FROM ${tableName} WHERE id = #{id}
2. MyBatis 解析为:SELECT * FROM user WHERE id = ?
(${tableName} 直接替换为 "user",#{id} 替换为 ?)
3. 只有 #{id} 使用 PreparedStatement 参数绑定
4. ${tableName} 的值直接嵌入 SQL
SQL 注入示例:
// 危险:如果 tableName = "user; DROP TABLE user"
// 使用 ${} 会导致 SQL 注入
// 必须使用白名单校验
String[] allowedTables = {"user", "order", "product"};
if (!Arrays.asList(allowedTables).contains(tableName)) {
throw new IllegalArgumentException("Invalid table name");
}
4. 怎么使用
// Mapper 接口
@Mapper
public interface UserMapper {
// #{id} → 预编译,安全
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(@Param("id") Long id);
// #{name} → 预编译,安全
List<User> selectByName(@Param("name") String name);
// ${tableName} → 动态表名,需要白名单
@Select("SELECT * FROM ${tableName} WHERE id = #{id}")
User selectFromTable(@Param("tableName") String tableName, @Param("id") Long id);
// ${orderBy} → 动态排序,需要白名单
@Select("SELECT * FROM user ORDER BY ${orderBy}")
List<User> selectAllOrderBy(@Param("orderBy") String orderBy);
}
XML 映射方式:
<!-- #{推荐使用} -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- ${仅用于动态表名/列名} -->
<select id="selectFromTable" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>
安全使用 ${} 的最佳实践:
public List<User> selectFromTable(String tableName) {
// 白名单校验
Set<String> allowedTables = Set.of("user", "order", "product");
if (!allowedTables.contains(tableName)) {
throw new IllegalArgumentException("Invalid table: " + tableName);
}
return userMapper.selectFromTable(tableName, id);
}
5. 适用场景
- #{}:所有参数值传递场景(WHERE 条件、INSERT/UPDATE 值等)。
- ${}:动态表名、列名、排序方式、SQL 片段拼接。
6. 不适用场景与替代方案
- 禁止在用户输入场景使用
${}。 - 动态 SQL 可使用
<if>、<where>、<foreach>等 XML 标签替代${}。
7. 优缺点与技术取舍
| 维度 | #{} | ${} |
|---|---|---|
| 安全性 | 防止 SQL 注入 | 存在 SQL 注入风险 |
| 类型处理 | 自动类型转换 | 直接字符串拼接 |
| 性能 | 预编译可复用 | 每次需重新编译 |
| 适用范围 | 值类型参数 | SQL 结构部分 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
${} 导致 SQL 注入 |
使用白名单校验或改用 <if>/<foreach> 标签 |
#{} 传入表名报错 |
表名不能用预编译,需使用 ${} + 白名单 |
| 动态 IN 条件 | 使用 <foreach> 标签而非 ${} |
<!-- 动态 IN 条件:使用 foreach -->
<select id="selectByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
9. 版本差异与实现边界
- MyBatis 3.x 中
#{}和${}行为一致。 - MyBatis 3.5+ 提供
@SelectProvider等注解支持动态 SQL。
10. 常见追问
追问 1:为什么
#{}能防止 SQL 注入? 答:#{}使用PreparedStatement,参数作为值传递,数据库将参数作为纯字面量处理,不会改变 SQL 语义。追问 2:
${}的实际应用场景有哪些? 答:动态表名(分表)、动态列名(动态查询)、动态排序(ORDER BY)、动态 Schema。
11. 易错点
❌ 错误:
${}比#{}性能更好。✅ 正确:
#{}使用预编译,性能更优且可复用执行计划。❌ 错误:可以用
${}传递 WHERE 条件值。✅ 正确:WHERE 条件的值应使用
#{},${}仅用于 SQL 结构部分。
一句话总结
#{} 预编译安全,用于传递参数值;${} 直接拼接灵活,用于动态 SQL 结构,必须配合白名单保证安全。
MyBatis的一级缓存和二级缓存是什么?
原始问法:
- MyBatis的一级缓存和二级缓存是什么?
来源题目:
SRC-11-114-399
面试先答
MyBatis 有两级缓存机制:一级缓存(SqlSession 级别)和二级缓存(Mapper/namespace 级别)。一级缓存默认开启,作用域为同一个 SqlSession,相同的查询在同一 SqlSession 中只执行一次 SQL;二级缓存需要手动开启,作用域为同一个 Mapper 的 namespace,跨 SqlSession 共享。一级缓存是 PerpetualCache(基于 HashMap),二级缓存默认实现也是 PerpetualCache,但可替换为 Redis、Ehcache 等。写入策略是:先写一级缓存,再写二级缓存。
核心结论
- 一级缓存:SqlSession 级别,默认开启,同一 SqlSession 内有效。
- 二级缓存:Mapper 级别,需手动开启,跨 SqlSession 共享。
- 更新操作(INSERT/UPDATE/DELETE)会清除相关缓存。
1. 是什么
MyBatis 缓存机制用于减少数据库查询次数,提升性能。
缓存层次结构:
SqlSessionFactory
└── SqlSession(一级缓存:PerpetualCache,默认开启)
└── Executor
├── BaseExecutor(基本执行器)
├── CachingExecutor(二级缓存装饰器,可选)
└── BatchExecutor(批处理执行器)
一级缓存(First-Level Cache):
| 属性 | 说明 |
|---|---|
| 作用域 | 同一个 SqlSession |
| 默认状态 | 开启(不可关闭) |
| 实现 | PerpetualCache(HashMap) |
| 生命周期 | SqlSession 创建时存在,关闭时清除 |
二级缓存(Second-Level Cache):
| 属性 | 说明 |
|---|---|
| 作用域 | 同一个 Mapper 的 namespace |
| 默认状态 | 关闭(需手动开启) |
| 实现 | 默认 PerpetualCache,可替换为 Redis/Ehcache |
| 生命周期 | 应用级别的缓存,跨 SqlSession |
2. 为什么需要它
- 减少数据库查询次数,降低 DB 压力。
- 提升应用响应速度。
- 一级缓存解决同一 SqlSession 内的重复查询。
- 二级缓存解决跨 SqlSession 的重复查询。
3. 底层原理与完整流程
查询流程:
1. 调用 Mapper 方法 → SqlSession.selectList()
↓
2. Executor 查询二级缓存
a. 如果命中 → 直接返回结果
b. 如果未命中 → 查询一级缓存
↓
3. 一级缓存查询
a. 如果命中 → 直接返回结果,同时写入二级缓存
b. 如果未命中 → 查询数据库
↓
4. 查询数据库,得到结果集
↓
5. 将结果写入一级缓存和二级缓存
↓
6. 返回结果
写入流程:
1. 执行 INSERT/UPDATE/DELETE
2. 清除一级缓存
3. 清除二级缓存(该 namespace 下的)
4. 执行 SQL 写入数据库
5. 提交事务
一级缓存核心代码:
// BaseExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) {
// 1. 查询一级缓存
CacheKey key = createCacheKey(ms, parameter, rowBounds);
List<E> list = localCache.getObject(key);
if (list != null) {
return list; // 缓存命中
}
// 2. 查询数据库
list = doQuery(ms, parameter, rowBounds, resultHandler);
// 3. 写入一级缓存
localCache.putObject(key, list);
return list;
}
二级缓存配置:
<!-- 在 Mapper XML 中开启二级缓存 -->
<mapper namespace="com.example.UserMapper">
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="false"/>
</mapper>
4. 怎么使用
一级缓存:
// 一级缓存:同一 SqlSession 内有效
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询:访问数据库
User user1 = mapper.selectById(1L);
// 第二次查询:命中一级缓存,不访问数据库
User user2 = mapper.selectById(1L);
// user1 == user2(同一对象)
sqlSession.close(); // 关闭后一级缓存清除
二级缓存:
<!-- 方式一:XML 配置 -->
<mapper namespace="com.example.UserMapper">
<cache/> <!-- 开启二级缓存 -->
<select id="selectById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
// 方式二:注解配置
@Mapper
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
@CacheNamespace
User selectById(Long id);
}
// 二级缓存:跨 SqlSession 有效
SqlSession session1 = sqlSessionFactory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1L);
session1.close(); // 关闭 SqlSession,结果写入二级缓存
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1L); // 命中二级缓存
session2.close();
5. 适用场景
- 一级缓存:同一事务内的重复查询。
- 二级缓存:读多写少的场景(如配置表、字典表)。
- 缓存热点数据减少数据库压力。
6. 不适用场景与替代方案
- 写操作密集的场景(缓存频繁失效)。
- 对数据实时性要求极高的场景。
- 一级缓存:不同 SqlSession 间不共享。
- 替代方案:使用 Redis 等外部缓存框架。
7. 优缺点与技术取舍
| 维度 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | Mapper namespace |
| 默认状态 | 开启 | 关闭 |
| 性能 | 内存操作,极快 | 跨会话,减少 DB 压力 |
| 一致性 | 高(同会话) | 低(需处理缓存失效) |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 一级缓存未命中 | 检查是否使用了同一个 SqlSession |
| 二级缓存数据不一致 | 使用 flushCache 手动刷新,或调整刷新策略 |
| 缓存穿透 | 对空结果也进行缓存 |
| 缓存雪崩 | 设置合理的过期时间,使用随机偏移 |
<!-- 手动刷新缓存 -->
<insert id="insertUser" flushCache="true">
INSERT INTO user (name) VALUES (#{name})
</insert>
9. 版本差异与实现边界
- MyBatis 3.x:支持 Redis、Ehcache 等二级缓存插件。
- MyBatis 3.5+:支持默认接口方法,缓存行为更稳定。
- 注意:MyBatis 二级缓存不适合分布式环境,需使用 Redis 等外部缓存。
10. 常见追问
追问 1:一级缓存何时失效? 答:SqlSession 关闭、执行 INSERT/UPDATE/DELETE、手动
clearCache()、跨 SqlSession。追问 2:二级缓存的淘汰策略有哪些? 答:LRU(最近最少使用,默认)、FIFO(先进先出)、SOFT(软引用)、WEAK(弱引用)。
追问 3:如何选择使用一级还是二级缓存? 答:一级缓存是默认行为,无需配置;二级缓存适用于读多写少、数据一致性要求不高的场景。
11. 易错点
❌ 错误:一级缓存默认关闭。
✅ 正确:一级缓存默认开启,无法关闭(MyBatis 设计如此)。
❌ 错误:二级缓存自动开启。
✅ 正确:二级缓存需要手动配置
<cache/>或@CacheNamespace。❌ 错误:一级缓存在所有 SqlSession 间共享。
✅ 正确:一级缓存在同一个 SqlSession 内有效。
一句话总结
MyBatis 一级缓存 SqlSession 级别默认开启,二级缓存 Mapper 级别需手动开启,两者协同减少数据库查询,更新操作自动清除相关缓存。
MyBatis的插件原理是什么?
原始问法:
- MyBatis的插件原理是什么?
来源题目:
SRC-11-114-400
面试先答
MyBatis 插件原理基于 JDK 动态代理和责任链模式,核心是 Interceptor 接口和 Plugin 工具类。MyBatis 允许在四大核心对象上进行拦截:Executor(执行器)、StatementHandler(语句处理器)、ParameterHandler(参数处理器)、ResultSetHandler(结果集处理器)。开发自定义插件需实现 Interceptor 接口,使用 @Intercepts 和 @Signature 注解指定拦截目标。MyBatis 在创建这些核心对象时,通过 Plugin.wrap() 方法生成代理对象。
核心结论
- 插件基于 JDK 动态代理拦截四大核心对象。
- 四大拦截目标:Executor、StatementHandler、ParameterHandler、ResultSetHandler。
- 核心接口:
Interceptor,核心工具类:Plugin。
1. 是什么
MyBatis 插件是一种拦截器机制,允许在 MyBatis 核心方法执行前后插入自定义逻辑(如分页、审计、性能监控)。
四大拦截目标:
| 目标 | 方法 | 说明 |
|---|---|---|
| Executor | query()、update() |
SQL 执行器 |
| StatementHandler | prepare()、batch() |
Statement 处理器 |
| ParameterHandler | setParameters() |
参数处理器 |
| ResultSetHandler | handleResultSets() |
结果集处理器 |
核心注解:
| 注解 | 说明 |
|---|---|
@Intercepts |
标识插件类,包含多个 @Signature |
@Signature |
指定拦截的方法和参数类型 |
2. 为什么需要它
- 在不修改 MyBatis 源码的前提下扩展功能。
- 实现分页、审计、性能监控等横切关注点。
- 统一数据访问层的增强逻辑。
- 解耦业务逻辑与增强逻辑。
3. 底层原理与完整流程
插件加载流程:
1. MyBatis 启动时加载所有 Interceptor 实现
2. 将 Interceptor 添加到 interceptorChain
3. 创建核心对象(Executor 等)时
a. 调用 InterceptorChain#pluginAll(target)
b. 遍历所有 Interceptor
c. 调用 interceptor.plugin(target)
d. 通过 Plugin.wrap() 创建代理对象
4. 返回代理对象替代原始对象
Plugin.wrap() 原理:
// Plugin#wrap
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Integer>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 创建 JDK 动态代理
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap)
);
}
return target;
}
拦截执行流程:
1. 调用代理对象的方法
2. Plugin#invoke 被触发
a. 检查当前方法是否在拦截列表中
b. 如果是 → 调用 interceptor.intercept(invocation)
c. 如果不是 → 直接调用原始方法
3. Interceptor#intercept 执行自定义逻辑
a. 执行前置逻辑
b. 调用 invocation.proceed() 执行目标方法
c. 执行后置逻辑
4. 怎么使用
实现分页插件:
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取 StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 2. 获取原始 SQL
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
// 3. 改造 SQL,添加分页
String pageSql = originalSql + " LIMIT #{offset}, #{limit}";
// 4. 创建新的 BoundSql
BoundSql newBoundSql = new BoundSql(
boundSql.getConfiguration(),
pageSql,
boundSql.getParameterMappings(),
boundSql.getParameterObject()
);
// 5. 替换 BoundSql
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
metaObject.setValue("delegate.boundSql", newBoundSql);
// 6. 执行原始方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
注册插件:
// Java 配置
@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
<!-- XML 配置 -->
<plugins>
<plugin interceptor="com.example.PageInterceptor">
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
5. 适用场景
- 数据库分页(PageHelper、MyBatis-Plus 分页插件)。
- 数据权限过滤(多租户、数据权限)。
- SQL 审计(记录所有 SQL 执行)。
- 性能监控(SQL 执行耗时统计)。
- 加密字段自动加解密。
- 自动填充(创建时间、更新时间等)。
6. 不适用场景与替代方案
- 简单的 SQL 改写(直接在 Mapper XML 中处理)。
- 复杂的跨数据源操作(使用分布式事务框架)。
- 替代方案:MyBatis-Plus(提供常用插件封装)。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 灵活性 | 无侵入扩展 | 动态代理有性能开销 |
| 解耦 | 插件逻辑与业务分离 | 多插件叠加调试困难 |
| 标准化 | 统一扩展机制 | 需理解四大核心对象 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 插件不生效 | 检查 @Intercepts 和 @Signature 注解是否正确 |
| 多次拦截 | 检查是否重复注册插件 |
| 插件顺序 | 使用 @Order 或实现 Ordered 接口排序 |
| 性能问题 | 避免在插件中执行耗时操作 |
9. 版本差异与实现边界
- MyBatis 3.x:插件机制稳定,四大拦截目标不变。
- MyBatis 3.5+:支持
default方法在 Interceptor 接口中。
10. 常见追问
追问 1:MyBatis 插件和 Spring AOP 的区别? 答:MyBatis 插件拦截的是 MyBatis 内部核心对象,Spring AOP 拦截的是 Spring Bean 方法;MyBatis 插件基于 JDK 动态代理,Spring AOP 支持 JDK 和 CGLIB。
追问 2:如何实现一个 MyBatis 插件? 答:实现
Interceptor接口,添加@Intercepts和@Signature注解,在intercept方法中编写增强逻辑。
11. 易错点
❌ 错误:MyBatis 插件可以拦截任意方法。
✅ 正确:只能拦截四大核心对象的指定方法。
❌ 错误:插件的
plugin()方法需要手动调用。✅ 正确:MyBatis 自动调用
plugin()方法,开发者只需返回Plugin.wrap()的结果。
一句话总结
MyBatis 插件基于 JDK 动态代理拦截四大核心对象的方法,通过 Interceptor 接口实现无侵入式扩展,是 MyBatis 扩展性的核心机制。
MyBatis一级缓存的作用域是哪里?默认是否开启?
原始问法:
- MyBatis一级缓存的作用域是哪里?默认是否开启?
来源题目:
SRC-11-114-401
面试先答
MyBatis 一级缓存的作用域是同一个 SqlSession,默认开启且无法关闭。在同一个 SqlSession 中,相同的查询(相同的 Mapper 方法、参数、分页等)只执行一次 SQL,后续查询直接从缓存中获取结果。一级缓存基于 PerpetualCache(内部使用 HashMap)实现,缓存键由 MappedStatement、参数、RowBounds 等组成。一级缓存在以下情况下清除:SqlSession 关闭、执行 INSERT/UPDATE/DELETE、手动调用 clearCache()。
核心结论
- 一级缓存作用域:同一个 SqlSession 内。
- 默认开启,无法关闭。
- 缓存基于
PerpetualCache(HashMap 实现)。 - 一级缓存是 MyBatis 默认行为,无需任何配置。
1. 是什么
一级缓存是 MyBatis 的本地缓存,在同一个 SqlSession 生命周期内有效。当同一个 SqlSession 执行相同的查询时,直接从缓存中返回结果,避免重复访问数据库。
缓存键的组成:
CacheKey = MappedStatement.getId()
+ 操作类型(QUERY/UPDATE)
+ 参数值
+ RowBounds(offset, limit)
+ 动态 SQL 的 SQL 字符串
一级缓存 vs 二级缓存对比:
| 属性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | Mapper namespace |
| 默认状态 | 开启(不可关闭) | 关闭(需配置) |
| 实现 | PerpetualCache(HashMap) | 可插拔(默认 PerpetualCache) |
| 生命周期 | SqlSession 级别 | 应用级别 |
| 共享性 | 不同 SqlSession 不共享 | 跨 SqlSession 共享 |
2. 为什么需要它
- 减少同一 SqlSession 内的重复数据库查询。
- 提升数据访问层性能。
- 与 Spring 事务集成时,同一事务内的查询共享一级缓存。
3. 底层原理与完整流程
一级缓存查询流程:
1. SqlSession.selectList() 调用
↓
2. Executor#query(ms, parameter, rowBounds)
↓
3. 创建 CacheKey(由 MappedStatement、参数、RowBounds 等组成)
↓
4. 查询本地缓存(localCache)
a. 命中 → 直接返回
b. 未命中 → 查询数据库
↓
5. 数据库查询 → 得到结果
↓
6. 写入本地缓存
↓
7. 返回结果
一级缓存失效时机:
以下操作会清除一级缓存:
1. SqlSession.close() — 关闭 SqlSession
2. SqlSession.clearCache() — 手动清除
3. 执行 INSERT/UPDATE/DELETE 操作
4. 调用 commit() 或 rollback()
核心代码实现:
// LocalCache 内部结构
public class LocalCache {
private final Map<Object, Object> cache = new HashMap<>(16);
public Object getObject(Object key) {
return cache.get(key); // HashMap 查询
}
public void putObject(Object key, Object value) {
cache.put(key, value); // HashMap 写入
}
public void clear() {
cache.clear(); // 清除缓存
}
}
// BaseExecutor#query
public <E> List<E> query(...) {
CacheKey key = createCacheKey(ms, parameter, rowBounds);
// 查询一级缓存
List<E> list = localCache.getObject(key);
if (list != null) {
return list; // 缓存命中
}
// 缓存未命中,查询数据库
list = doQuery(ms, parameter, rowBounds, resultHandler);
localCache.putObject(key, list); // 写入缓存
return list;
}
4. 怎么使用
// 一级缓存默认开启,无需配置
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 同一 SqlSession 内的相同查询
User user1 = mapper.selectById(1L); // 第一次:访问数据库
User user2 = mapper.selectById(1L); // 第二次:命中缓存
// user1 == user2(相同对象引用)
// 不同 SqlSession 之间不共享一级缓存
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user3 = mapper2.selectById(1L); // 访问数据库(非缓存命中)
sqlSession.close();
sqlSession2.close();
与 Spring 事务集成:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional // 开启事务
public void processUser() {
// 同一事务内,Spring 复用同一个 SqlSession
// 因此一级缓存在事务内有效
User user1 = userMapper.selectById(1L);
User user2 = userMapper.selectById(1L);
// 两次查询命中一级缓存
}
}
手动清除缓存:
// 手动清除一级缓存
sqlSession.clearCache();
// 注意:Spring 管理的 SqlSession 不要手动清除
// Spring 事务结束后会自动关闭 SqlSession
5. 适用场景
- 同一事务内的重复查询。
- Service 层方法中多次查询同一数据。
- 与 Spring 事务集成时,事务内共享一级缓存。
6. 不适用场景与替代方案
- 跨 SqlSession 的重复查询(使用二级缓存或 Redis)。
- 不同事务间的数据一致性要求高(一级缓存不保证跨事务一致性)。
- 分布式环境(使用 Redis 等分布式缓存)。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 性能 | 内存操作,极快 | 不同 SqlSession 不共享 |
| 一致性 | 同事务内一致 | 跨事务不一致 |
| 透明性 | 默认开启,无感知 | 调试时需注意缓存影响 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 一级缓存未命中 | 检查是否使用了同一 SqlSession/事务 |
| 一级缓存数据不一致 | 执行写操作后缓存自动清除 |
| 缓存穿透 | 对 null 结果也进行缓存 |
| 如何关闭一级缓存 | 无法关闭(MyBatis 强制开启) |
9. 版本差异与实现边界
- MyBatis 3.x:一级缓存行为稳定,基于 HashMap 实现。
- MyBatis 3.5+:一级缓存的键生成逻辑有微小优化。
- 注意:一级缓存在分布式环境下不保证一致性。
10. 常见追问
追问 1:如何验证一级缓存是否生效? 答:开启 MyBatis SQL 日志,观察同一 SqlSession 内相同查询是否只执行一次。
追问 2:一级缓存和二级缓存如何配合使用? 答:查询时先查二级缓存,再查一级缓存,最后查数据库;写入时同时更新两级缓存。
追问 3:Spring 事务对一级缓存的影响? 答:Spring 在同一事务内复用同一个 SqlSession,因此一级缓存在事务内有效;事务提交后 SqlSession 关闭,一级缓存清除。
11. 易错点
❌ 错误:一级缓存可以通过配置关闭。
✅ 正确:一级缓存默认开启且无法关闭,这是 MyBatis 的设计。
❌ 错误:一级缓存在所有 SqlSession 间共享。
✅ 正确:一级缓存在同一个 SqlSession 内有效,不同 SqlSession 之间不共享。
❌ 错误:一级缓存会在 SqlSession 关闭后仍然存在。
✅ 正确:SqlSession 关闭后一级缓存随之销毁。
一句话总结
MyBatis 一级缓存作用域为同一个 SqlSession,默认开启无法关闭,基于 HashMap 实现,与 Spring 事务集成时在事务内共享,有效减少重复数据库查询。
介绍一下Spring AI这个框架?
原始问法:
- 介绍一下Spring AI这个框架?
来源题目:
SRC-11-115-402
面试先答
Spring AI 是 Spring 官方推出的 AI 应用开发框架,旨在将大语言模型(LLM)集成到 Spring 应用中。它提供了统一的 API 抽象,支持多种 AI 模型(如 OpenAI ChatGPT、Azure OpenAI、Ollama、阿里云百炼等),支持 Chat(对话)、Embedding(向量嵌入)、RAG(检索增强生成)、Tool Calling(工具调用)等核心能力。开发者可以用熟悉的 Spring 编程模型(自动装配、依赖注入、注解)快速构建 AI 驱动的应用。Spring AI 的核心设计理念是"面向 AI 的 Spring",让 Java 开发者无需切换语言栈即可拥抱 AI 能力。
核心结论
- Spring AI 是 Spring 官方的 AI 应用开发框架。
- 提供统一的 API 抽象,支持多种 LLM 提供商。
- 核心能力:Chat、Embedding、RAG、Tool Calling、Agent。
1. 是什么
Spring AI 是 Spring 官方项目,旨在简化 Java 企业级应用集成 AI 能力的过程。它遵循 Spring 的设计哲学,提供声明式、自动装配、模板方法等开发模式。
核心模块:
| 模块 | 说明 |
|---|---|
spring-ai-core |
核心抽象(ChatClient、Prompt、Response) |
spring-ai-model-* |
各 AI 模型提供商适配 |
spring-ai-vector-store-* |
向量存储适配(Milvus、PgVector、Redis) |
spring-ai-mcp |
Model Context Protocol 支持 |
spring-ai-rag |
RAG(检索增强生成)支持 |
支持的 AI 模型:
| 提供商 | 依赖模块 |
|---|---|
| OpenAI | spring-ai-openai-spring-boot-starter |
| Azure OpenAI | spring-ai-azure-openai-spring-boot-starter |
| Ollama | spring-ai-ollama-spring-boot-starter |
| 阿里云百炼 | spring-ai-alibaba-starter |
| 百度千帆 | spring-ai-qianfan-spring-boot-starter |
| 月之暗面 | spring-ai-moonshot-spring-boot-starter |
2. 为什么需要它
- Java 企业级应用需要 AI 能力,但 Python 生态的 AI 框架难以集成。
- 不同 AI 提供商的 API 差异大,切换成本高。
- Spring AI 提供统一抽象,一次编码、多模型部署。
- 与 Spring Boot 无缝集成,享受自动装配、配置管理等便利。
3. 底层原理与完整流程
核心架构:
┌─────────────────────────────────────┐
│ Spring AI App │
│ (ChatClient, VectorStore, Agent) │
├─────────────────────────────────────┤
│ Spring AI Core API │
│ (ChatModel, Prompt, Embedding) │
├─────────────────────────────────────┤
│ AI Provider Adapters │
│ (OpenAI, Ollama, Azure, ...) │
├─────────────────────────────────────┤
│ Vector Store Adapters │
│ (Milvus, PgVector, Redis, ...) │
└─────────────────────────────────────┘
Chat 调用流程:
1. 创建 ChatClient
2. 构建 Prompt(系统消息、用户消息、上下文)
3. 调用 chatClient.call(prompt)
4. Spring AI 根据配置选择 AI 提供商
5. 适配层转换为提供商 API
6. 发送 HTTP 请求到 AI 服务
7. 解析响应,返回 AI 回复
RAG 流程:
1. 文档加载 → DocumentReader
2. 文本切分 → DocumentSplitter
3. 向量化 → EmbeddingModel
4. 存储到向量数据库 → VectorStore
5. 用户提问 → 向量化
6. 相似文档检索 → VectorStore.similaritySearch()
7. 构建 Prompt(问题 + 检索结果)
8. 调用 AI 生成回答
4. 怎么使用
快速开始:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
# application.yml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
model: gpt-4
Chat 对话:
@Service
public class AIService {
private final ChatClient chatClient;
public AIService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String chat(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
public String chatWithSystem(String question) {
return chatClient.prompt()
.system("你是一个专业的Java技术顾问")
.user(question)
.call()
.content();
}
}
流式对话:
public Flux<String> streamChat(String question) {
return chatClient.prompt()
.user(question)
.stream()
.content();
}
RAG 检索增强:
@Service
public class RAGService {
private final VectorStore vectorStore;
private final ChatClient chatClient;
public RAGService(VectorStore vectorStore, ChatClient.Builder builder) {
this.vectorStore = vectorStore;
this.chatClient = builder.build();
}
public String query(String question) {
// 1. 检索相关文档
List<Document> docs = vectorStore.similaritySearch(
Search.query(question).withTopK(3));
// 2. 构建上下文
String context = docs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n"));
// 3. 生成回答
return chatClient.prompt()
.system("基于以下上下文回答问题,如果上下文没有相关信息,请说"我不知道"")
.user("上下文:" + context + "\n问题:" + question)
.call()
.content();
}
}
5. 适用场景
- 智能客服和对话机器人。
- 文档问答系统(RAG)。
- 代码生成和辅助编程。
- 数据分析和报告生成。
- 企业知识管理系统。
6. 不适用场景与替代方案
- 非 Java 技术栈(使用 LangChain、Haystack 等 Python 框架)。
- 对 AI 延迟极致敏感(需考虑本地部署模型)。
- 简单的 AI 调用(可直接使用各 AI 提供商 SDK)。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 统一性 | 一套 API 多模型 | 抽象层可能增加调试难度 |
| 生态 | 与 Spring 无缝集成 | 社区相对年轻 |
| 灵活性 | 支持多种向量数据库 | 性能略低于直接 SDK 调用 |
| 学习曲线 | Spring 开发者友好 | 需理解 AI 基本概念 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| API Key 配置 | 使用环境变量或配置文件,不要硬编码 |
| 模型切换 | 只需更换依赖和配置,业务代码不变 |
| 超时处理 | 配置 timeout 和 retry 参数 |
| Token 限制 | 使用 DocumentSplitter 合理切分文本 |
9. 版本差异与实现边界
- Spring AI 1.0.0-M1(里程碑版本):核心 API 已稳定。
- Spring AI 1.0.0-RC1(候选发布版):API 基本定型。
- 注意:Spring AI 仍在快速迭代中,API 可能有变化。
10. 常见追问
追问 1:Spring AI 和 LangChain 的区别? 答:Spring AI 是 Java 生态的框架,与 Spring 无缝集成;LangChain 是 Python 生态的框架,更成熟、功能更丰富。
追问 2:如何选择 AI 模型提供商? 答:根据需求(延迟、成本、模型能力)选择,Spring AI 支持随时切换。
追问 3:向量数据库如何选择? 答:Milvus(大规模)、PgVector(PostgreSQL 集成)、Redis(简单场景)。
11. 易错点
❌ 错误:Spring AI 只能使用 OpenAI。
✅ 正确:Spring AI 支持多种 AI 提供商,可灵活切换。
❌ 错误:Spring AI 会自动处理所有 AI 相关问题。
✅ 正确:Spring AI 提供框架级支持,具体业务逻辑仍需开发者实现。
一句话总结
Spring AI 是 Spring 官方的 AI 开发框架,提供统一的 API 抽象,支持多种 LLM 和向量数据库,让 Java 开发者以熟悉的 Spring 编程模型快速构建 AI 应用。
用Spring AI写一个Agent的过程大概是什么样的?
原始问法:
- 用Spring AI写一个Agent的过程大概是什么样的?
来源题目:
SRC-11-115-403
面试先答
使用 Spring AI 开发一个 Agent 主要分为六步:1. 添加依赖(Spring AI 核心 + AI 模型提供商 + 向量数据库);2. 配置 AI 模型和向量存储(API Key、模型名称等);3. 创建知识文档(准备 Agent 能回答的领域知识);4. 实现文档向量化(文档切分→Embedding→存储到向量库);5. 实现 Agent 服务(接收问题→检索相关文档→调用 AI 生成回答);6. 提供 API 接口暴露 Agent 能力。可选增强:添加 Tool Calling(工具调用)、Memory(多轮对话记忆)、MCP(模型上下文协议)等。
核心结论
- Agent 开发六步:依赖→配置→文档→向量化→服务→API。
- 核心链路:用户提问→向量检索→构建 Prompt→AI 生成回答。
- 可扩展:Tool Calling、Memory、MCP、多 Agent 协作。
1. 是什么
Agent 是一个能够自主理解、规划、执行任务的 AI 程序。Spring AI 提供了构建 Agent 的核心能力:
| 能力 | 说明 | Spring AI 支持 |
|---|---|---|
| Chat | 自然语言对话 | ChatClient |
| RAG | 检索增强生成 | VectorStore + EmbeddingModel |
| Tool Calling | 调用外部工具 | FunctionToolCallback |
| Memory | 多轮对话记忆 | ChatMemoryRepository |
| Planning | 任务规划 | Agent 接口 |
| MCP | 模型上下文协议 | spring-ai-mcp |
2. 为什么需要它
- 让 AI Agent 具备领域知识(通过 RAG)。
- 让 AI Agent 能调用外部工具(通过 Tool Calling)。
- 让 AI Agent 具备多轮对话能力(通过 Memory)。
- 构建生产级 AI 应用的标准流程。
3. 底层原理与完整流程
Agent 核心架构:
用户请求
↓
Agent 接收请求
↓
┌─────────────────────────────┐
│ 1. Memory 查询历史上下文 │
│ 2. RAG 检索相关知识 │
│ 3. 判断是否需要调用工具 │
│ 4. 构建完整 Prompt │
│ 5. 调用 AI 模型生成回答 │
│ 6. 解析工具调用请求(可选) │
│ 7. 执行工具调用(可选) │
│ 8. 将结果加入上下文 │
│ 9. 返回最终回答 │
└─────────────────────────────┘
↓
返回给用户
完整开发流程:
步骤 1:添加 Maven 依赖
↓
步骤 2:配置 AI 模型和向量数据库
↓
步骤 3:准备领域知识文档
↓
步骤 4:实现文档向量化(索引构建)
↓
步骤 5:实现 Agent 服务(核心逻辑)
↓
步骤 6:提供 REST API 接口
↓
步骤 7:增强(Tool Calling、Memory 等)
4. 怎么使用
步骤一:添加依赖
<dependencies>
<!-- Spring AI 核心 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- 向量数据库(以 Milvus 为例) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
</dependency>
</dependencies>
步骤二:配置
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
model: gpt-4
vectorstore:
milvus:
client:
host: localhost
port: 19530
collection-name: agent_knowledge
embedding-dimension: 1536
步骤三:文档向量化(索引构建)
@Service
public class DocumentIngestionService {
private final VectorStore vectorStore;
private final DocumentReader documentReader;
public DocumentIngestionService(VectorStore vectorStore,
@Value("classpath:/docs") DocumentReader documentReader) {
this.vectorStore = vectorStore;
this.documentReader = documentReader;
}
public void ingestDocuments() {
// 1. 读取文档
List<Document> documents = documentReader.get();
// 2. 文档切分(默认按 1000 字符切分)
// Spring AI 自动处理切分和向量化
// 3. 存储到向量数据库(自动向量化)
vectorStore.add(documents);
System.out.println("已导入 " + documents.size() + " 个文档片段");
}
}
步骤四:实现 Agent 服务
@Service
public class KnowledgeAgent {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public KnowledgeAgent(ChatClient.Builder builder, VectorStore vectorStore) {
this.chatClient = builder.build();
this.vectorStore = vectorStore;
}
/**
* Agent 核心方法:接收问题,返回回答
*/
public String ask(String question) {
// 1. 从向量库检索相关知识
List<Document> relevantDocs = vectorStore.similaritySearch(
Search.query(question)
.withTopK(3)
.withSimilarityThreshold(0.7));
// 2. 构建上下文
String context = relevantDocs.stream()
.map(doc -> "参考资料:" + doc.getContent())
.collect(Collectors.joining("\n\n"));
// 3. 构建 Prompt 并调用 AI
String systemPrompt = """
你是一个专业的知识问答助手。
请基于以下参考资料回答用户的问题。
如果参考资料中没有相关信息,请回答"抱歉,我没有找到相关信息"。
""";
return chatClient.prompt()
.system(systemPrompt)
.user("参考资料:\n" + context + "\n\n问题:" + question)
.call()
.content();
}
}
步骤五:提供 API 接口
@RestController
@RequestMapping("/api/agent")
public class AgentController {
private final KnowledgeAgent knowledgeAgent;
public AgentController(KnowledgeAgent knowledgeAgent) {
this.knowledgeAgent = knowledgeAgent;
}
@PostMapping("/ask")
public Map<String, String> ask(@RequestBody Map<String, String> request) {
String question = request.get("question");
String answer = knowledgeAgent.ask(question);
return Map.of("question", question, "answer", answer);
}
}
步骤六:添加 Tool Calling(可选)
@Component
public class WeatherTool {
@Tool(description = "获取指定城市的天气信息")
public String getWeather(@ToolParam(description = "城市名称") String city) {
// 调用天气 API
return city + "今天晴,温度25°C";
}
}
// Agent 使用 Tool
@Service
public class ToolAgent {
private final ChatClient chatClient;
private final WeatherTool weatherTool;
public ToolAgent(ChatClient.Builder builder, WeatherTool weatherTool) {
this.chatClient = builder
.tools(weatherTool) // 注册工具
.build();
}
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
// AI 会自动判断是否需要调用天气工具
}
}
5. 适用场景
- 企业知识库问答系统。
- 智能客服和售前支持。
- 代码辅助(Code Review、代码生成)。
- 数据分析助手。
- 文档解析和摘要生成。
6. 不适用场景与替代方案
- 需要强确定性的业务流程(Agent 可能不稳定)。
- 对 AI 幻觉零容忍的场景(需额外校验)。
- 替代方案:LangChain(Python 生态)、AutoGen(多 Agent 框架)。
7. 优缺点与技术取舍
| 维度 | 优点 | 缺点 |
|---|---|---|
| 开发效率 | Spring 编程模型,快速上手 | 需理解 AI 概念 |
| 灵活性 | 支持多种模型和向量库 | AI 输出有不确定性 |
| 扩展性 | Tool Calling、MCP | 调试相对困难 |
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| AI 回答不准确 | 优化知识文档、调整相似度阈值、增加检索数量 |
| AI 幻觉 | 添加"找不到就说不知道"的系统提示 |
| 响应慢 | 使用流式输出、减少检索文档数量 |
| 上下文过长 | 合理切分文档、控制 Prompt 长度 |
9. 版本差异与实现边界
- Spring AI 1.0.0 提供稳定 API。
- Spring AI 与 Spring Boot 3.x 深度集成。
- Agent 能力仍在积极发展中。
10. 常见追问
追问 1:RAG 和 Fine-tuning 的区别? 答:RAG 是检索增强,不改模型参数,适合知识更新频繁的场景;Fine-tuning 是微调模型参数,适合特定风格和格式的输出。
追问 2:如何评估 Agent 的效果? 答:从准确性(回答是否正确)、相关性(回答是否切题)、完整性(是否覆盖关键点)、时效性(响应速度)四个维度评估。
追问 3:Agent 的 Tool Calling 如何实现? 答:通过
@Tool注解标注方法,Spring AI 在运行时将函数描述传递给 AI,AI 返回函数调用请求后自动执行。
11. 易错点
❌ 错误:Agent 能 100% 准确回答所有问题。
✅ 正确:Agent 基于概率生成,存在不确定性,需通过 RAG 和 Prompt Engineering 提高准确性。
❌ 错误:知识文档越多,Agent 效果越好。
✅ 正确:过多的无关文档会干扰 AI 回答,需精心筛选和组织知识。
一句话总结
Spring AI 构建 Agent 的核心流程是:依赖→配置→文档→向量化→RAG 检索→AI 生成→API 暴露,通过 RAG 注入领域知识、Tool Calling 扩展能力、Memory 实现多轮对话,快速构建生产级 AI 应用。