# Spring * [Spring](#spring) * [一、IoC 与 Bean](#一ioc-与-bean) * [IoC 与依赖注入](#ioc-与依赖注入) * [Bean 的生命周期](#bean-的生命周期) * [Bean 的作用域与线程安全](#bean-的作用域与线程安全) * [循环依赖](#循环依赖) * [二、AOP](#二aop) * [动态代理](#动态代理) * [自调用](#自调用) * [三、事务](#三事务) * [声明式事务](#声明式事务) * [传播行为](#传播行为) * [事务失效](#事务失效) * [四、Spring Boot 自动配置](#四spring-boot-自动配置) * [五、Spring MVC](#五spring-mvc) * [请求的处理过程](#请求的处理过程) * [六、MyBatis](#六mybatis) * [#{} 和 ${} 的区别](#-和--的区别) * [Mapper 接口为什么不需要实现类](#mapper-接口为什么不需要实现类) * [动态 SQL](#动态-sql) * [缓存](#缓存) * [七、Spring 中的设计模式](#七spring-中的设计模式) * [附录:实测](#附录实测) * [A1 单例 Bean 的成员变量](#a1-单例-bean-的成员变量) * [A2 锁与事务的先后](#a2-锁与事务的先后) * [A3 Bean 创建的调用栈](#a3-bean-创建的调用栈) * [参考资料](#参考资料) ## 一、IoC 与 Bean ### IoC 与依赖注入 控制反转(Inversion of Control, IoC)指的是对象不再自己创建所依赖的对象,而是由容器创建好之后交给它。例如 OrderService 需要 OrderRepository,它不在内部 new 一个,而是声明自己需要,由 Spring 容器把创建好的对象传进来,这个过程称为依赖注入(Dependency Injection, DI)。 ```java @Service public class OrderService { private final OrderRepository repository; public OrderService(OrderRepository repository) { // 构造器注入 this.repository = repository; } } ``` 这样做的好处: - 对象只依赖接口,替换实现、在测试中注入模拟对象都不需要修改代码; - 对象的创建、作用域和生命周期由容器统一管理,事务、缓存这类功能也是容器在创建对象时织入的。 容器通过注解知道要创建哪些对象:类上标注 @Component 或者它的派生注解 @Service、@Repository、@Controller,启动时由组件扫描发现;第三方库中的类无法修改源码,就在 @Configuration 类中写一个返回该对象的方法,并标注 @Bean。 依赖注入有构造器注入、Setter 注入和字段注入(把 @Autowired 加在字段上)三种方式。Spring 官方推荐构造器注入:依赖可以声明为 final,对象创建出来就是完整可用的,缺少依赖时启动就会失败。 @Autowired 和 @Resource 都能注入依赖,区别在于查找的顺序: - @Autowired 是 Spring 的注解,先按类型查找,找到多个时再按字段名或参数名匹配,仍然无法确定就报错,可以用 @Qualifier 指定 Bean 的名称,或者给其中一个 Bean 加 @Primary; - @Resource 是 Jakarta EE(原 JSR-250)的注解,指定了 name 时按名称查找,否则先按字段名查找,找不到再按类型查找。 例如容器中有两个 PayService 类型的 Bean,名称分别是 alipay 和 wechat: ```java @Resource PayService wechat; // 注入名称为 wechat 的 Bean @Autowired PayService alipay; // 按类型找到两个,再按字段名匹配到 alipay @Autowired PayService payService; // 字段名匹配不上,启动失败 ``` 第三行启动时报错 NoUniqueBeanDefinitionException: expected single matching bean but found 2: alipay,wechat。 ### Bean 的生命周期 容器启动时,AbstractApplicationContext 的 refresh() 分两步创建 Bean。第一步解析配置:扫描 @Component,解析 @Bean 方法和自动配置类,把每个 Bean 的类名、作用域、依赖等信息保存为 BeanDefinition,这时还没有创建任何对象。这一步由 ConfigurationClassPostProcessor 完成,它是一个 BeanFactoryPostProcessor,这类扩展点在创建对象之前读取或修改 BeanDefinition。 第二步在 finishBeanFactoryInitialization() 中,按 BeanDefinition 依次创建所有非懒加载的单例,创建过程依靠反射。在一个 Bean 的构造函数和带 @Autowired 的 Setter 方法中打印调用栈,按调用顺序整理如下(Spring Boot 3.3 实测,见[附录 A3](#a3-bean-创建的调用栈),最后一行 initializeBean 来自源码): ```text AbstractApplicationContext.refresh() └─ finishBeanFactoryInitialization() → preInstantiateSingletons() 遍历 BeanDefinition └─ getBean() → doGetBean() 先查单例缓存,没有再创建 └─ createBean() → doCreateBean() ├─ createBeanInstance() → Constructor.newInstance() 实例化:反射调用构造函数 ├─ populateBean() → AutowiredAnnotationBeanPostProcessor │ .postProcessProperties() → Method.invoke() 属性填充:反射调用 Setter └─ initializeBean() Aware 回调、初始化、AOP 代理 ``` @Autowired 加在字段上时,同样由 postProcessProperties() 处理,区别是用 Field.set() 反射赋值。 一个单例 Bean 从创建到销毁会经过以下步骤: 1. 实例化:调用构造函数创建对象; 2. 属性填充:注入依赖; 3. Aware 回调:例如 BeanNameAware 把 Bean 的名字传给它; 4. BeanPostProcessor 的 postProcessBeforeInitialization(); 5. 初始化:依次调用 @PostConstruct 方法、InitializingBean 的 afterPropertiesSet() 和配置的 init-method; 6. BeanPostProcessor 的 postProcessAfterInitialization(),AOP 就是在这一步把 Bean 替换为代理对象的; 7. 容器关闭时销毁:依次调用 @PreDestroy 方法、DisposableBean 的 destroy() 和配置的 destroy-method。 让一个 Bean 实现上面所有的回调,实际运行打印的顺序如下(Spring Boot 3.3): ```text 1 构造函数 2 依赖注入 3 BeanNameAware: orderService 4 BeanPostProcessor before 5 @PostConstruct 6 afterPropertiesSet 7 init-method 8 BeanPostProcessor after 9 @PreDestroy 10 DisposableBean.destroy 11 destroy-method ``` ### Bean 的作用域与线程安全 Bean 默认是单例(singleton)的,整个容器中只有一个对象,所有请求共用它。其它常用的作用域: - prototype:每次从容器获取都创建一个新对象,容器创建之后就不再管理它,不会调用它的销毁方法; - request、session:每个 HTTP 请求或会话一个对象,只能在 Web 应用中使用。 多个线程同时使用一个单例 Bean 时,Spring 不做任何同步,它是否线程安全取决于有没有可变的成员变量。Controller、Service 通常只持有其他 Bean 的引用,不保存与请求有关的数据,所以是安全的;一旦把请求中的数据存到成员变量里,不同请求就会互相覆盖。例如: ```java @RestController public class CounterController { private int count = 0; // 所有请求共享 @GetMapping("/count") public int count() { return ++count; } // ++ 不是原子操作 } ``` 例如用 64 个线程并发请求 1 万次,多次运行中每轮丢失的更新从 0 次到 67 次不等(Spring Boot 3.3 实测,见[附录 A1](#a1-单例-bean-的成员变量))。是否丢失、丢失多少取决于线程执行的时序,这类问题在测试中很难稳定复现。解决方法是不在单例中保存状态,把数据放在方法的局部变量中;确实需要共享时使用 AtomicInteger 等线程安全的类,每个线程各自的数据放在 ThreadLocal 中。 需要注意的是,把 prototype 的 Bean 注入到单例中,只会在创建单例时注入一次,之后每次使用的都是同一个对象。需要每次得到新对象时,注入 ObjectProvider 并在使用时调用 getObject()。 ### 循环依赖 循环依赖指的是两个 Bean 互相依赖,例如 A 依赖 B,B 又依赖 A。 对于通过字段或者 Setter 注入的单例 Bean,Spring 通过提前暴露还没有初始化完成的对象来解决。DefaultSingletonBeanRegistry 中有三个缓存: - 一级缓存 singletonObjects:初始化完成的 Bean; - 二级缓存 earlySingletonObjects:提前暴露的、还没有初始化完成的 Bean; - 三级缓存 singletonFactories:用于生成提前引用的工厂。

