深入 Spring 22:回滚规则、异常捕获与rollback-only
抛出异常与数据库回滚不是同一个结果 事务方法插入一行后抛出 checked exception,调用方捕获到异常,数据库却保留了那一行。另一个事务方法捕获内部异常并正常返回,调用方反而收到 UnexpectedRollbackException,数据库没有留下记录。两个结果都符合 Spring 的默认行为,它们分别涉及回滚规则与共享事务的 rollback-only 状态。 默认规则把 RuntimeException 和 Error 视为回滚原因,普通 checked exception 不触发回滚。这个规则处理的是穿过事务拦截边界的异常;在方法内部已捕获的异常不会自动传递给拦截器。同时,方法正常返回也不能清除已存在的 rollback-only 标志。默认事务属性源码 本文固定 Spring Framework 6.2.11、JDK 21,源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2。PostgreSQL 18.0、pgjdbc 42.7.7、HikariCP 5.0.1 提供真实数据库与连接池。每个场景都通过独立连接读取已提交记...
深入 Spring 21:事务传播、保存点与连接池等待
内层成功不等于整个业务成功 外层事务先写订单,内层写审计记录。内层正常返回后,外层抛出异常。审计记录是否保留,取决于内部范围是否拥有独立物理事务:REQUIRED 的写入随外层一起撤销;REQUIRES_NEW 可以已经提交;NESTED 即使成功释放保存点,也仍受外层最终回滚约束。 方法嵌套只描述 Java 调用结构。解释事务传播,还要同时画出逻辑范围、物理事务、数据库连接三条线。一条连接在外层挂起期间仍被借出,内部的新逻辑范围也不必然对应新连接。混淆这两个方向,会同时误判数据原子性和连接池容量。 本文固定 Spring Framework 6.2.11、源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2,使用 JDK 21、PostgreSQL 18.0、pgjdbc 42.7.7、HikariCP 5.0.1。实验通过 TransactionTemplate 直接指定传播,排除自调用是否穿过代理这一变量。第 20 篇声明式入口在解析属性后调用同一个事务管理器;以下结论仍要求那个入口真实生效。 三条时间线如何对应 AbstractPl...
深入 Spring 20:声明式事务的方法属性与代理入口
同一个注解为何产生不同事务 一个订单服务在类上声明只读事务,在写入方法上另选事务管理器。外部调用写入方法时,数据库能够提交;目标对象用 this 调用另一个事务方法时,却可能没有事务。排查这种差异,需要分别回答三个问题:调用是否进入代理、方法最终解析出什么属性、选中的管理器实际控制哪份资源。 注解只是方法元数据。第 19 篇的 JDBC 事务由管理器取得连接、绑定线程、完成提交或回滚;声明式入口增加了一层拦截逻辑,把方法元数据转换成管理器参数。数据库提交并不发生在读取注解时,也不发生在目标方法的最后一行,而发生在代理控制的调用边界上。 本文固定 Spring Framework 6.2.11、JDK 21,源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2。实验使用 PostgreSQL 18.0、pgjdbc 42.7.7、HikariCP 5.0.1。两个连接池连接同一实验数据库,用于辨认管理器选择;它们不是分布式事务,也不模拟两个数据库的原子提交。 从方法入口到事务属性 TransactionInterceptor.invoke 把...
深入 Spring 19:编程式事务、连接绑定与提交结果
回调成功,订单可能没有提交 一次下单操作在事务回调里写入订单,返回金额 25.00。单元测试看到返回值正确,日志显示 INSERT 执行成功,另一个数据库会话却查不到订单。这些观察可以同时成立:回调可能设置了 rollback-only,也可能只是参加外层尚未完成的事务。方法返回、SQL完成和事务提交是三个不同的时点。 另一种更危险的情况是订单被回滚,通知记录却留在数据库。假如通知代码从原始连接池另取了一条开启 autocommit 的连接,它就没有使用当前事务绑定的连接。两个方法在同一个线程里运行,不足以证明它们属于同一个物理事务。 本篇通过普通 JDBC 和 TransactionTemplate 对照这两个问题。基线为 Spring Framework 6.2.11,源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2;应用使用 JDK 21、pgJDBC 42.7.7、HikariCP 5.0.1 与真实 PostgreSQL 18.0。所有数据库结果均由独立 DriverManager 连接在 READ COMMITTED 下查询。...
深入 Spring 18:自调用、目标引用与方法元数据
注解没变,调用经过的对象变了 订单服务的 inner 方法有边界注解,outer 方法内部执行 this.inner()。外部调用 proxy.inner() 能进入通知,调用 proxy.outer() 却不会为内部 inner 再执行一次通知。两个路径执行同一个业务方法,得到同一个 order 字符串,但代理边界不同。 这里不能用“注解失效”代替原因。注解只是方法元数据;只有相应基础设施读取它、生成通知,并且调用进入这个通知所在的代理,元数据才会影响运行。本实验使用自定义 @Boundary 与计数拦截器把问题缩到一个变量:调用经过哪个对象。 固定环境为 Spring Framework 6.2.11、JDK 21,源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2。代码位于 examples/spring-framework-lab/src/main/java/blog/spring/Chapter18.java。JDK 与 CGLIB 各执行相同六项调用断言,另有泛型桥接解析实验。 从代理入口追到目标上的 this 实验中的服务只...
深入 Spring 17:proceed 与拦截器链的返回和异常
两个 around 怎样改变同一个返回值 订单方法返回字符串 order。A 通知把结果包装成 A(结果),B 通知包装成 B(结果)。按照 A、B 的链顺序执行,调用者得到 A(B(order));把链顺序改成 B、A,结果变成 B(A(order))。通知顺序不只决定日志先后,也可以改变业务结果。 实验固定 Spring Framework 6.2.11、JDK 21,源码 SHA 4c134254642d88e058aa004bdaf44168e1be7bb2。完整入口是 examples/spring-framework-lab/src/main/java/blog/spring/Chapter17.java。它用 AOP Alliance MethodInterceptor 表达 around 语义,随后对照 AspectJ around 的适配路径;没有把两套 proceed API 的所有细节当成完全相同。 proceed 推进的是同一条调用链 ReflectiveMethodInvocation 保存 proxy、target、方法、参数、目标类和拦截器列表。c...
深入 Spring 16:切点的静态筛选与运行时匹配
实参是 String,为什么 execution 不匹配 订单接口有一个 Object submit(Object input) 方法。调用者传入字符串 "order",args(java.lang.String) 的通知执行了,execution(* submit(java.lang.String)) 的通知没有执行。两条表达式观察的对象不同:前者约束这次调用的实参类型,后者约束方法声明的签名。 这个差异使切点成为可以验证的筛选算法。只盯着表达式的英文含义,会把声明类型、运行时类型、目标类型和代理类型混为一谈。本实验把方法固定为 submit(Object),分别改变表达式、实参和代理种类,观察通知计数。 环境为 Spring Framework 6.2.11、AspectJ Weaver 1.9.22.1、JDK 21。这里使用 AspectJ 的表达式解析器,没有启动字节码 weaving。Spring 源码 SHA 为 4c134254642d88e058aa004bdaf44168e1be7bb2,完整入口为 examples/spring-fram...
深入 Spring 15:Advisor 怎样进入 Bean 的创建过程
同一个订单服务为何取得的是代理 new OrderTarget() 返回业务对象,容器 getBean("orders") 却可能返回代理。Advisor 没有修改 Java 的 new 指令;自动代理创建器作为 BeanPostProcessor 参与实例暴露,决定交给调用者的引用。 本实验先手动 ProxyFactory 包装一次,再把相同 Advisor 交给 DefaultAdvisorAutoProxyCreator,让容器创建订单与观察者两个 Bean。观察者回指订单,订单又持有观察者,强制出现早期引用需求。可证伪的问题是:观察者注入的早期对象是否与 refresh 后查到的最终对象相同?若只是数通知执行了几次,无法回答对象图是否一致。 环境固定 Spring Framework 6.2.11、JDK 21,源码 SHA 为 4c134254642d88e058aa004bdaf44168e1be7bb2。可运行代码为 examples/spring-framework-lab/src/main/java/blog/spring/Chapter15...
深入 Spring 14:JDK 与 CGLIB 代理的拦截边界
final 方法为何在两种代理中得到不同结果 订单接口声明了 fixed(),实现类把它声明成 final。同样一个计数通知,通过 JDK 代理调用时计数增加,通过 CGLIB 代理调用时计数不增加。不能据此判断 Spring 偶尔忽略了 final,也不能把“final 不能被代理”当成不带条件的规则。 JDK 代理实现接口,接住接口调用以后委托目标对象;它不需要覆盖实现类的 final 方法。类代理依赖子类覆盖可以覆盖的方法,把调用转入拦截器。final 限制覆盖,private 方法不被子类继承,这两种限制与选用的代理入口直接相关。JLS 21 方法覆盖与 final 本文要验证的问题是:改变代理策略后,哪些调用确实经过通知,哪些只在类型上看似相同?实验固定 Spring Framework 6.2.11、JDK 21,源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2。完整入口为 examples/spring-framework-lab/src/main/java/blog/spring/Chapter14.java。 把接口、...
深入 Spring 13:启停顺序、层级查找与失败后的资源终态
Bean 已经创建,工作线程是否已经可用 订单服务依赖一个后台通知 Worker。Worker 对象构造成功,并不意味着线程池已经启动;close 日志出现,也不意味着任务线程已经结束。对象存在、组件运行、资源终止是三个状态,需要各自的入口和证据。 Spring 的初始化回调处理对象准备,Lifecycle 提供显式 start/stop,SmartLifecycle 再增加自动启动、phase 和带完成回调的停止契约。把业务启动放在哪一层,会改变失败时点和依赖约束。本文用真实 ExecutorService,而不是只打印“启动/停止”的空方法,检查容器启动与关闭对资源的影响。生命周期官方契约 实验固定 Spring Framework 6.2.11、JDK 21,源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2。完整代码位于 examples/spring-framework-lab/src/main/java/blog/spring/Chapter13.java。每个 Worker 持有独立线程池,start 提交一次任务并限时等待,...
