Files
CyC2018 2fdcece880 Update Spring.md
补充事务回滚规则的新配置、循环依赖的处理过程、初始化回调中不宜做的事、事务提交后的监听方法写数据库、Runner 的执行顺序等说明。
2026-11-02 01:31:11 +08:00

84 KiB
Raw Permalink Blame History

Spring

一、IoC 与 Bean

IoC 与依赖注入 🔥

控制反转(Inversion of Control, IoC)指的是对象不再自己创建所依赖的对象,而是由容器创建好之后交给它。例如 OrderService 需要 OrderRepository,它不在内部 new 一个,而是声明自己需要,由 Spring 容器把创建好的对象传进来,这个过程称为依赖注入(Dependency Injection, DI)。

@Service
public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {   // 构造器注入
        this.repository = repository;
    }
}

这样做的好处:

  • 对象只依赖接口,替换实现、在测试中注入模拟对象都不需要修改代码;
  • 对象的创建、作用域和生命周期由容器统一管理,事务、缓存这类功能也是容器在创建对象时织入的。

容器通过注解知道要创建哪些对象:类上标注 @Component 或者它的派生注解 @Service、@Repository、@Controller,启动时由组件扫描发现;第三方库中的类无法修改源码,就在 @Configuration 类中写一个返回该对象的方法,并标注 @Bean。

@Service 等派生注解本身标注了 @Component(称为元注解),所以自定义的注解标上 @Component 也能声明 Bean。

依赖注入有构造器注入、Setter 注入和字段注入(把 @Autowired 加在字段上)三种方式。Spring 文档通常推荐构造器注入:依赖可以声明为 final,对象创建出来就是完整可用的;字段注入的对象脱离容器直接 new 出来时不会被注入,私有字段在测试中通常要借助容器或反射赋值。可选的依赖用 Setter 注入。

类中只有一个构造器时可以省略 @Autowired;有多个构造器时,在要用的那个上标 @Autowired,其它构造器不能再标;都没有标时,Spring 使用无参构造器(如果有)。

@Autowired 和 @Resource 都能注入依赖,区别在于查找的顺序:

  • @Autowired 是 Spring 的注解,先按类型查找,找到多个时依次看 @Primary、字段名或参数名、@Priority(Spring 6.1 及以前 @Priority 先于名称),仍然无法确定就报错,可以用 @Qualifier 指定 Bean 的名称;
  • @Resource 是 Jakarta EE(原 JSR-250)的注解,指定了 name 时按名称查找,否则先按字段名查找,找不到再按类型查找。它只能用在字段和 Setter 上,不能用在构造器上。

例如容器中有两个 PayService 类型的 Bean,名称分别是 alipay 和 wechat:

@Resource PayService wechat;      // 注入名称为 wechat 的 Bean
@Autowired PayService alipay;     // 按类型找到两个,再按字段名匹配到 alipay
@Autowired PayService payService; // 字段名匹配不上,启动失败

第三行启动时报错 NoUniqueBeanDefinitionException: expected single matching bean but found 2: alipay,wechat。给 wechat 加上 @Primary 后,后两行注入的都是 wechat(见附录 A8)。

需要按条件选择实现时,可以注入 Map<String, PayService>,按 Bean 的名称取出,例如 services.get("wechat").pay(),新增渠道只需再写一个实现类,这是策略模式在 Spring 中的常见写法(见附录 A13)。

常用的注解按用途分为以下几类:

  • 声明 Bean:@Component、@Service、@Repository、@Controller,@Configuration 与 @Bean;
  • 注入:@Autowired、@Resource、@Qualifier、@Primary,@Value 注入配置项;
  • 作用域与生命周期:@Scope、@Lazy、@PostConstruct、@PreDestroy;
  • AOP 与事务:@Aspect、@Around、@Transactional、@Async;
  • Spring Boot:@SpringBootApplication、@ConfigurationProperties、@ConditionalOnClass;
  • Spring MVC:@RestController、@GetMapping、@PathVariable、@RequestParam、@RequestBody。

Bean 的生命周期 🔥

一个单例 Bean 从创建到销毁分为实例化、属性填充、初始化、销毁四个阶段,第 3~6 步属于初始化:

  1. 实例化:调用构造函数创建对象;
  2. 属性填充:注入依赖;
  3. Aware 回调:例如 BeanNameAware 把 Bean 的名字传给它;
  4. BeanPostProcessor 的 postProcessBeforeInitialization();
  5. 初始化:依次调用 @PostConstruct 方法、InitializingBean 的 afterPropertiesSet() 和配置的 init-method,其中 @PostConstruct 实际由内置的 BeanPostProcessor 在第 4 步调用,排在自定义的之后;
  6. BeanPostProcessor 的 postProcessAfterInitialization(),AOP 就是在这一步把 Bean 替换为代理对象的;
  7. 容器关闭时销毁:依次调用 @PreDestroy 方法、DisposableBean 的 destroy() 和配置的 destroy-method。

让一个 Bean 实现上面所有的回调,实际运行打印的顺序如下(Spring Boot 3.3):

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

需要注意的是,@PostConstruct 等初始化方法在容器创建单例的锁内执行,官方文档建议只用来校验配置、准备数据结构,避免在其中访问其它 Bean,否则可能在初始化时死锁。预热缓存、启动后台任务这类耗时的工作可以移到之后的回调中:非懒加载的单例都初始化完后,refresh() 依次调用 SmartInitializingSingleton 的 afterSingletonsInstantiated()、自动启动的 SmartLifecycle 的 start(),再发布 ContextRefreshedEvent(Spring 6.2)。

容器启动时,AbstractApplicationContext 的 refresh() 分两步创建 Bean。第一步解析配置:扫描 @Component,解析 @Bean 方法和自动配置类,把每个 Bean 的类名、作用域、依赖等信息保存为 BeanDefinition,这时还没有创建任何对象。这一步由 ConfigurationClassPostProcessor 完成,它是一个 BeanFactoryPostProcessor,这类扩展点在创建对象之前读取或修改 BeanDefinition;BeanPostProcessor 则在之后处理创建出来的每个 Bean(上面的第 4、6 步),可以返回代理替换原来的对象。

第二步在 finishBeanFactoryInitialization() 中,按 BeanDefinition 依次创建所有非懒加载的单例,创建过程依靠反射。在一个 Bean 的构造函数和带 @Autowired 的 Setter 方法中打印调用栈,按调用顺序整理如下(Spring Boot 3.3 实测,见附录 A3,最后一行 initializeBean 来自源码):

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() 反射赋值。static 字段上的 @Autowired 会被忽略。

Bean 的作用域与线程安全

Bean 默认是单例(singleton)的,整个容器中只有一个对象,所有请求共用它。其它常用的作用域:

  • prototype:每次从容器获取都创建一个新对象,容器创建之后就不再管理它,不会调用它的销毁方法;
  • request、session:每个 HTTP 请求或会话一个对象,只能在 Web 应用中使用。

多个线程同时使用一个单例 Bean 时,Spring 不做任何同步,它是否线程安全取决于有没有可变的成员变量。Controller、Service 通常只持有其他 Bean 的引用,不保存与请求有关的数据,所以是安全的;一旦把请求中的数据存到成员变量里,不同请求就会互相覆盖。例如:

@RestController
public class CounterController {
    private int count = 0;                    // 所有请求共享

    @GetMapping("/count")
    public int count() { return ++count; }    // ++ 不是原子操作
}

用 64 个线程并发请求 1 万次,多次运行中每轮丢失的更新从 0 次到 67 次不等(Spring Boot 3.3 实测,见附录 A1)。是否丢失、丢失多少取决于线程执行的时序,这类问题在测试中很难稳定复现。解决方法是不在单例中保存状态,把数据放在方法的局部变量中;确实需要共享时使用 AtomicInteger 等线程安全的类,每个线程各自的数据放在 ThreadLocal 中。

需要注意的是,把 prototype 的 Bean 注入到单例中,只会在创建单例时注入一次,之后每次使用的都是同一个对象。需要每次得到新对象时,注入 ObjectProvider 并在使用时调用 getObject()。

循环依赖 🔥

循环依赖指的是两个 Bean 互相依赖,例如 A 依赖 B,B 又依赖 A。

