深入 Spring E06:Kotlin 挂起调用、事务与上下文边界
方法拦截返回时,挂起业务可能还没有完成 一个 suspend 方法插入数据后等待异步结果,等待期间它没有给调用方返回最终业务值。普通 Java 拦截器却可能已经结束 invocation.proceed()。如果把这个返回时点直接当作业务完成,记录的耗时和成功结果就会提前,事务边界也可能解释错误。 实验先构造真实 Spring ProxyFactory 代理,再执行实际 PostgreSQL 事务。前一个场景说明普通环绕拦截的返回时点,后一个场景验证 Spring 协程扩展如何等待事务回调、传播异常并处理取消。两者使用同一个版本基线,但不能把代理适配与事务协议混为一层。 运行使用 JDK 21.0.10、Boot BOM 3.5.6、Framework 6.2.11、Kotlin 1.9.25、kotlinx-coroutines 1.8.1、Reactor Core 3.7.11。数据库为 PostgreSQL 18.0,R2DBC 驱动 1.0.7.RELEASE,连接池 1.0.2.RELEASE。版本来自实际 Maven 依赖树;不能将滚动文档中的后续版本行为直接套用。 ...
深入 Spring E05:虚拟线程、MVC 与 WebFlux 的资源上限
能接受更多任务,不代表数据库能多执行查询 同一条需要等待数据库的请求,分别使用 MVC 平台线程、MVC 虚拟线程和 WebFlux。三个入口都返回字符串 42,数据库连接上限都为 2。每轮同时发出 12 个 HTTP 请求,三个模式都出现资源等待;虚拟线程使等待任务更容易进入应用,却没有把两条连接变成十二条。 这个实验关注业务结果、资源租约、等待和取消终态,不给框架排性能名次。线程模式改变了应用在等待时如何占用执行资源,但 SQL 本身的耗时、数据库连接容量以及请求进入速率仍然存在。 运行基线为 JDK 21.0.10、Boot 3.5.6、Framework 6.2.11。MVC 使用 Tomcat 10.1.46 和 pgJDBC 42.7.7,WebFlux 使用 Reactor Netty 1.2.10、Reactor Core 3.7.11 与 r2dbc-postgresql 1.0.7.RELEASE。JDBC 池为 HikariCP 6.3.3,响应式池为 r2dbc-pool 1.0.2.RELEASE。它们都由同一个 Boot BOM 管理,完整实际依赖树随...
深入 Spring E04:XA 恢复与 outbox 的一致性承诺
第二个提交失败,第一次提交怎么办 订单数据库提交成功,库存数据库的连接随即断开。此时依次执行两个本地事务的代码,已经无法靠第二个 rollback 撤销第一个 commit。即使外层方法使用一个 Spring 事务注解,两个独立资源也不会因此自动获得同一个提交决定。 本篇分别运行 XA 两阶段提交和 outbox。前者让两个参与资源在最终决定前进入可恢复的 prepared 状态;后者先在一个本地事务内提交业务和待发送记录,再通过可重试投递推进接收端。它们约束的边界不同,验收结果也必须分别写出数据库、投递和接收端终态。 XA 实验使用 JDK 21、pgJDBC 42.7.7、PostgreSQL 18.0,在独立集群中创建两个数据库。二者使用不同 XAConnection,驱动 isSameRM 返回 false。它们是同一 PostgreSQL 进程里的两个数据库资源,没有模拟跨机网络分区或独立服务器故障。 协调代码只支持固定的两个分支和明确的进程中断窗口。它直接使用 PGXADataSource、XAResource 与文件中的提交决定,是用于观察协议的教学协调器;没有实现...
深入 Spring E08:最小容器的对象图、代理与失败契约
注册、注入和拦截为什么仍有这么多边界 把类型放进Map、反射调用构造器、用动态代理包一层,就能运行一个很小的依赖注入容器。真正需要解释的是对象何时进入缓存、依赖得到原始对象还是代理、构造失败以后下一次查询会发生什么,以及同一个对象从不同入口调用时哪些方法经过拦截器。 E08把容器缩减到三个功能:显式注册一个接口到一个实现、通过唯一public构造器注入依赖、为最终接口引用安装一个拦截器。完整代码在下载工程的minimal-lab/MinimalContainerLab.java,只有JDK21依赖。这里的目标是用可以运行的差异解释成熟容器的责任范围。 Spring对照基线仍为6.2.11,源码固定在提交4c134254642d88e058aa004bdaf44168e1be7bb2。本练习没有启动Spring,也没有重新运行上游测试;Spring侧的真实对象图与代理实验分别见03、05、07、14–18。本篇实际运行最小容器,得到21条断言和退出码0。 一个接口对应一个定义,定义先于实例 注册表保存Map<Class<?>, Class<?>>...
深入 Spring 40:观测事件、资源等待与业务终态
HTTP 成功、SQL 成功与订单存在可以不一致 一次真实 HTTP 请求返回 200,Actuator 的请求指标记录 SUCCESS,SQL 日志包含成功执行的 INSERT,独立数据库会话却查不到订单。这个结果不需要网络故障:请求代码最后显式 rollback,同时正常返回字符串,所有表面现象都符合各自的语义。 观测的难点是不同记录只覆盖不同边界。请求指标覆盖 HTTP 处理,SQL 完成覆盖语句执行,事务终态决定写入是否保留。要解释订单为何不存在,需要将这些记录关联到同一操作,而不能从某一条“成功”推断整体业务结果。 本篇在一个合成请求内注入数据库休眠、连接池等待和线程池拒绝,另开一次独立启动失败场景。运行基线为 JDK 21、Boot 3.5.6、Framework 6.2.11、Tomcat 10.1.46、Micrometer 1.15.4、HikariCP 6.3.3、pgJDBC 42.7.7、真实 PostgreSQL 18.0。HTTP 使用本机随机端口,数据库只操作专用 spring_ch40_orders 表中的合成行。 实验故障均为主动构造,不是生产事...
深入 Spring 39:导入、选择、注册与容器扩展边界
增加一个策略,是否需要一个框架 订单应用需要一个限额为 7 的策略。直接写 @Bean OrderPolicy registeredPolicy() 就能完成注册、构造器调用、依赖注入和销毁管理。增加一个 @EnableOrderPolicy 注解后,最终对象仍然是同一个 record 类型,使用方的业务能力没有增加。 扩展有价值的条件,是声明来源和注册规则出现重复:多个应用需要按同一种注解协议启用一组定义,或者注册数量与导入类元数据有关。只有一个应用、一个固定对象时,普通配置已经表达了全部事实。多加 Selector 和 Registrar 会增加故障发生的阶段与定位成本。 本篇构造一个刻意很小的扩展,并保留普通 Bean 对照。它只注册一个名字固定的策略,遇到独立导入者争用名称时立即失败,不提供扫描全包、动态脚本、自动发现所有策略或通用插件平台。这个限制让重复导入与名称冲突可以在十条断言中解释清楚。 实验使用 JDK 21、Framework 6.2.11,Framework 固定源码提交为 4c134254642d88e058aa004bdaf44168e1be7bb2。自...
深入 Spring 38:Boot 自动配置的输入、退让与属性绑定
同一个策略类型,为什么加一个 Bean 就变了 订单应用没有声明 OrderPolicy 时,容器里存在一个来源为 auto 的策略。应用新增名叫 customPolicy 的 Bean 后,策略来源变成 user,原先的 orderPolicy 名称也查不到。两者名字不同,Bean definition 覆盖开关保持关闭,应用仍然正常启动。 这个结果说明,自动配置的退让发生在默认定义注册之前。它不能用“后一个同名 Bean 覆盖前一个”解释,也不需要把 Bean 覆盖开关改成允许。判断依据是策略类型与条件评估时已经可见的定义。 Spring Boot 没有重新实现一套依赖注入容器。配置解析、对象创建、后处理器与销毁仍由 Framework 执行。Boot 提供启动流程、配置数据处理、条件化基础设施装配和运行管理。在原生容器里已经定位过的问题,可以继续沿定义来源、候选类型、创建阶段和调用对象追踪;新增的是这些定义为什么被导入。 本篇冻结 Boot 3.5.6,源码提交 23bcd7b4d8969cdef27771f1594b044bb98724a6。独立 boot-lab 使用该...
Scala 36:纯业务规则接入文件与异步服务
把一个订单文件变成可判定的进程结果 本章完成一个小型批量订单校验 CLI。输入文件每行包含订单标识和数量,例如 A,2。数量必须是一到一百;所有行合法后,程序通过显式报价接口取得金额并汇总。正常输入输出已接受条数和总额,非法输入、报价失败、文件错误使用不同退出码。 这组需求会同时触及纯函数、错误模型、文件生命周期、Future 与进程边界。把所有逻辑写在 main 里虽然能运行,但很难分别验证“规则正确”“外部依赖失败被翻译”“资源关闭”“退出码正确”。本章保留少量明确函数,不预先引入通用效果框架。 实验使用 Scala 3.3.7、Scala CLI 1.9.1、JDK 21.0.11。报价端是可注入故障的假实现,不访问真实网络。文件则真实创建、读取和删除,CLI以独立子进程运行,因此退出码和标准输出不是通过直接调用函数推测出来的。 最终四种场景分别得到零、二、三、四。每个场景都保存原始命令、stdout、stderr和退出码,便于检查它们是否互相一致。正常路径的 accepted=2 total=500 对应两条数量分别为二和三的订单,单价固定为一百个最小货币单位。 纯核心只...
深入 Spring 34:线程上下文的捕获、恢复与资源边界
请求 B 的日志为什么出现请求 A 的标识 线程池复用线程,也复用尚未清理的 ThreadLocal 值。请求 A 在工作线程写入追踪标识,任务结束时没有移除;请求 B 恰好复用这条线程,读到的仍可能是 A。这个错误并不要求两个请求同时执行,连续请求就足以触发。 另一端的错误是完全没有传播。请求线程里能读到标识,提交到线程池后却变成 null。自动复制所有线程变量又会产生更危险的问题:请求对象、数据库连接和事务同步状态有各自的生命周期,不能因为都叫“上下文”就一起搬到另一个线程。 blog.spring.Chapter34 使用 JDK 21、Spring Framework 6.2.11、真实 PostgreSQL 18.0,分别观察普通 ThreadLocal、Spring 请求 holder 和 JDBC 事务绑定。HTTP 部分使用 JDK HttpServer 与真实 HttpClient 回环请求,手动绑定 RequestAttributes 测试夹具;它不是 Spring MVC DispatcherServlet 实验,也没有使用日志框架 MDC。 Thread...
深入 Spring 33:缓存键、并发加载与事务回滚
数据库回滚,缓存为什么还保留新值 一次操作写入数据库,再通过 @CachePut 更新价格缓存,最后把事务标记为 rollback-only。独立连接确认数据库里没有新记录,缓存里却能读到 uncommitted。这个结果不需要框架出错:普通内存缓存与 JDBC 事务管理器没有共同的提交协议。 缓存一致性至少涉及键空间、加载并发和提交边界。代理命中只能说明某次调用找到了缓存条目,不能证明条目仍然新鲜,也不能证明它来自已经提交的数据库状态。 实验 blog.spring.Chapter33 固定 Spring Framework 6.2.11、JDK 21、ConcurrentMapCacheManager,数据库使用 PostgreSQL 18.0。所有缓存结论都限定在这个 provider 和单 JVM 内;没有用本地 sync=true 推导 Redis 集群的行为。 CacheInterceptor 处理调用,provider 保存数据 @EnableCaching 注册缓存 Advisor,CacheInterceptor 把代理调用委托给 CacheAspectSup...