需要三级缓存而不是两级,是因为 A 可能需要被 AOP 代理,而代理正常是在初始化的最后一步才创建的。三级缓存中的工厂被调用时,会判断 A 是否需要代理,需要的话提前创建代理返回给 B,之后初始化完成时不再重复创建。这样 B 拿到的对象与最终放入容器的对象是同一个;没有循环依赖时工厂不会被调用,代理仍按正常流程创建。 以下情况无法解决,启动时抛出 BeanCurrentlyInCreationException: - 构造器注入的循环依赖:创建 A 时必须先有 B,A 还没有实例化,没有可以提前暴露的对象; - 原型(prototype)作用域的 Bean:每次获取都创建新对象,不会放入缓存。 需要注意的是,Spring Boot 2.6 起默认禁止循环依赖,出现时直接启动失败,设置 spring.main.allow-circular-references=true 才能恢复。循环依赖通常说明职责划分有问题,更好的做法是调整依赖关系。 ## 二、AOP ### 动态代理 面向切面编程(Aspect-Oriented Programming, AOP)用于把日志、事务、权限校验这类分散在各处的逻辑从业务代码中抽离出来。Spring AOP 基于动态代理实现:容器中保存的不是原始对象,而是代理对象,调用方法时先执行代理中的切面逻辑,再调用原始对象的方法。 两种代理方式: - JDK 动态代理:目标类实现了接口时可以使用,代理类实现同样的接口,只能代理接口中的方法; - CGLIB:在运行时生成目标类的子类,通过重写方法来代理,因此 final 类、final 方法和 private 方法无法被代理。 Spring Framework 在目标类实现了接口时默认使用 JDK 动态代理;Spring Boot 默认使用 CGLIB,可以通过 spring.aop.proxy-target-class 修改。 JDK 动态代理的原理可以用下面的代码说明,切面逻辑写在 InvocationHandler 中: ```java public interface OrderService { void create(); } OrderService target = () -> System.out.println("创建订单"); OrderService proxy = (OrderService) Proxy.newProxyInstance( OrderService.class.getClassLoader(), new Class[]{OrderService.class}, (p, method, args) -> { System.out.println("开启事务"); Object result = method.invoke(target, args); System.out.println("提交事务"); return result; }); proxy.create(); ``` ```text 开启事务 创建订单 提交事务 ``` 生成的代理类 jdk.proxy1.$Proxy0 的父类是 java.lang.reflect.Proxy。Java 不支持多继承,代理类已经继承了 Proxy,就只能通过实现接口与目标类关联,这就是 JDK 动态代理只能代理接口的原因,没有实现接口的类要用 CGLIB 生成子类(JDK 21 实测)。 动态代理与反射是两个概念:反射指的是在运行时获取类的信息、调用方法和读写字段,例如 method.invoke(target, args);动态代理指的是在运行时生成新的类,例如上面的 Proxy.newProxyInstance() 生成了一个实现 OrderService 接口的类。JDK 动态代理依赖反射:代理类把每次调用都交给 InvocationHandler,传入表示被调用方法的 Method 对象,再由 method.invoke() 调用目标对象。 实际开发中不需要自己创建代理,用 @Aspect 声明切面,Spring 在创建 Bean 时生成代理。常见的做法是自定义一个注解,让切面拦截带有这个注解的方法。例如给下单方法加分布式锁: ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DistributedLock { String prefix(); // 锁的 key 为 prefix 加第一个参数 } @Aspect @Component @Order(1) // 比事务切面先执行,锁包在事务外面 public class DistributedLockAspect { private final StringRedisTemplate redis; // 构造器注入,省略构造函数 @Around("@annotation(lock)") // 拦截带有 @DistributedLock 的方法 public Object around(ProceedingJoinPoint pjp, DistributedLock lock) throws Throwable { String key = lock.prefix() + pjp.getArgs()[0]; String value = UUID.randomUUID().toString(); while (!Boolean.TRUE.equals(redis.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(30)))) { Thread.onSpinWait(); // 简化:自旋重试 } try { return pjp.proceed(); // 调用原方法 } finally { redis.execute(UNLOCK, List.of(key), value); // 是自己的锁才删除 } } } @Service public class OrderService { @Transactional @DistributedLock(prefix = "lock:order:user:") public boolean createOrder(int userId, int skuId) { // 查询这个用户是否已经买过这个商品,没有就插入订单,完整代码见附录 A2 } } ``` @Around 中的 @annotation(lock) 表示拦截所有带 @DistributedLock 的方法,并把注解对象作为参数 lock 传入;pjp.proceed() 调用原方法。UNLOCK 是先比较再删除的 Lua 脚本,原理见 [Redis 的 SET NX 指令](分布式.md#redis-的-set-nx-指令)。记录日志、限流、幂等校验也都是这种写法。 需要注意的是,createOrder() 上同时有事务和锁两个切面,它们的先后决定了锁是否包住事务。锁必须在外层:如果事务在外层,锁释放之后事务才提交,这期间另一个请求拿到锁,查不到还没有提交的订单,就会再创建一个。20 个请求同时为同一个用户下单,锁在事务外时 10 轮都只创建了 1 个订单;锁在事务内时 10 轮中有 8 轮创建了 2 个(Spring Boot 3.3、MySQL 8.0 实测,见[附录 A2](#a2-锁与事务的先后))。 两个切面都没有指定顺序时,执行的先后是不确定的。@Order 的值越小越先执行、越靠外层,事务切面的默认值是 Ordered.LOWEST_PRECEDENCE,所以上面给锁的切面加上了 @Order(1)。 ### 自调用 代理只能拦截从外部经过代理的调用。在目标对象内部调用自己的另一个方法时,等价于 this.inner(),调用的是原始对象,不经过代理,切面不会生效:

```java @Service public class OrderService { public void outer() { inner(); // 自调用,inner() 上的事务不生效 } @Transactional public void inner() { } } ``` 这是 @Transactional、@Async、@Cacheable 等注解失效最常见的原因。解决方法是把被调用的方法移到另一个 Bean 中,或者注入自身的代理,通过代理调用。 ## 三、事务 ### 声明式事务 @Transactional 同样基于 AOP 实现:代理在方法执行前开启事务,方法正常返回时提交,抛出异常时回滚。 需要注意的是,默认只有 RuntimeException 和 Error 会触发回滚,受检异常(例如 IOException)不会回滚,需要通过 rollbackFor 指定。 除了声明式事务,还可以用 TransactionTemplate 编程式地使用事务,事务的范围就是传入的回调: ```java transactionTemplate.execute(status -> { jdbc.update("UPDATE goods SET stock = stock - 1 WHERE id = ?", 1001); jdbc.update("INSERT INTO orders (user_id, goods_id) VALUES (?, ?)", 1024, 1001); if (riskCheckFailed) { status.setRollbackOnly(); // 不抛异常也能回滚 } return null; }); ``` 回调正常返回时提交,抛出异常或者调用了 setRollbackOnly() 时回滚。它不依赖代理,不会因为自调用而失效,事务的范围也可以只是方法中的一段,适合只有一小段操作需要事务、或者要按条件决定回滚的场景,其余情况用 @Transactional 更简洁。 ### 传播行为 传播行为指的是一个事务方法调用另一个事务方法时,事务如何传递。常用的有以下三种: - REQUIRED:默认值,当前有事务就加入,没有就新建。内外两个方法属于同一个事务,一起提交或者一起回滚; - REQUIRES_NEW:挂起当前事务,新建一个独立的事务,两个事务分别提交和回滚,例如无论业务成功与否都要保存的操作日志; - NESTED:在当前事务中创建一个保存点,内层回滚时只回滚到保存点,外层回滚时内层也一起回滚。 REQUIRED 有一个常见的问题:内层方法抛出异常后,即使外层捕获了这个异常,整个事务也已经被标记为只能回滚,外层方法结束时事务回滚,并抛出 UnexpectedRollbackException。 ### 事务失效 事务不生效的常见原因: - 自调用:同一个类中的方法互相调用,不经过代理; - 方法无法被代理:final 方法、static 方法和 private 方法;Spring 6.0 之前,非 public 方法上的注解也会被忽略; - 异常被吞掉:方法内部捕获了异常而没有重新抛出,代理认为方法正常结束,提交了事务; - 抛出的是受检异常,而没有配置 rollbackFor; - 对象不是由 Spring 管理的,例如自己 new 出来的对象; - 在新线程中操作数据库:事务绑定在当前线程上,其它线程中的操作不属于这个事务; - 数据库不支持事务,例如 MySQL 的 MyISAM 引擎。 ## 四、Spring Boot 自动配置 Spring Boot 在 Spring 的基础上提供了自动配置和 starter:starter 把一组常用的依赖打包在一起,例如 spring-boot-starter-web 引入了 Spring MVC、内嵌的 Tomcat 和 JSON 库;自动配置则根据引入的依赖自动创建所需的 Bean,不需要手动配置就能启动一个 Web 应用。 Spring Boot 并不是新的框架,容器、AOP、事务仍然是 Spring 提供的。除了 starter 和自动配置,它还能把应用连同内嵌的服务器打包成一个 jar,用 java -jar 直接运行,不再需要把 war 包部署到单独安装的 Tomcat 中。 自动配置的过程: 1. 启动类上的 @SpringBootApplication 组合了三个注解:@SpringBootConfiguration(相当于 @Configuration,启动类中也可以定义 @Bean)、@ComponentScan(扫描启动类所在的包及其子包)和 @EnableAutoConfiguration(开启自动配置); 2. 启动时读取所有 jar 包中的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,得到自动配置类的列表,Spring Boot 2.7 之前写在 META-INF/spring.factories 中; 3. 每个自动配置类都带有条件注解,条件满足时才生效。例如 @ConditionalOnClass 要求类路径上存在某个类,@ConditionalOnMissingBean 要求用户没有定义同类型的 Bean,@ConditionalOnProperty 根据配置项决定。 例如引入 spring-boot-starter-jdbc 和数据库驱动后,数据源的自动配置类就会生效,按配置文件中的地址创建一个 DataSource;如果用户自己定义了 DataSource,由于 @ConditionalOnMissingBean,自动配置不再创建。因此用户的配置总是优先于自动配置。 自定义 starter 也是同样的做法。例如把公司内部的短信客户端做成 starter,自动配置类如下: ```java @AutoConfiguration @EnableConfigurationProperties(SmsProperties.class) public class SmsAutoConfiguration { @Bean @ConditionalOnMissingBean // 用户自己定义了 SmsClient 时不再创建 public SmsClient smsClient(SmsProperties props) { return new SmsClient(props.sign()); } } @ConfigurationProperties("sms") // 绑定配置文件中 sms.* 的配置 public record SmsProperties(String sign) {} ``` 再在 jar 包的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中写上 SmsAutoConfiguration 的全名。其它项目引入这个 jar、配置 sms.sign 之后,就能直接注入 SmsClient;自己定义了 SmsClient 时,注入的是自己定义的那个。 需要注意的是,自动配置类要放在不会被组件扫描到的包中,只通过 imports 文件加载。自动配置类在用户定义的 Bean 都注册之后才处理,@ConditionalOnMissingBean 才能看到用户的 Bean;如果它被组件扫描到,就会和普通的配置类一起提前处理,条件可能判断错误。第三方 starter 按约定命名为 xxx-spring-boot-starter,spring-boot-starter-xxx 留给 Spring Boot 官方使用。 ## 五、Spring MVC Spring MVC 是基于 Servlet 的 Web 框架,按模型-视图-控制器(Model-View-Controller, MVC)的方式组织代码:Controller 接收请求、调用业务逻辑,Model 是要展示的数据,View 把数据渲染成页面。前后端分离后,Controller 通常直接把数据写成 JSON 返回,没有 View。 ### 请求的处理过程 所有请求都先交给一个 DispatcherServlet,由它的 doDispatch 方法完成分发: 1. 通过 HandlerMapping 找到处理这个请求的方法(Handler),以及要经过的拦截器,组成 HandlerExecutionChain; 2. 依次执行拦截器的 preHandle,任何一个返回 false 就不再继续; 3. 通过 HandlerAdapter 调用 Controller 的方法,调用前把请求参数转换为方法参数,调用后把返回值转换为响应,例如用 Jackson 把对象写成 JSON; 4. 倒序执行拦截器的 postHandle; 5. 返回的是视图名时,由 ViewResolver 找到视图并渲染;@RestController 直接把返回值写入响应体,没有这一步; 6. 倒序执行拦截器的 afterCompletion,即使前面发生了异常也会执行,常用于清理资源。 在 Spring Boot 3.3(Spring MVC 6.1)中注册一个打印日志的拦截器,请求 GET /orders?userId=1024 时依次输出: ```text [interceptor] preHandle /orders handler=demo.App$OrderController#list(int) [controller] OrderController.list(1024) [interceptor] postHandle [interceptor] afterCompletion status=200 ``` 拦截器与过滤器(Filter)的区别:过滤器是 Servlet 规范定义的,在请求进入 DispatcherServlet 之前执行,拿不到要调用哪个 Controller 方法;拦截器由 Spring MVC 调用,可以拿到 Handler,并且能使用 Spring 容器中的 Bean。 拦截器与 Spring AOP 的区别:拦截器按 URL 匹配,只作用于进入 DispatcherServlet 的请求;AOP 按切点匹配任意 Spring Bean 的方法,Service 之间的调用也能拦截。因此登录校验、访问日志这类与请求有关的处理用拦截器,事务、缓存、分布式锁这类与业务方法有关的处理用 AOP。 ## 六、MyBatis ### #{} 和 ${} 的区别 MyBatis 中 #{} 会被替换为占位符 ?,参数通过 PreparedStatement 单独传给数据库;${} 则是把参数直接拼接到 SQL 字符串中。用 ${} 时,传入的参数会改变 SQL 的结构,造成 SQL 注入: ```java @Select("SELECT id, user_id AS userId, status FROM orders WHERE user_id = #{userId} ORDER BY id LIMIT 3") List byUser(@Param("userId") int userId); @Select("SELECT id, user_id AS userId, status FROM orders WHERE user_id = ${userId} ORDER BY id LIMIT 3") List byUserUnsafe(@Param("userId") String userId); ``` 参数传入 1024 OR 1=1 时,MyBatis 输出的 SQL 分别为: ```text ==> Preparing: SELECT ... WHERE user_id = ? ORDER BY id LIMIT 3 ==> Parameters: 1024(Integer) ==> Preparing: SELECT ... WHERE user_id = 1024 OR 1=1 ORDER BY id LIMIT 3 ==> Parameters: ``` 第二条语句的条件恒为真,返回了其他用户的订单。${} 只用于表名、列名、ORDER BY 的字段这类不能使用占位符的地方,并且要用白名单校验参数。 ### Mapper 接口为什么不需要实现类 MyBatis 在启动时为每个 Mapper 接口生成 JDK 动态代理,例如上面的 OrderMapper 注入的对象类型是 jdk.proxy2.$Proxy63,调用处理器是 MapperProxy。调用接口方法时,MapperProxy 按“接口全名 + 方法名”找到对应的 SQL,绑定参数后交给 SqlSession 执行,再把结果映射成返回类型。 这个代理是通过 FactoryBean 放进容器的。FactoryBean 是一种特殊的 Bean,容器按名称获取它时,得到的是它的 getObject() 返回的对象,名称前加 & 才能取到 FactoryBean 本身。MyBatis-Spring 为每个 Mapper 接口注册一个 MapperFactoryBean:getBean("app.OrderMapper") 得到的是上面的代理对象,getBean("&app.OrderMapper") 得到的是 MapperFactoryBean(Spring Boot 3.3、mybatis-spring 3.0 实测)。BeanFactory 则是容器本身的根接口,负责创建和管理所有 Bean,两者只是名称相近。 ### 动态 SQL XML 中可以用 if、where、foreach 等标签按参数拼接 SQL: ```xml ``` 只传 status 时生成 WHERE status = ?,两个都传时生成 WHERE user_id = ? AND status = ?,where 标签会去掉开头多余的 AND。 foreach 用于拼接 IN 列表。方法有多个参数时用 @Param 给参数命名,XML 中按名称引用: ```java List byStatuses(@Param("userId") Integer userId, @Param("statuses") List statuses); ``` ```xml ``` 传入 1024 和 [0, 3] 时,生成的 SQL 和参数为: ```text ==> Preparing: SELECT id, user_id AS userId, status FROM orders WHERE user_id = ? AND status IN ( ? , ? ) ==> Parameters: 1024(Integer), 0(Integer), 3(Integer) ``` 列表中的每个元素仍然是一个占位符。需要注意的是,列表为空时 foreach 不输出任何内容,括号也没有,SQL 变成 status IN 后面直接结束,执行时报语法错误,所以调用前要先判断列表是否为空。 ### 缓存 - 一级缓存:默认开启,作用范围是一个 SqlSession,同一个会话中相同的查询只执行一次,执行增删改或提交后清空。与 Spring 集成时,不在事务中的每次调用都会使用新的 SqlSession,一级缓存基本不起作用; - 二级缓存:需要在 Mapper 中配置 cache 才开启,多个会话共享,但缓存在单个应用的内存中,多个实例之间不一致,实际项目中很少使用,通常用 Redis 等缓存代替。 ## 七、Spring 中的设计模式 | 模式 | Spring 中的例子 | 解决的问题 | | --- | --- | --- | | [工厂](设计模式%20-%20工厂方法.md) | BeanFactory、FactoryBean | 使用者只按名称或类型获取对象,不关心对象怎样创建 | | [单例](设计模式%20%20-%20单例.md) | 默认的 singleton 作用域,由 DefaultSingletonBeanRegistry 保存 | 每个 Bean 在容器中只有一个对象,不是通过私有构造函数实现的 | | [代理](设计模式%20-%20代理.md) | AOP 的 JdkDynamicAopProxy、CglibAopProxy | 在不修改原类的情况下加入事务、日志等逻辑,见 [动态代理](#动态代理) | | [模板方法](设计模式%20-%20模板方法.md) | JdbcTemplate、TransactionTemplate、AbstractApplicationContext 的 refresh() | 固定的步骤(获取连接、执行、释放、处理异常)由框架完成,变化的部分由使用者传入 | | [观察者](设计模式%20-%20观察者.md) | ApplicationEvent、ApplicationListener、@EventListener | 发布方不需要知道有哪些接收方 | | [适配器](设计模式%20-%20适配器.md) | Spring MVC 的 HandlerAdapter | DispatcherServlet 用同一个接口调用不同形式的处理器,见 [请求的处理过程](#请求的处理过程) | | [责任链](设计模式%20-%20责任链.md) | HandlerExecutionChain 中的拦截器、AOP 的 ReflectiveMethodInvocation | 多个拦截器或通知依次处理同一个请求 | | [策略](设计模式%20-%20策略.md) | InstantiationStrategy、Resource 的多种实现 | 同一件事有多种做法,运行时选择其中一种 | 以观察者模式为例,订单支付成功后需要加积分、发短信,用事件解耦后,订单服务只负责发布事件: ```java public record OrderPaidEvent(long orderId) {} publisher.publishEvent(new OrderPaidEvent(1024)); // 订单服务,publisher 是注入的 ApplicationEventPublisher @EventListener public void on(OrderPaidEvent e) { ... } // 积分服务、短信服务各写一个监听方法 ``` 需要注意的是,监听方法默认在发布事件的线程中同步执行。在 Spring Boot 3.3 中让短信服务的监听方法抛出异常,输出如下: ```text [main] 订单 1024 支付成功,发布事件 [main] 短信服务:发送失败 [main] 发布方收到异常:短信网关超时 ``` 两个监听方法都在 main 线程中执行;短信服务先执行并抛出异常后,积分服务的监听方法没有被调用,异常还传回了订单服务。监听方法之间的顺序默认不确定,可以用 @Order 指定;不希望互相影响时,加上 @Async 让监听方法在线程池中异步执行,或者用 @TransactionalEventListener 在订单的事务提交之后再处理。 ## 附录:实测 ### A1 单例 Bean 的成员变量 Spring Boot 3.3.5、JDK 21,Apple M1 Pro。CounterController 与正文相同,用 Python 的线程池以 64 个线程请求 /count,每轮 1 万次,用请求前后 /count 的返回值计算增加了多少,应用启动后连续运行 3 轮: ```text 第 1 轮:64 个线程并发请求 10000 次,计数增加 9987,丢失 13 次更新 第 2 轮:64 个线程并发请求 10000 次,计数增加 10000,丢失 0 次更新 第 3 轮:64 个线程并发请求 10000 次,计数增加 10000,丢失 0 次更新 ``` 2026-10-28 重新启动应用后再运行两组: ```text == 启动后第 1 组 第 1 轮:64 个线程并发请求 10000 次,计数增加 9933,丢失 67 次更新 第 2 轮:64 个线程并发请求 10000 次,计数增加 9986,丢失 14 次更新 第 3 轮:64 个线程并发请求 10000 次,计数增加 9997,丢失 3 次更新 == 启动后第 2 组 第 1 轮:64 个线程并发请求 10000 次,计数增加 9999,丢失 1 次更新 第 2 轮:64 个线程并发请求 10000 次,计数增加 9997,丢失 3 次更新 第 3 轮:64 个线程并发请求 10000 次,计数增加 9998,丢失 2 次更新 ``` ### A2 锁与事务的先后 Spring Boot 3.3.5(spring-boot-starter-aop、spring-boot-starter-data-redis)、JDK 21,MySQL 8.0.44(默认的可重复读),Redis 7.4.1,Apple M1 Pro。切面和 OrderService 与正文相同,createOrder() 的方法体为: ```java Integer n = jdbc.queryForObject( "SELECT COUNT(*) FROM t3_order WHERE user_id = ? AND sku_id = ?", Integer.class, userId, skuId); if (n > 0) { return false; // 一人一单 } jdbc.update("INSERT INTO t3_order (user_id, sku_id) VALUES (?, ?)", userId, skuId); return true; ``` 每轮先清空 t3_order,再用 20 个线程同时调用 createOrder(1024, 10086),统计返回 true 的次数和表中的行数。对照组加一个配置类 @EnableTransactionManagement(order = 0),让事务切面比锁的切面(@Order(1))更靠外: ```text 锁在事务外(锁 @Order(1),事务切面为默认的 LOWEST_PRECEDENCE) 第 1~10 轮:20 个并发请求,返回成功 1 次,订单表中 1 行 锁在事务内(事务切面 order = 0) 第 1 轮:20 个并发请求,返回成功 2 次,订单表中 2 行 第 2 轮:20 个并发请求,返回成功 2 次,订单表中 2 行 第 3 轮:20 个并发请求,返回成功 1 次,订单表中 1 行 第 4 轮:20 个并发请求,返回成功 2 次,订单表中 2 行 第 5 轮:20 个并发请求,返回成功 1 次,订单表中 1 行 第 6 轮:20 个并发请求,返回成功 2 次,订单表中 2 行 第 7 轮:20 个并发请求,返回成功 2 次,订单表中 2 行 第 8 轮:20 个并发请求,返回成功 2 次,订单表中 2 行 第 9 轮:20 个并发请求,返回成功 2 次,订单表中 2 行 第 10 轮:20 个并发请求,返回成功 2 次,订单表中 2 行 ``` ### A3 Bean 创建的调用栈 环境同 A2。在一个 @Component 的构造函数和带 @Autowired 的 Setter 方法中 new Throwable().getStackTrace(),只保留 org.springframework.beans、org.springframework.context 和反射的帧(从栈顶到栈底): ```text == 构造函数 DirectConstructorHandleAccessor.newInstance Constructor.newInstanceWithCaller Constructor.newInstance BeanUtils.instantiateClass SimpleInstantiationStrategy.instantiate AbstractAutowireCapableBeanFactory.instantiateBean AbstractAutowireCapableBeanFactory.createBeanInstance AbstractAutowireCapableBeanFactory.doCreateBean AbstractAutowireCapableBeanFactory.createBean AbstractBeanFactory.lambda$doGetBean$0 DefaultSingletonBeanRegistry.getSingleton AbstractBeanFactory.doGetBean AbstractBeanFactory.getBean DefaultListableBeanFactory.preInstantiateSingletons AbstractApplicationContext.finishBeanFactoryInitialization AbstractApplicationContext.refresh == Setter 注入 DirectMethodHandleAccessor.invoke Method.invoke AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject InjectionMetadata.inject AutowiredAnnotationBeanPostProcessor.postProcessProperties AbstractAutowireCapableBeanFactory.populateBean AbstractAutowireCapableBeanFactory.doCreateBean (以下与构造函数相同) ``` ## 参考资料 - [Spring Framework: Dependency Injection](https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html) - [Spring Framework: Customizing the Nature of a Bean](https://docs.spring.io/spring-framework/reference/core/beans/factory-nature.html) - [Spring Framework: Container Extension Points](https://docs.spring.io/spring-framework/reference/core/beans/factory-extension.html) - [Spring Framework: Proxying Mechanisms](https://docs.spring.io/spring-framework/reference/core/aop/proxying.html) - [Spring Framework: Using @Transactional](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html) - [Spring Framework: Transaction Propagation](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-propagation.html) - [Spring Framework 源码:DefaultSingletonBeanRegistry](https://github.com/spring-projects/spring-framework/blob/main/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java) - [Spring Boot: Creating Your Own Auto-configuration](https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html) - [Spring Boot: Using the @SpringBootApplication Annotation](https://docs.spring.io/spring-boot/reference/using/using-the-springbootapplication-annotation.html) - [Spring Framework: Programmatic Transaction Management](https://docs.spring.io/spring-framework/reference/data-access/transaction/programmatic.html) - [Spring Framework: Declaring Advice(Advice Ordering)](https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/advice.html) - [Spring Framework 源码:AbstractApplicationContext](https://github.com/spring-projects/spring-framework/blob/6.1.x/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java) - [Spring Boot 2.6 Release Notes](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes) - [Spring Boot 2.7 Release Notes](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.7-Release-Notes) - [MyBatis 3 文档](https://mybatis.org/mybatis-3/zh_CN/index.html) - [MyBatis 3: Dynamic SQL](https://mybatis.org/mybatis-3/dynamic-sql.html) - [DispatcherServlet | Spring Framework 文档](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet.html)