对于通过字段或者 Setter 注入的单例 Bean,Spring 通过提前暴露还没有初始化完成的对象来解决。DefaultSingletonBeanRegistry 中有三个缓存:

  • 一级缓存 singletonObjects:初始化完成的 Bean;
  • 二级缓存 earlySingletonObjects:提前暴露的、还没有初始化完成的 Bean;
  • 三级缓存 singletonFactories:用于生成提前引用的工厂。

以 A、B 互相依赖为例:A 实例化后把自己的工厂放入三级缓存,填充属性时创建 B;B 需要 A 时调用 A 的工厂,得到 A 的提前引用并移入二级缓存;B 完成初始化后放入一级缓存,A 再拿到 B 完成初始化。


需要三级缓存而不是两级,是因为 A 可能需要被 AOP 代理,而代理正常是在初始化的最后一步才创建的。如果只有两级,A 实例化后就要决定提前暴露什么:暴露原始对象,B 拿到的与最终放入容器的代理不是同一个;暴露代理,则不管有没有循环依赖都要提前创建代理。三级缓存中的工厂只在 B 来取 A 时才调用(图中第 ③ 步),A 需要代理就由它提前创建,没有循环依赖时不会被调用;结果移入二级缓存,A 同时被 B、C 依赖时工厂只调用一次。

以下情况无法解决,启动时抛出 BeanCurrentlyInCreationException:

  • 构造器注入的循环依赖:创建 A 时必须先有 B,A 还没有实例化,没有可以提前暴露的对象;
  • 原型(prototype)作用域的 Bean:每次获取都创建新对象,不会放入缓存。

另一种无法解决的情况与 @Async 有关。@Async 的代理由 AsyncAnnotationBeanPostProcessor 创建,它只在初始化之后生成代理,没有实现提前引用的 getEarlyBeanReference(),三级缓存中的工厂不会为它提前创建代理:B 拿到的是原始的 A,A 初始化完成后却被替换成了代理,B 中的引用与容器中的对象不一致。Spring 检查到这种情况时启动失败,提示 A 已经以原始版本注入到了 B 中、最终却被包装了;同样的结构把 @Async 换成 @Transactional 则可以正常启动,B 中注入的就是最终的代理(Spring Boot 3.3 实测,见附录 A6)。

这是 Spring 6.1 的行为。在 Spring 6.2.19、7.0.10 上,允许循环依赖时同样的结构能够启动(见附录 A6):6.2.3 起,容器在预先创建单例时遇到这个异常会跳过 A,之后从 B 开始重新创建。官方尚未确认这是否是预期行为,不能据此认为 @Async 支持循环依赖。Spring Boot 默认禁止循环依赖,所以在 Boot 中默认仍是启动失败。

构造器注入的循环依赖可以在其中一个参数上加 @Lazy 解开:Spring 先注入一个延迟解析的代理,第一次调用它的方法时才从容器中取出真正的 Bean。

需要注意的是,Spring Boot 2.6 起默认禁止循环依赖,出现时直接启动失败。设置 spring.main.allow-circular-references=true 可以恢复,Boot 的错误提示把它称为最后的手段;也可以在其中一侧用 @Lazy 注入或用 ObjectProvider 延迟获取,默认配置下就能启动。这是 Spring Boot 的默认设置,直接使用 Spring Framework 的容器时仍然默认允许。循环依赖通常说明职责划分有问题,更好的做法是调整依赖关系,例如把互相调用的逻辑抽到新的 Bean C 中。

二、AOP 🔥🔥

动态代理

面向切面编程(Aspect-Oriented Programming, AOP)用于把日志、事务、权限校验这类分散在各处的逻辑从业务代码中抽离出来。Spring AOP 基于动态代理实现:容器中保存的不是原始对象,而是代理对象,调用方法时先执行代理中的切面逻辑,再调用原始对象的方法。

两种代理方式:

  • JDK 动态代理:目标类实现了接口时可以使用,代理类实现同样的接口,只能代理接口中的方法;
  • CGLIB:在运行时生成目标类的子类,通过重写方法来代理,因此 final 类,以及 final、static 和 private 方法无法被代理。

Spring Framework 在目标类实现了接口时默认使用 JDK 动态代理;Spring Boot 从 2.0 起默认使用 CGLIB,可以通过 spring.aop.proxy-target-class 修改。Boot 这样做是为了减少类型转换的问题:JDK 代理只实现接口,一个实现了接口的 Bean 被别处按实现类注入时,会因为代理的类型不匹配而启动失败(BeanNotOfRequiredTypeException)。目标本身是 JDK 代理(例如 MyBatis 的 Mapper)时,生成的仍是 JDK 代理。

动态代理在运行时生成代理类,不必像静态代理那样为每个接口手写,同一段增强逻辑可以复用到不同的类上。JDK 动态代理的原理可以用下面的代码说明,切面逻辑写在 InvocationHandler 中:

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();
开启事务
创建订单
提交事务

生成的代理类 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 时生成代理。常见的做法是自定义一个注解,让切面拦截带有这个注解的方法。例如给下单方法加分布式锁:

@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 指令。记录日志、限流、幂等校验也都是这种写法。

需要注意的是,createOrder() 上同时有事务和锁两个切面,它们的先后决定了锁是否包住事务。锁必须在外层:如果事务在外层,锁释放之后事务才提交,这期间另一个请求拿到锁,查不到还没有提交的订单,就会再创建一个。20 个请求同时为同一个用户下单,锁在事务外时 10 轮都只创建了 1 个订单;锁在事务内时 10 轮中有 8 轮创建了 2 个(Spring Boot 3.3、MySQL 8.0 实测,见附录 A2)。


两个切面都没有指定顺序时,执行的先后是不确定的。@Order 的值越小越先执行、越靠外层,事务切面的默认值是 Ordered.LOWEST_PRECEDENCE,所以上面给锁的切面加上了 @Order(1)。

代理的创建与通知的执行

AOP 的几个术语可以和上面的分布式锁切面对应起来:

  • 切面(Aspect):DistributedLockAspect 这个类,它把“拦截哪些方法”和“做什么”组织在一起;
  • 切点(Pointcut):@annotation(lock),描述要拦截哪些方法;
  • 通知(Advice):around() 方法,即在被拦截的方法前后执行的逻辑;
  • 连接点(Join Point):被拦截的一次方法执行,例如某次 createOrder(1024, 10086),Spring AOP 只支持方法执行这一种连接点。

通知按执行的时机分为五种:@Before、@AfterReturning(正常返回后)、@AfterThrowing(抛出异常后)、@After(无论是否抛出异常,类似 finally)和 @Around(包住整个调用,可以决定是否调用原方法,也能修改参数和返回值)。

代理是在 Bean 的生命周期 中初始化之后的那一步创建的。引入 AOP 后,容器中会注册一个 BeanPostProcessor:AnnotationAwareAspectJAutoProxyCreator。每个 Bean 初始化完成后,它找出所有切面中与这个 Bean 的方法匹配的通知,有匹配的就创建代理,用代理代替原始对象放入容器,没有匹配的原样返回。在另一个 BeanPostProcessor 中打印,orderService 在初始化前的类型是 OrderService,初始化后变成了 OrderService$SpringCGLIB$0。存在循环依赖时代理会提前创建,见循环依赖。

调用代理的方法时,代理把匹配这个方法的通知排成一条链,依次调用,每个通知通过 proceed() 进入下一个,最后一个之后才是目标方法,这是责任链模式。例如给 OrderService 加日志 @Order(1) 和鉴权 @Order(2) 两个切面,各有四种通知,调用 create(1024) 的输出如下(Spring Boot 3.3 实测,见附录 A4):

日志 @Around 前
  日志 @Before
    鉴权 @Around 前
      鉴权 @Before
        目标方法 create(1024)
      鉴权 @AfterReturning
      鉴权 @After
    鉴权 @Around 后
  日志 @AfterReturning
  日志 @After
日志 @Around 后

@Order 值小的切面在外层,先进入、后退出。同一个切面内的顺序是:@Around 的前半段、@Before、目标方法、@AfterReturning 或 @AfterThrowing、@After、@Around 的后半段。

Spring AOP 与 AspectJ 的区别:Spring AOP 在运行时生成代理,只能拦截 Spring Bean 的方法调用,自调用不会被拦截;AspectJ 在编译时或类加载时修改字节码(称为织入),能拦截构造函数、字段访问和自调用,但需要专门的编译器或 Java Agent。Spring 只借用了 AspectJ 的注解和切点表达式的解析,织入仍然靠代理,所以用 @Aspect 写的切面也是 Spring AOP。

