目录

11-Spring

发表于
6 160.8~206.8 分钟 72364

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 流程

  1. 加载配置(XML 或注解),通过 BeanDefinitionReader 解析为 BeanDefinition
  2. BeanFactory 收到获取 Bean 请求时,先从缓存(singletonObjects)中查找。
  3. 缓存未命中则创建 Bean:实例化→属性填充→初始化→放入单例缓存。
  4. 依赖注入通过反射(setter 或构造器)完成。

AOP 流程

  1. 容器启动时扫描切面类,解析切点表达式。
  2. 为符合切点的 Bean 创建代理对象(JDK 动态代理或 CGLIB)。
  3. 方法调用时,代理对象拦截调用,按顺序织入通知(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 是一种对象创建和依赖管理的设计模式。核心思想是"你不要创建对象,我来创建并注入给你"。

控制反转的两层含义

  1. 控制权反转:对象的创建和销毁不再由开发者代码控制,而是交给容器。
  2. 依赖注入(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 时:

  1. 创建 A,实例化后放入三级缓存
  2. 发现 B,创建 B,B 从三级缓存获取 A 的早期引用
  3. B 初始化完成后放入一级缓存
  4. 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. 底层原理与完整流程

代理方式选择

  1. JDK 动态代理:目标类实现接口时默认使用。基于 java.lang.reflect.Proxy,生成实现相同接口的代理类。
  2. 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-injectionApplicationContext.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 接口、BeanPostProcessorInitializingBeanDisposableBean
  • 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@PostConstructInitializingBean 的执行顺序? 答:@PostConstructInitializingBean#afterPropertiesSetinit-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 作用域,RequestScopeSessionScope 等实现了该接口,将 Bean 存储在 HttpServletRequestHttpSession 中。

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 注解、ObjectProviderApplicationContext.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 作用域
多个变量需要一致性 使用 synchronizedReentrantLock
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 支持 nametype 属性指定。@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+)。
  • 避免使用 NEVERNOT_SUPPORTED,除非非常确定需要非事务执行。
7. 优缺点与技术取舍
传播行为 优点 缺点
REQUIRED 简单、默认选择 子方法失败会影响主事务
REQUIRES_NEW 独立事务,互不影响 资源消耗大、性能开销
NESTED 支持部分回滚 依赖数据库保存点支持
MANDATORY 强制事务保护 调用方必须提供事务
8. 常见问题及解决方案
问题 解决方案
事务不生效 检查方法是否为 public、是否在同类内部调用、是否有异常
嵌套事务回滚导致全部回滚 使用 REQUIRES_NEWNESTED 隔离事务
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
  • 默认只回滚 RuntimeExceptionError,不回滚受检异常。
  • 常见失效场景:非 public、同类调用、异常未抛出、引擎不支持。
1. 是什么

@Transactional 是 Spring 提供的声明式事务管理注解,可标注在方法或类上,表示该方法/类的所有方法需要事务支持。

属性说明

属性 说明 默认值
propagation 事务传播行为 REQUIRED
isolation 事务隔离级别 DEFAULT
timeout 事务超时时间 -1(不超时)
readOnly 是否只读 false
rollbackFor 指定回滚的异常类 RuntimeException
noRollbackFor 指定不回滚的异常类
2. 为什么需要它
  • 声明式事务使业务代码与事务逻辑解耦。
  • 避免手动管理事务的繁琐代码(begincommitrollback)。
  • 统一事务管理策略,便于维护。
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+ 支持 @TransactionalrollbackForClassName 属性。
  • 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 默认回滚所有异常。

  • ✅ 正确:默认只回滚 RuntimeExceptionError,受检异常需配置 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@EnableAutoConfigurationAutoConfigurationImportSelector → 读取 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 分析条件评估结果
排除不需要的自动配置 使用 excludespring.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,控制台会输出条件评估报告。

  • 追问 3spring.factoriesAutoConfiguration.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. 常见追问
  • 追问 1DispatcherServletServlet 的关系? 答:DispatcherServlet 继承自 HttpServlet,是 Spring MVC 的前端控制器。

  • 追问 2HandlerMappingHandlerAdapter 的区别? 答: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 或释放占用端口的进程
请求超时 调整 connectionTimeoutasyncTimeout
大文件上传失败 配置 max-swallow-sizemax-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,javaxjakarta 命名空间。
  • 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 配置 使用环境变量或配置文件,不要硬编码
模型切换 只需更换依赖和配置,业务代码不变
超时处理 配置 timeoutretry 参数
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 应用。


推荐文章

02-Java集合
19-HR与软技能
18-Git
上一篇 10-微服务