需要注意的是,@Transactional 不依赖 AspectJ。去掉 spring-boot-starter-aop(Spring Boot 4 起改名为 spring-boot-starter-aspectj)后,容器中的自动代理创建器变为 InfrastructureAdvisorAutoProxyCreator,它只处理事务这类框架内置的通知,事务仍然生效(见附录 A5)。

自调用

代理只能拦截从外部经过代理的调用。在目标对象内部调用自己的另一个方法时,等价于 this.inner(),调用的是原始对象,不经过代理,切面不会生效:


@Service
public class OrderService {
    public void outer() {
        inner();             // 自调用,inner() 上的事务不生效
    }

    @Transactional
    public void inner() {
    }
}

这是 @Transactional、@Async、@Cacheable 等注解失效最常见的原因。解决方法是把被调用的方法移到另一个 Bean 中,或者注入自身的代理,通过代理调用。Spring Boot 2.6 起直接用 @Autowired 注入自身也算循环依赖,默认启动失败,要加 @Lazy 或改用 ObjectProvider。

outer() 本身有事务时,自调用的 inner() 即使声明了 REQUIRES_NEW 也不会开启新事务,外层回滚时一起回滚(Spring 6.1 实测,见附录 A9)。

切面失效的其它原因见事务失效。其中 final 方法在 CGLIB 代理下容易出问题:调用直接在没有注入依赖的代理对象上执行,@Autowired 的字段为 null(Spring Boot 3.3 实测,见附录 A11)。

三、事务

声明式事务

@Transactional 同样基于 AOP 实现:代理在方法执行前开启事务,方法正常返回时提交,抛出异常时回滚。

需要注意的是,默认只有 RuntimeException 和 Error 会触发回滚,受检异常(例如 IOException)不会回滚,需要通过 rollbackFor 指定。Spring 6.2 起(Spring Boot 3.4 起)也可以用 @EnableTransactionManagement(rollbackOn = ALL_EXCEPTIONS) 把默认规则改为所有异常都回滚,官方文档建议不依赖受检异常提交事务的项目这样设置;方法上指定的 rollbackFor、noRollbackFor 仍然优先。

除了声明式事务,还可以用 TransactionTemplate 编程式地使用事务,事务的范围就是传入的回调:

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 更简洁。

事务的实现原理

数据库的事务建立在一个连接上:同一个连接在开始事务之后执行的 SQL 才属于这个事务。@Transactional 要解决的问题是,Service 中通过 JdbcTemplate、MyBatis 执行的多条 SQL 怎样用上同一个连接,又不需要把连接作为参数层层传递。

调用带 @Transactional 的方法时,代理中的 TransactionInterceptor 按以下步骤执行:

  1. 读取方法上的事务属性:传播行为、隔离级别、超时、是否只读和回滚规则;
  2. 调用事务管理器开启事务,使用 JDBC 和 MyBatis 时是 DataSourceTransactionManager:从连接池取一个连接,执行 setAutoCommit(false),需要时设置隔离级别,再把连接绑定到当前线程(TransactionSynchronizationManager 中的 ThreadLocal);
  3. 调用目标方法。JdbcTemplate 和 MyBatis 获取连接时先查看当前线程有没有绑定的连接,有就直接使用,所以方法中的所有 SQL 都在这个连接上执行;
  4. 方法正常返回时提交,抛出符合回滚规则的异常时回滚;最后恢复连接的自动提交和隔离级别,解除绑定,把连接还给连接池。

在事务方法中查询 MySQL 的连接 ID,可以看到这个过程(Spring Boot 3.3、MySQL 8.0 实测,见附录 A5):

create() 的第 1、2 条 SQL           连接 5435,autocommit=0
  调用 StockService(REQUIRED)     连接 5435,autocommit=0
  调用 LogService(REQUIRES_NEW)   连接 5436,autocommit=0
  方法中新建的线程                  连接 5436,autocommit=1,不在事务中
report()(READ_COMMITTED)          连接 5435,隔离级别 READ-COMMITTED
事务结束后                          连接 5435,autocommit=1,隔离级别 REPEATABLE-READ

REQUIRED 的内层方法加入了外层的事务,用的是同一个连接。REQUIRES_NEW 先挂起外层事务,即把外层的连接从线程上解绑、暂存起来,再取一个新连接开启新事务,结束后把外层的连接绑回去,所以两个事务互不影响。新线程上没有绑定连接,它的 SQL 自动提交,不属于这个事务,这就是事务失效中“在新线程中操作数据库”的原因。新线程用到的 5436 是 LogService 用完后还给连接池的连接,autocommit 已经恢复为 1。


隔离级别默认是 Isolation.DEFAULT,即使用数据库自己的设置(MySQL 默认为可重复读);指定了其它级别时,事务管理器只对这个连接、这一个事务设置,结束后恢复。

需要注意的是,连接在整个事务方法执行期间都被占用。方法中有调用外部接口这类耗时的操作时,连接会被长时间占用,并发高时连接池很快耗尽,所以事务方法中只放必须在事务中完成的数据库操作。

使用 HikariCP 时,连接池耗尽后获取连接最多等待 connectionTimeout(默认 30 秒),期间有连接归还就返回,超时仍未取得才抛出 SQLTransientConnectionException。排查连接一直不归还的代码时,可以设置 leakDetectionThreshold,连接借出超过这个时间就在日志中打印借出处的调用栈,它只报告、不回收连接。

传播行为

传播行为指的是一个事务方法调用另一个事务方法时,事务如何传递。常用的有以下三种:

  • REQUIRED:默认值,当前有事务就加入,没有就新建。内外两个方法属于同一个事务,一起提交或者一起回滚;
  • REQUIRES_NEW:挂起当前事务,新建一个独立的事务,两个事务分别提交和回滚,例如无论业务成功与否都要保存的操作日志。需要注意的是,已有外层事务时,执行内层事务期间一个线程同时占用两个连接,并发高时所有线程可能都拿着外层的连接等内层的连接,直到获取连接超时;内层修改外层已加锁的行时,也会一直等到锁等待超时;
  • NESTED:在当前事务中创建一个保存点,内层回滚时只回滚到保存点,外层回滚时内层也一起回滚。保存点是 JDBC 连接的功能,只引入 JDBC 时 Spring Boot 默认的 JdbcTransactionManager 支持;引入 Spring Data JPA 后默认是 JpaTransactionManager,用 Hibernate 时在已有事务中使用 NESTED 会抛出 NestedTransactionNotSupportedException。没有当前事务时,NESTED 和 REQUIRED 一样新建事务。

其余四种用得较少:SUPPORTS 有事务就加入,没有就非事务执行;NOT_SUPPORTED 挂起事务后非事务执行;MANDATORY 没有事务就抛出异常;NEVER 有事务就抛出异常。

REQUIRED 有一个常见的问题:内层方法(另一个 Bean 的事务方法)抛出 RuntimeException 时不能单独回滚,只能把整个事务标记为只能回滚(rollback-only),即使外层捕获了这个异常,方法结束时仍然回滚,并抛出 UnexpectedRollbackException(Spring 6.1 实测,见附录 A9)。内层失败不应影响外层时,改用 REQUIRES_NEW 或 NESTED。

事务失效

事务不生效的常见原因:

  • 自调用:同一个类中的方法互相调用,不经过代理;
  • 方法无法被代理:CGLIB 无法重写 final、static 和 private 方法,JDK 动态代理只拦截接口中声明的方法;Spring 6.0 之前,非 public 方法上的注解也会被忽略;
  • 异常被吞掉:方法内部捕获了异常而没有重新抛出,代理认为方法正常结束,提交了事务;
  • 抛出的是受检异常,而没有配置 rollbackFor;
  • 对象不是由 Spring 管理的,例如自己 new 出来的对象;
  • 在新线程中操作数据库:事务绑定在当前线程上,其它线程(包括 @Async 方法使用的线程池线程)中的操作不属于这个事务;
  • 数据库不支持事务,例如 MySQL 的 MyISAM 引擎。

四、Spring Boot 自动配置 🔥

Spring Boot 在 Spring 的基础上提供了自动配置和 starter:starter 把一组常用的依赖打包在一起,例如 spring-boot-starter-web 引入了 Spring MVC、内嵌的 Tomcat 和 JSON 库;自动配置则根据引入的依赖自动创建所需的 Bean,不需要手动配置就能启动一个 Web 应用。

Spring Boot 4 起,原来的 spring-boot-autoconfigure 拆成了按技术划分的小模块,每种技术都有自己的 starter。以前只加第三方依赖就能生效的部分自动配置,现在还需要对应的 Spring Boot 模块,通常通过 starter 引入,例如迁移指南要求把原来的 Flyway 依赖换成 spring-boot-starter-flyway。

Spring Boot 并不是新的框架,容器、AOP、事务仍然是 Spring 提供的。除了 starter 和自动配置,它还能把应用连同内嵌的服务器打包成一个 jar,用 java -jar 直接运行,不再需要把 war 包部署到单独安装的 Tomcat 中。

自动配置的过程:

  1. 启动类上的 @SpringBootApplication 组合了三个注解:@SpringBootConfiguration(相当于 @Configuration,启动类中也可以定义 @Bean)、@ComponentScan(扫描启动类所在的包及其子包)和 @EnableAutoConfiguration(开启自动配置);
  2. @EnableAutoConfiguration 通过 @Import 引入 AutoConfigurationImportSelector,它读取所有 jar 包中的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,得到自动配置类的列表。Spring Boot 2.7 之前写在 META-INF/spring.factories 中,2.7 两处都读,3.0 起不再读 spring.factories 中 EnableAutoConfiguration 键下的类,只写在那里的自动配置类不会加载,启动时也不报错(Spring Boot 3.5 实测);spring.factories 的其它键(例如 ApplicationListener)仍然有效。思路与 Java 的 SPI 相同,区别在于读到的类不直接实例化,而是作为配置类交给容器解析;
  3. 自动配置类大多带有条件注解,条件满足时才生效。例如 @ConditionalOnClass 要求类路径上存在某个类,@ConditionalOnMissingBean 要求用户没有定义同类型的 Bean,@ConditionalOnProperty 根据配置项决定。

例如引入 spring-boot-starter-jdbc 和数据库驱动后,数据源的自动配置类就会生效,按配置文件中的地址创建一个 DataSource;如果用户自己定义了 DataSource,由于 @ConditionalOnMissingBean,自动配置不再创建。因此带 @ConditionalOnMissingBean 的自动配置会让位于用户自己定义的 Bean。

启动时加上 --debug 会打印条件评估报告,列出每个自动配置类生效或跳过的原因,例如只引入 spring-boot-starter-web 时,152 个中只有 22 个生效(Spring Boot 3.3 实测,见附录 A7)。

自定义 starter 也是同样的做法。例如把公司内部的短信客户端做成 starter,自动配置类如下:

@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 官方使用。

启动流程

启动类的 main 方法调用 SpringApplication.run(BootApp.class, args),它先创建一个 SpringApplication 对象,再执行这个对象的 run()。以一个只引入了 spring-boot-starter-web 的应用为例,在各个步骤中打印耗时,并监听启动过程中发布的事件(Spring Boot 3.3 实测,见附录 A7,有删减):

  146 ms  new SpringApplication(),应用类型 SERVLET
  187 ms  事件 ApplicationStartingEvent
  417 ms  事件 ApplicationEnvironmentPreparedEvent
  494 ms  ApplicationContextInitializer,上下文类型 AnnotationConfigServletWebServerApplicationContext,BeanDefinition 5 个
  527 ms  事件 ApplicationPreparedEvent
  811 ms  BeanFactoryPostProcessor,BeanDefinition 157 个,含 DispatcherServlet 自动配置:true
  897 ms  创建内嵌 Tomcat 的工厂:TomcatServletWebServerFactory
 1182 ms  创建单例 Bean OrderService,8097 连接被拒绝
 1356 ms  事件 ServletWebServerInitializedEvent
 1359 ms  事件 ContextRefreshedEvent
 1360 ms  事件 ApplicationStartedEvent
 1363 ms  CommandLineRunner 执行,8097 可以连接
 1363 ms  事件 ApplicationReadyEvent

对照输出,启动分为以下几步:

  1. 创建 SpringApplication:根据类路径上的类判断应用类型,有 Servlet 和 Spring Web 的类时是 SERVLET,只有 WebFlux 时是 REACTIVE,都没有时是 NONE;再从各个 jar 包的 META-INF/spring.factories 中加载 ApplicationContextInitializer、ApplicationListener 等扩展;
  2. 准备环境:把命令行参数、系统属性、环境变量和 application.properties(或 application.yml)合并到 Environment 中,之后 @Value 和 @ConfigurationProperties 都从这里取值;
  3. 创建并准备容器:按应用类型创建 AnnotationConfigServletWebServerApplicationContext,执行各个 ApplicationContextInitializer(这时容器中只有 5 个内部组件的 BeanDefinition),再把启动类注册为 BeanDefinition;
  4. 刷新容器:执行 Spring 容器的 refresh(),Bean 都在这一步创建,见 Bean 的生命周期。与 Spring Boot 有关的有三处,见下文;
  5. 执行 Runner:调用容器中的 ApplicationRunner 和 CommandLineRunner,两种 Runner 放在一起排序,要按顺序执行时用 @Order 或 Ordered 指定,没有指定时先后不固定(Spring Boot 3.5),然后发布 ApplicationReadyEvent,run() 返回。

refresh() 中与 Spring Boot 有关的三处:

  • 解析配置类:从启动类开始做组件扫描、解析 @Bean 方法,并按上文的过程加载自动配置类,BeanDefinition 从 5 个增加到 157 个;
  • onRefresh():创建内嵌的 Tomcat,把 DispatcherServlet 注册到其中,这时还不监听端口;
  • 创建所有非懒加载的单例 Bean,之后在 finishRefresh() 中启动 Tomcat 的连接器,开始监听端口。所以创建 OrderService 时 8097 端口还连不上,执行 CommandLineRunner 时已经可以连接。

启动完成后要执行的逻辑,例如缓存预热,可以放在 ApplicationRunner、CommandLineRunner 中,或者监听 ApplicationReadyEvent。Runner 抛出异常会导致启动失败。需要注意的是,执行 Runner 时 Tomcat 已经在接收请求,预热还没完成的请求可能直接落到数据库上。部署在 Kubernetes 中时,Spring Boot 在发布 ApplicationReadyEvent 之后才把就绪状态设为 ACCEPTING_TRAFFIC,就绪探针 检查 /actuator/health/readiness 时,流量要等 Runner 执行完才会进来。

用 java -jar 启动打包好的 jar 时,jar 的 Main-Class 是 Spring Boot 的 JarLauncher,它从 jar 中的 BOOT-INF/classes 和 BOOT-INF/lib 加载应用和依赖,再调用 Start-Class 指定的启动类的 main 方法。

启动太慢时,可以设置 spring.main.lazy-initialization=true,让单例 Bean 在第一次使用时才创建,代价是配置错误要到第一次使用时才暴露,第一个请求也更慢。

五、Spring MVC

Spring MVC 是 Spring Framework 中基于 Servlet 的 Web 模块,按模型-视图-控制器(Model-View-Controller, MVC)的方式组织代码:Controller 接收请求、调用业务逻辑,Model 是要展示的数据,View 把数据渲染成页面。前后端分离后,Controller 通常直接把数据写成 JSON 返回,没有 View。

请求的处理过程 🔥

所有请求都先交给一个 DispatcherServlet,由它的 doDispatch 方法完成分发:

  1. 通过 HandlerMapping 找到处理这个请求的方法(Handler),以及要经过的拦截器,组成 HandlerExecutionChain,找不到时返回 404;
  2. 依次执行拦截器的 preHandle,任何一个返回 false 就不再继续,Controller 不会被调用;
  3. 通过 HandlerAdapter 调用 Controller 的方法,调用前把请求参数转换为方法参数,调用后把返回值转换为响应,例如用 Jackson 把对象写成 JSON;
  4. 倒序执行拦截器的 postHandle,Controller 抛出异常时跳过这一步;@RestController 的返回值在第 3 步已经写进响应,postHandle 拿到的 ModelAndView 为 null,Spring 6.x 中响应此时已经提交,再加首部不会生效,要修改 JSON 响应时用 ResponseBodyAdvice;
  5. 返回的是视图名时,由 ViewResolver 找到视图并渲染;@RestController 直接把返回值写入响应体,没有这一步;
  6. 倒序执行 preHandle 已经返回 true 的拦截器的 afterCompletion,即使前面发生了异常也会执行(异常已被 @ExceptionHandler 处理时,参数 ex 为 null),常用于清理资源,返回 false 的拦截器则要在返回前自己清理(见附录 A10)。

HandlerMapping 按路径和 HTTP 方法等条件匹配 @GetMapping 等注解的方法,多个都匹配时选最具体的,例如 /orders/latest 优先于 /orders/{id};路径匹配而方法不匹配时返回 405。处理器还可以是实现 Controller 等接口的类,DispatcherServlet 为每种处理器找到支持它的 HandlerAdapter 来调用。

参数由 HandlerMethodArgumentResolver 解析,@RequestBody 和返回值由 HttpMessageConverter 按 Content-Type、Accept 读写,转换失败时 GET /orders/abc 返回 400,用 text/plain 提交 JSON 返回 415(见附录 A14)。

拦截器实现 HandlerInterceptor 接口,在 WebMvcConfigurer 的 addInterceptors 中注册。在 Spring Boot 3.3(Spring MVC 6.1)中注册一个打印日志的拦截器,请求 GET /orders?userId=1024 时依次输出:

[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 是半自动的 ORM 框架:SQL 由开发者写在 XML 或注解中,MyBatis 负责绑定参数、把结果集映射成对象。Hibernate 则按实体的映射自动生成 SQL,复杂查询和调优时不容易控制。MyBatis-Plus 在 MyBatis 之上提供通用的 BaseMapper 和条件构造器,单表操作不用写 SQL。

#{} 和 ${} 的区别

MyBatis 中 #{} 会被替换为占位符 ?,参数通过 PreparedStatement 单独传给数据库;${} 则是把参数直接拼接到 SQL 字符串中。用 ${} 时,传入的参数会改变 SQL 的结构,造成 SQL 注入:

@Select("SELECT id, user_id AS userId, status FROM orders WHERE user_id = #{userId} ORDER BY id LIMIT 3")
List<Order> byUser(@Param("userId") String userId);

@Select("SELECT id, user_id AS userId, status FROM orders WHERE user_id = ${userId} ORDER BY id LIMIT 3")
List<Order> byUserUnsafe(@Param("userId") String userId);

参数传入 1024 OR 1=1 时,MyBatis 输出的 SQL 分别为:

==>  Preparing: SELECT ... WHERE user_id = ? ORDER BY id LIMIT 3
==> Parameters: 1024 OR 1=1(String)

==>  Preparing: SELECT ... WHERE user_id = 1024 OR 1=1 ORDER BY id LIMIT 3
==> Parameters:

第一条语句的结构没有变,整个字符串只是一个参数值;第二条语句的条件恒为真,返回了其他用户的订单(见附录 A10)。${} 只用于表名、列名、ORDER BY 的字段这类不能使用占位符的地方,并且要用白名单校验参数。

Mapper 接口为什么不需要实现类

MyBatis 在启动时为每个 Mapper 接口注册一个代理工厂 MapperProxyFactory,获取 Mapper 时由它创建 JDK 动态代理;Spring 中由 MapperFactoryBean 在创建 Bean 时获取一次,注入的就是这个代理。例如上面的 OrderMapper 注入的对象类型是 jdk.proxy2.$Proxy63,调用处理器是 MapperProxy。调用接口方法时,MapperProxy 按“接口全名 + 方法名”找到对应的 SQL,绑定参数后交给 SqlSession 执行,再把结果映射成返回类型。

由于 SQL 只按方法名查找,Mapper 接口中的方法不能重载,两个同名方法都写了 SQL 时启动报错(见附录 A12)。

结果映射按列名匹配属性名,所以前面的 SQL 写成 user_id AS userId,也可以开启 mapUnderscoreToCamelCase 或用 resultMap 指定。对不上的列默认被忽略、不报错,属性保留初始值。

Mapper 的代理对象是通过 FactoryBean 放进容器的。FactoryBean 是一种特殊的 Bean,容器按名称获取它时,得到的是它的 getObject() 返回的对象,名称前加 & 才能取到 FactoryBean 本身。MyBatis-Spring 为每个 Mapper 接口注册一个 MapperFactoryBean:getBean("orderMapper") 得到的是上面的代理对象,getBean("&orderMapper") 得到的是 MapperFactoryBean(Spring Boot 3.3、mybatis-spring 3.0 实测)。BeanFactory 则是容器本身的根接口,负责创建和管理所有 Bean,两者只是名称相近。

动态 SQL

XML 中可以用 if、where、foreach 等标签按参数拼接 SQL:

<select id="search" resultType="demo.App$Order">
  SELECT id, user_id AS userId, status FROM orders
  <where>
    <if test="userId != null">AND user_id = #{userId}</if>
    <if test="status != null">AND status = #{status}</if>
  </where>
</select>

只传 status 时生成 WHERE status = ?,两个都传时生成 WHERE user_id = ? AND status = ?,where 标签会去掉开头多余的 AND。

foreach 用于拼接 IN 列表。方法有多个参数时用 @Param 给参数命名,XML 中按名称引用:

List<Order> byStatuses(@Param("userId") Integer userId, @Param("statuses") List<Integer> statuses);
<select id="byStatuses" resultType="demo.App$Order">
  SELECT id, user_id AS userId, status FROM orders
  WHERE user_id = #{userId} AND status IN
  <foreach collection="statuses" item="s" open="(" separator="," close=")">#{s}</foreach>
</select>

传入 1024 和 [0, 3] 时,生成的 SQL 和参数为:

==>  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 的命名空间,多个会话共享,在 Mapper XML 中加 <cache/> 或在接口上加 @CacheNamespace 开启,结果对象要实现 Serializable。查询结果在会话提交后才写入(只有查询的会话关闭时也会写入;执行过增删改的会话回滚时丢弃),同一命名空间的增删改提交后清空。缓存在单个应用的内存中,多个实例之间不一致,实际项目中很少使用,通常用 Redis 等缓存代替。

七、Spring 中的设计模式

模式 Spring 中的例子 解决的问题
简单工厂、工厂方法 BeanFactory 的 getBean()(简单工厂);FactoryBean 的 getObject()(工厂方法) 使用者只按名称或类型获取对象,不关心对象怎样创建
单例 默认的 singleton 作用域,由 DefaultSingletonBeanRegistry 保存 每个 Bean 在容器中只有一个对象,不是通过私有构造函数实现的
代理 AOP 的 JdkDynamicAopProxy、CglibAopProxy 在不修改原类的情况下加入事务、日志等逻辑,见 动态代理
模板方法 AbstractApplicationContext 的 refresh();JdbcTemplate、TransactionTemplate 固定的步骤由框架完成,变化的部分由子类重写(例如 onRefresh() 创建 Tomcat)或以回调传入
观察者 ApplicationEvent、ApplicationListener、@EventListener 发布方不需要知道有哪些接收方
适配器 Spring MVC 的 HandlerAdapter DispatcherServlet 用同一个接口调用不同形式的处理器,见 请求的处理过程
责任链 HandlerExecutionChain 中的拦截器、AOP 的 ReflectiveMethodInvocation 多个拦截器或通知依次处理同一个请求
策略 InstantiationStrategy、Resource 的多种实现 同一件事有多种做法,运行时选择其中一种

以观察者模式为例,订单支付成功后需要加积分、发短信,用事件解耦后,订单服务只负责发布事件:

public record OrderPaidEvent(long orderId) {}

publisher.publishEvent(new OrderPaidEvent(1024));      // 订单服务,publisher 是注入的 ApplicationEventPublisher

@EventListener
public void on(OrderPaidEvent e) { ... }               // 积分服务、短信服务各写一个监听方法

需要注意的是,监听方法默认在发布事件的线程中同步执行。在 Spring Boot 3.3 中让短信服务的监听方法抛出异常,输出如下:

[main] 订单 1024 支付成功,发布事件
[main] 短信服务:发送失败
[main] 发布方收到异常:短信网关超时

两个监听方法都在 main 线程中执行;短信服务先执行并抛出异常后,积分服务的监听方法没有被调用,异常还传回了订单服务。监听方法之间的顺序默认不确定,可以用 @Order 指定;不希望互相影响时,加上 @Async(配置类要加 @EnableAsync)让监听方法在线程池中异步执行,或者用 @TransactionalEventListener 在订单的事务提交之后再处理。后者在发布时没有事务就不会执行(Spring 6.1 实测,见附录 A9)。

默认在提交之后(AFTER_COMMIT)同步执行的监听方法中,事务资源可能还绑定在当前线程上,这时调用 REQUIRED 的事务方法会加入这个已经提交的事务,之后不会再提交。所以需要事务性的写入时,要通过代理开启 REQUIRES_NEW 的事务,例如调用另一个 Bean 中 REQUIRES_NEW 的方法,或者直接在监听方法上标 @Transactional(propagation = REQUIRES_NEW)。

Spring 6.1 起注册监听器时会检查:监听方法上的 @Transactional 只能是 REQUIRES_NEW 或 NOT_SUPPORTED(后者不开启事务),标成默认的 REQUIRED 通常会导致启动失败;6.2 起也检查所在类上的注解,6.2.10 起 BEFORE_COMMIT 阶段的监听方法不受这个限制。

附录:实测

A1 单例 Bean 的成员变量

Spring Boot 3.3.5、JDK 21,Apple M1 Pro。CounterController 与正文相同,用 Python 的线程池以 64 个线程请求 /count,每轮 1 万次,用请求前后 /count 的返回值计算增加了多少,应用启动后连续运行 3 轮:

第 1 轮:64 个线程并发请求 10000 次,计数增加 9987,丢失 13 次更新
第 2 轮:64 个线程并发请求 10000 次,计数增加 10000,丢失 0 次更新
第 3 轮:64 个线程并发请求 10000 次,计数增加 10000,丢失 0 次更新

2026-10-28 重新启动应用后再运行两组:

== 启动后第 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() 的方法体为:

Integer n = jdbc.queryForObject(
        "SELECT COUNT(*) FROM seckill_order WHERE user_id = ? AND sku_id = ?", Integer.class, userId, skuId);
if (n > 0) {
    return false;                          // 一人一单
}
jdbc.update("INSERT INTO seckill_order (user_id, sku_id) VALUES (?, ?)", userId, skuId);
return true;

每轮先清空 seckill_order,再用 20 个线程同时调用 createOrder(1024, 10086),统计返回 true 的次数和表中的行数。对照组加一个配置类 @EnableTransactionManagement(order = 0),让事务切面比锁的切面(@Order(1))更靠外:

锁在事务外(锁 @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 和反射的帧(从栈顶到栈底):

== 构造函数
  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
  (以下与构造函数相同)

A4 代理的创建与通知的顺序

Spring Boot 3.3.5(spring-boot-starter-aop,AspectJ Weaver 1.9.22.1)、JDK 21。OrderService.create() 只打印一行;LogAspect(@Order(1))和 AuthAspect(@Order(2))的切点都是 execution(* lab.AopDemo.OrderService.*(..)),各有 @Around、@Before、@AfterReturning、@After 四个通知,按缩进打印。另写一个普通的 BeanPostProcessor,在初始化前后打印 orderService 的类型:

public Object postProcessBeforeInitialization(Object bean, String name) {
    if (bean instanceof OrderService) System.out.println("[初始化前] " + name + " 的类型:" + bean.getClass().getSimpleName());
    return bean;
}
// postProcessAfterInitialization 同理

输出(省略了包名,通知列表只保留类名和所属的切面):

[初始化前] orderService 的类型:OrderService
[初始化后] orderService 的类型:AopDemo$OrderService$$SpringCGLIB$$0
BeanPostProcessor 列表:
  ApplicationContextAwareProcessor
  ImportAwareBeanPostProcessor
  BeanPostProcessorChecker
  ConfigurationPropertiesBindingPostProcessor
  AnnotationAwareAspectJAutoProxyCreator
  PersistenceExceptionTranslationPostProcessor
  Probe
  CommonAnnotationBeanPostProcessor
  AutowiredAnnotationBeanPostProcessor
  ApplicationListenerDetector
代理中的通知(Advisor):
  ExposeInvocationInterceptor
  AspectJAroundAdvice          LogAspect.around
  AspectJMethodBeforeAdvice    LogAspect.before
  AspectJAfterAdvice           LogAspect.after
  AspectJAfterReturningAdvice  LogAspect.ret
  AspectJAroundAdvice          AuthAspect.around
  AspectJMethodBeforeAdvice    AuthAspect.before
  AspectJAfterAdvice           AuthAspect.after
  AspectJAfterReturningAdvice  AuthAspect.ret
---- 调用 create(1024)
日志 @Around 前
  日志 @Before
    鉴权 @Around 前
      鉴权 @Before
        目标方法 create(1024)
      鉴权 @AfterReturning
      鉴权 @After
    鉴权 @Around 后
  日志 @AfterReturning
  日志 @After
日志 @Around 后
返回 OK

Probe 是上面打印类型的 BeanPostProcessor,它排在 AnnotationAwareAspectJAutoProxyCreator 之后,所以在“初始化后”看到的已经是代理。

A5 事务与数据库连接

环境同 A2。conn() 在当前线程上用 JdbcTemplate 依次查询 CONNECTION_ID()、@@autocommit、@@transaction_isolation,并用 TransactionSynchronizationManager.isActualTransactionActive() 判断是否在事务中:

@Transactional public void create() throws Exception {          // OrderService
    System.out.println("  OrderService.create() 第 1 条 SQL:" + conn());
    System.out.println("  OrderService.create() 第 2 条 SQL:" + conn());
    System.out.println("  绑定在当前线程上的资源:" + TransactionSynchronizationManager.getResourceMap().values().stream()
            .map(v -> v.getClass().getSimpleName()).toList());
    stock.deduct();              // StockService,@Transactional
    log.write();                 // LogService,@Transactional(propagation = Propagation.REQUIRES_NEW)
    Thread t = new Thread(() -> System.out.println("  新线程中:" + conn()));
    t.start(); t.join();
}

@Transactional(isolation = Isolation.READ_COMMITTED) public void report() {
    System.out.println("  report()(READ_COMMITTED):" + conn());
}
自动代理:AnnotationAwareAspectJAutoProxyCreator
事务外:连接 5435,autocommit=1,隔离级别 REPEATABLE-READ,不在事务中
调用 create():
  OrderService.create() 第 1 条 SQL:连接 5435,autocommit=0,隔离级别 REPEATABLE-READ,在事务中
  OrderService.create() 第 2 条 SQL:连接 5435,autocommit=0,隔离级别 REPEATABLE-READ,在事务中
  绑定在当前线程上的资源:[ConnectionHolder]
  StockService.deduct()(REQUIRED):连接 5435,autocommit=0,隔离级别 REPEATABLE-READ,在事务中
  LogService.write()(REQUIRES_NEW):连接 5436,autocommit=0,隔离级别 REPEATABLE-READ,在事务中
  新线程中:连接 5436,autocommit=1,隔离级别 REPEATABLE-READ,不在事务中
调用 report():
  report()(READ_COMMITTED):连接 5435,autocommit=0,隔离级别 READ-COMMITTED,在事务中
事务结束后:连接 5435,autocommit=1,隔离级别 REPEATABLE-READ,不在事务中

事务外的连接 5435 是连接池复用的同一个连接。去掉 spring-boot-starter-aop(类路径上没有 AspectJ)后重新运行,第一行变为“自动代理:InfrastructureAdvisorAutoProxyCreator”,其余结果相同(连接 ID 为 5437、5438)。

A6 @Async 与 @Lazy 的循环依赖

环境同 A2,使用 AnnotationConfigApplicationContext(默认允许循环依赖):

public static class AsyncA { @Autowired AsyncB b; @Async public void notifyUser() {} }
public static class AsyncB { @Autowired AsyncA a; }
@Configuration @EnableAsync static class AsyncCfg {
    @Bean AsyncA asyncA() { return new AsyncA(); } @Bean AsyncB asyncB() { return new AsyncB(); }
}

// 对照:结构相同,A 的方法换成 @Transactional,配置类加 @EnableTransactionManagement

public static class CA { final CB b; CA(CB b) { this.b = b; } public CB b() { return b; } }
public static class CB { final CA a; CB(@Lazy CA a) { this.a = a; } }
@Configuration static class LazyCfg {
    @Bean CA ca(CB b) { return new CA(b); }
    @Bean CB cb(@Lazy CA a) { return new CB(a); }
}
@Async 的 Bean 参与循环依赖:启动失败,BeanCurrentlyInCreationException: Error creating bean with name 'asyncA': Bean with name 'asyncA' has been injected into other beans [asyncB] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. ...
@Transactional 的 Bean 参与循环依赖:启动成功
  TxB 中注入的是容器中的代理:true
构造器循环依赖,一侧加 @Lazy:启动成功
  CB 中注入的 CA 是代理:true(CircularDemo$CA$$SpringCGLIB$$0)
  通过它调用 b() 得到的 CB 就是容器中的 CB:true

同样的 @Async 代码在 Spring 6.2.19、7.0.10 上启动成功,A、B 的构造顺序为 A、B、B、A,日志中有一条 Bean 'asyncA' marked for pre-instantiation (not lazy-init) but currently initialized by other thread - skipping it in mainline thread,B 中注入的 A 与容器中的是同一个代理对象。

A7 Spring Boot 的启动过程

Spring Boot 3.3.5(spring-boot-starter-web)、JDK 21,Apple M1 Pro。application.properties 中设置 server.port=8097、关闭 Banner,日志级别为 WARN,只保留 Tomcat 的启动日志。在各个扩展点中打印距 JVM 加载启动类的毫秒数,用 java -jar 运行(代码有删减):

@SpringBootApplication
public class BootApp {
    static final long T0 = System.nanoTime();
    static void log(String s) { System.out.printf("%5d ms  %s%n", (System.nanoTime() - T0) / 1_000_000, s); }
    static String port() {                       // 尝试连接自己的端口
        try (Socket ignored = new Socket("127.0.0.1", 8097)) { return "8097 可以连接"; }
        catch (Exception e) { return "8097 连接被拒绝"; }
    }

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(BootApp.class);
        log("new SpringApplication(),应用类型 " + app.getWebApplicationType());
        app.addListeners(e -> log("事件 " + e.getClass().getSimpleName()));   // AvailabilityChangeEvent 另外打印状态
        app.addInitializers(ctx -> log("ApplicationContextInitializer,上下文类型 " + ctx.getClass().getSimpleName()
                + ",BeanDefinition " + ctx.getBeanFactory().getBeanDefinitionCount() + " 个"));
        ConfigurableApplicationContext ctx = app.run(args);
        log("run() 返回,BeanDefinition " + ctx.getBeanDefinitionCount() + " 个");
        ctx.close();
    }

    @Bean static BeanFactoryPostProcessor countDefinitions() {
        return bf -> log("BeanFactoryPostProcessor,BeanDefinition " + bf.getBeanDefinitionCount() + " 个,含 DispatcherServlet 自动配置:"
                + bf.containsBeanDefinition("org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletConfiguration"));
    }
    @Bean WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> tomcatCustomizer() {
        return f -> log("创建内嵌 Tomcat 的工厂:" + f.getClass().getSimpleName());
    }
    // 另有一个 BeanPostProcessor,在名为 dispatcherServlet 的 Bean 初始化后打印
    @Bean CommandLineRunner warmUp() { return args -> log("CommandLineRunner 执行," + port()); }
}

@Component
class OrderService {
    OrderService() { BootApp.log("创建单例 Bean OrderService," + BootApp.port()); }
}
  146 ms  new SpringApplication(),应用类型 SERVLET
  187 ms  事件 ApplicationStartingEvent
  417 ms  事件 ApplicationEnvironmentPreparedEvent
  494 ms  ApplicationContextInitializer,上下文类型 AnnotationConfigServletWebServerApplicationContext,BeanDefinition 5 个
  495 ms  事件 ApplicationContextInitializedEvent
  527 ms  事件 ApplicationPreparedEvent
  811 ms  BeanFactoryPostProcessor,BeanDefinition 157 个,含 DispatcherServlet 自动配置:true
  897 ms  创建内嵌 Tomcat 的工厂:TomcatServletWebServerFactory
  918 ms  创建 DispatcherServlet,8097 连接被拒绝
[INFO] 15202 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8097 (http)
 1182 ms  创建单例 Bean OrderService,8097 连接被拒绝
[INFO] 15202 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8097 (http) with context path '/'
 1356 ms  事件 ServletWebServerInitializedEvent
 1359 ms  事件 ContextRefreshedEvent
 1360 ms  事件 ApplicationStartedEvent
 1361 ms  事件 AvailabilityChangeEvent CORRECT
 1363 ms  CommandLineRunner 执行,8097 可以连接
 1363 ms  事件 ApplicationReadyEvent
 1363 ms  事件 AvailabilityChangeEvent ACCEPTING_TRAFFIC
 1363 ms  run() 返回,BeanDefinition 158 个
 1364 ms  事件 AvailabilityChangeEvent REFUSING_TRAFFIC
 1364 ms  事件 ContextClosedEvent

加上 -Dstack=true 时,每一行之后打印当时调用栈中 SpringApplication 和容器 refresh() 的方法(从外到内,节选):

ApplicationContextInitializer:SpringApplication.run > prepareContext > applyInitializers
BeanFactoryPostProcessor:SpringApplication.run > refreshContext > AbstractApplicationContext.refresh > invokeBeanFactoryPostProcessors
创建内嵌 Tomcat 的工厂、创建 DispatcherServlet:AbstractApplicationContext.refresh > ServletWebServerApplicationContext.onRefresh > createWebServer
创建单例 Bean OrderService:AbstractApplicationContext.refresh > finishBeanFactoryInitialization
CommandLineRunner:SpringApplication.run > callRunners

打包出的 jar 中 META-INF/MANIFEST.MF 的相关内容:

Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: lab.BootApp
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/

同一个 jar 加上 --debug 启动,日志中的条件评估报告(CONDITIONS EVALUATION REPORT,节选):

Positive matches:
   DispatcherServletAutoConfiguration matched:
      - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)
      - found 'session' scope (OnWebApplicationCondition)
Negative matches:
   DataSourceAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition)
Unconditional classes:
    org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
    ...

spring-boot-autoconfigure 3.3.5 的 imports 文件中共 152 个自动配置类:类级别条件满足的 16 个,没有条件的 6 个,条件不满足的 130 个,其中 122 个是 OnClassCondition。

A8 @Autowired 的多个候选

Spring Framework 6.1.14、JDK 21,Apple M1 Pro,使用 AnnotationConfigApplicationContext。容器中有 alipay、wechat 两个 PayService,wechat 标了 @Primary:

@Configuration
static class Cfg {
    @Bean PayService alipay() { return () -> "alipay"; }
    @Bean @Primary PayService wechat() { return () -> "wechat"; }
    @Bean Client client() { return new Client(); }
}

static class Client {
    @Autowired PayService alipay;        // 字段名与 alipay 相同
    @Autowired PayService payService;    // 字段名不匹配任何 Bean
}
字段 alipay 注入:wechat
字段 payService 注入:wechat

换成 Spring Framework 6.2.19 运行,结果相同。

去掉 @Primary,改为 Alipay 类标 @Priority(1)、Wechat 类标 @Priority(2)(数字越小优先级越高),字段名为 wechat:

6.1.14 注入 Alipay    # @Priority 先于字段名
6.2.19 注入 Wechat    # 字段名先于 @Priority

A9 事务的传播与事件监听

Spring Framework 6.1.14、JDK 21,Apple M1 Pro。事务管理器是一个不连数据库的 AbstractPlatformTransactionManager 子类,在开启、提交、回滚和标记 rollback-only 时打印一行,用来观察 Spring 做出的决定。Inner 和 Outer 是两个 Bean:

static class Inner {
    @Transactional public void failRuntime() { throw new IllegalStateException("库存不足"); }
    @Transactional public void failChecked() throws Exception { throw new Exception("受检异常"); }
}

static class Outer {
    final Inner inner;                     // 构造器注入,省略构造函数

    @Transactional public void catchRuntime() {
        try { inner.failRuntime(); } catch (RuntimeException e) { System.out.println("外层捕获:" + e.getMessage()); }
    }
    @Transactional public void catchChecked() {
        try { inner.failChecked(); } catch (Exception e) { System.out.println("外层捕获:" + e.getMessage()); }
    }
    @Transactional public void selfCallRequiresNew() { requiresNew(); }   // 自调用
    @Transactional(propagation = Propagation.REQUIRES_NEW) public void requiresNew() {
        System.out.println("requiresNew() 执行,当前事务名:" + TransactionSynchronizationManager.getCurrentTransactionName());
    }
}
== 1. 内层(另一个 Bean,REQUIRED)抛 RuntimeException,外层捕获
[tx1] begin
参与者回滚:把外层事务标记为 rollback-only
外层捕获:库存不足
[tx1] rollback
调用方收到:UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
== 2. 内层抛受检异常(未配 rollbackFor),外层捕获
[tx2] begin
外层捕获:受检异常
[tx2] commit
== 3. 外层事务中自调用 REQUIRES_NEW 方法
[tx3] begin
requiresNew() 执行,当前事务名:lab.TxProp$Outer.selfCallRequiresNew
[tx3] commit

第 3 项只有一次 begin,requiresNew() 看到的事务名是外层方法,没有开启新事务。

事件监听使用同一个事务管理器,监听方法标注 @TransactionalEventListener(默认 AFTER_COMMIT),分别在事务方法和普通方法中发布事件:

@Transactional public void payInTx() { publisher.publishEvent(new OrderPaidEvent(1024)); System.out.println("事务方法结束"); }
public void payNoTx() { publisher.publishEvent(new OrderPaidEvent(1025)); System.out.println("非事务方法结束"); }

@TransactionalEventListener
public void on(OrderPaidEvent e) { System.out.println("短信监听收到订单 " + e.orderId()); }
== 在事务中发布
[tx] begin
事务方法结束
[tx] commit
短信监听收到订单 1024
== 不在事务中发布
非事务方法结束

A10 MyBatis 的参数与拦截器的回调

Spring Boot 3.3.5、MyBatis-Spring 3.0、MySQL 8.0.44、JDK 21,Apple M1 Pro。orders 表 200 万行,user_id 上有索引。两个 Mapper 方法的参数都是 String,分别用 #{userId} 和 ${userId},传入 1024 OR 1=1:

==>  Preparing: SELECT id, user_id AS userId, status FROM orders WHERE user_id = ? ORDER BY id LIMIT 3
==> Parameters: 1024 OR 1=1(String)
<==        Row: 1024, 1024, 3
<==        Row: 101024, 1024, 0
<==        Row: 201024, 1024, 0

==>  Preparing: SELECT id, user_id AS userId, status FROM orders WHERE user_id = 1024 OR 1=1 ORDER BY id LIMIT 3
==> Parameters: 
<==        Row: 1, 1, 1
<==        Row: 2, 2, 2
<==        Row: 3, 3, 3

按顺序注册拦截器 A、B,请求参数 deny=1 时 B 的 preHandle 返回 false,/r2/boom 的 Controller 方法抛出 IllegalStateException,没有配置处理它的 @ExceptionHandler(异常被处理时 afterCompletion 收到的 ex 为 null)。三种请求的输出:

# 正常完成,200
A.preHandle -> true
B.preHandle -> true
controller
B.postHandle
A.postHandle
B.afterCompletion ex=null
A.afterCompletion ex=null

# B 拒绝,403
A.preHandle -> true
B.preHandle -> false
A.afterCompletion ex=null

# Controller 抛出异常,500
A.preHandle -> true
B.preHandle -> true
controller 抛出异常
B.afterCompletion ex=IllegalStateException
A.afterCompletion ex=IllegalStateException

A11 无法被代理的方法

Spring Boot 3.3.5(spring-aop 6.1.14,AspectJ Weaver 1.9.22.1)、JDK 21。切面拦截 Svc 的所有方法,在通知中打印方法名;Svc 中通过 @Autowired 注入了 Dep:

@Component public static class Svc {
    @Autowired Dep dep;
    public String normal() { return "normal " + dep.name(); }
    public final String fin() { return "final " + (dep == null ? "dep=null" : dep.name()); }
    public static String stat() { return "static"; }
    public String callsPrivate() { return priv(); }
    private String priv() { return "private"; }
}

@Around("execution(* lab.AopFail.Svc.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("  [切面] " + pjp.getSignature().getName());
    return pjp.proceed();
}

输出:

代理类:AopFail$Svc$$SpringCGLIB$$0
  [切面] normal
normal() -> normal dep
fin() -> final dep=null
stat() -> static
  [切面] callsPrivate
callsPrivate() -> private

final 方法和 static 方法都没有经过切面,fin() 在代理对象上执行,dep 为 null;private 方法 priv() 只在 callsPrivate() 内部被调用,同样没有经过切面。

A12 MyBatis 的结果映射、重载与二级缓存

MyBatis 3.5.14(不使用 Spring)、MySQL 8.0.44、JDK 21,Apple M1 Pro。orders 表中 id = 1 的行为 (1, 1024, 0),Order 的属性为 id、userId、status,SQL 为 SELECT id, user_id, status FROM orders WHERE id = #{id},日志有删减。

列名 user_id 不起别名,分别关闭和开启 mapUnderscoreToCamelCase:

mapUnderscoreToCamelCase=false -> Order{id=1, userId=null, status=0}
mapUnderscoreToCamelCase=true -> Order{id=1, userId=1024, status=0}

接口中有两个同名的 find 方法。两个都加 @Select 时,注册 Mapper 失败;只有 find(int) 加 @Select 时,调用 find("2") 执行的是 find(int) 的 SQL:

IllegalArgumentException: Mapped Statements collection already contains key demo.Main$Overload.find. ...

==>  Preparing: SELECT id, user_id, status FROM orders WHERE id = ?
find("2") -> PersistenceException: class java.lang.String cannot be cast to class java.lang.Integer ...

Mapper 加 @CacheNamespace,依次打开会话 s1~s5 查询 id = 1,s4 中先把 status 改为 5:

s1 查询                    -> 查数据库
s2 查询(s1 未关闭)       -> 查数据库
s3 查询(s1 已关闭)       -> Cache Hit Ratio 0.33,未查数据库
s4 UPDATE 后查询           -> 查数据库,status=5
s5 查询(s4 已提交)       -> Cache Hit Ratio 0.6,未查数据库,status=5

A13 注解声明 Bean 与集合注入

Spring Framework 6.1.14、JDK 21,Apple M1 Pro,使用 AnnotationConfigApplicationContext 和组件扫描。自定义注解 @PayChannel 上标注 @Component,Alipay、Wechat 两个实现类只标 @PayChannel:

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME)
@Component
@interface PayChannel {
    @AliasFor(annotation = Component.class)       // value 作为 Bean 的名称
    String value() default "";
}

@PayChannel("alipay") static class Alipay implements PayService { ... }
@PayChannel("wechat") static class Wechat implements PayService { ... }

@Service static class Checkout {
    @Autowired Map<String, PayService> services;
    @Autowired List<PayService> list;
    @Value("${sms.sign:默认签名}") String sign;      // 冒号后是没有配置时的默认值
}
Service 上的 @Component: @org.springframework.stereotype.Component("")
Map keys: [alipay, wechat]
List size: 2
pay(wechat): wechat
@Value: 默认签名

A14 Spring MVC 的映射与参数解析

Spring Boot 3.3.5(Spring MVC 6.1.14、Tomcat 10.1.31)、JDK 21,Apple M1 Pro。Controller 中有三个方法,启动后从 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 中读出映射表与解析器:

@GetMapping("/orders/{id}")  public Order get(@PathVariable long id) { ... }
@GetMapping("/orders/latest") public Order latest() { ... }
@PostMapping("/orders")       public Order create(@RequestBody Order o) { ... }
[mapping] {POST [/orders]} => create
[mapping] {GET [/orders/{id}]} => get
[mapping] {GET [/orders/latest]} => latest
[resolvers] 27
[HandlerAdapter bean] requestMappingHandlerAdapter RequestMappingHandlerAdapter
[HandlerAdapter bean] handlerFunctionAdapter HandlerFunctionAdapter
[HandlerAdapter bean] httpRequestHandlerAdapter HttpRequestHandlerAdapter
[HandlerAdapter bean] simpleControllerHandlerAdapter SimpleControllerHandlerAdapter

用 curl 发送请求,拦截器的 preHandle 打印匹配到的方法:

GET /orders/1024           200  preHandle -> OrderController#get
GET /orders/latest         200  preHandle -> OrderController#latest
DELETE /orders/1024        405  Request method 'DELETE' is not supported
GET /nope                  404
GET /orders/abc            400  preHandle -> OrderController#get,之后 MethodArgumentTypeMismatchException
POST /orders(JSON)       200  {"id":1024,"userId":1001}
POST /orders(text/plain) 415  Content-Type 'text/plain;charset=UTF-8' is not supported

参考资料