深入 Spring 42:AOT 生成代码与运行时配置的边界
启动参数出现 red,容器里的 Bean 仍然是 blue 配置类定义两个同类型 Bean:@Profile("blue") 返回 blue,@Profile("red") 返回 red。普通 JVM 分别带 blue、red 启动,容器里也分别出现对应 Bean。用 blue 生成 Spring AOT 产物后,再给 AOT JVM 传 red,实验仍取得 blue Bean。 日志中有效 profile 是 blue,red,这并不矛盾。环境属性仍可在启动时参与处理,已经生成的 Bean 注册代码却不会重新执行一次任意的配置类发现与条件选择。把“Environment 中出现了某个值”当作“容器根据该值重建了 Bean 集合”,会误判 AOT 应用的装配行为。 实验固定 Framework 6.2.11、Boot 3.5.6 与 JDK 21。final-lab/AotApp 是独立的非 Web 入口,避免把数据库连接和服务器启动引入 AOT 基础实验;它与订单入口共享 Maven 模块和冻结依赖。普通 JVM、生成代码、AOT JVM...
深入 Spring 41:测试边界与提交回调的盲区
订单测试通过,提交后的通知仍然失败 订单方法在事务中插入数据,并注册一个 afterCommit 回调。测试调用该方法,检查插入成功,最后由 Spring Test 自动回滚。整组测试通过,但真实 HTTP 请求一提交就抛异常。这两个结果可以同时成立:测试从未走过提交路径,回调里的错误也从未执行。 测试的价值取决于实际经过的边界。纯函数断言证明金额规则;MockMvc 证明参数绑定和异常到响应的映射;真实服务器加独立数据库连接证明请求线程完成了提交。把三类结果都写成“订单接口测试通过”,会丢掉最需要保留的条件。 实验固定 Spring Framework 6.2.11、Spring Boot 3.5.6、JDK 21 和 PostgreSQL 18.0。Boot BOM 管理本模块依赖,数据库使用独立 spring_final schema。JUnit 实际执行 8 个测试方法,产生 15 条具名断言,没有将手写演示输出当作 JUnit 报告。依赖清单、XML 报告与原始输出随实验工程保留。 从同一个金额错误区分三种证明 业务规则 Store.validate(amount) ...
深入 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 使用该...
深入 Spring 37:HTTP 客户端的超时、重试与连接释放
超时之后,订单究竟有没有写入 客户端发送 POST,下游已经完成写入,却返回 503。调用端增加一次 retry,第二次得到 200,日志最终显示成功。若只统计最终响应,会漏掉两次真实请求和两次业务写入。HTTP 调用失败描述的是一次观察结果,不等于下游没有产生副作用。 另一个容易混淆的问题是“已经设了超时”。连接池等待、TCP 建连、TLS 握手、等待响应数据和整条业务链的时限分别属于不同阶段。一个响应超时不能自动覆盖 DNS、连接池等待和重试总时长;上层调用结束也不能单独证明连接已经释放。 Chapter37 在本机运行真实 Reactor Netty HTTP 服务,注入无响应、503、提前断开和写入后返回错误。客户端分别使用 RestClient、WebClient 与 HTTP Interface 代理。运行基线为 Spring Framework 6.2.11、Reactor Netty 1.2.10、Reactor Core 3.7.11 与 JDK 21,源码在下载工程独立的 reactive-lab 模块中。 三个入口仍然需要具体传输实现 RestClient...
深入 Spring 36:响应式事务、取消窗口与重新订阅
没有收到完成信号,能否认定订单被回滚 客户端取消请求后,应用没有收到 onComplete,日志也没有打印“下单成功”。这只能描述订阅者看到的信号,不能直接描述数据库状态。取消可能发生在 SQL 之前、SQL 执行之后、事务提交之前,也可能发生在已经提交但响应尚未到达时。不同窗口的业务结果不同。 响应式事务还增加了另一个容易遗漏的边界:Publisher 对象不等于一次事务执行。同一个冷 Publisher 被订阅两次,可以启动两次事务并执行两次 INSERT。第二次执行各自拥有正常提交结果,仍可能违反订单只能创建一次的业务要求。 实验固定 Spring Framework 6.2.11、Reactor 3.7.11、R2DBC PostgreSQL 1.0.7.RELEASE、R2DBC Pool 1.0.2.RELEASE、PostgreSQL 18.0 和 JDK 21。Chapter36 使用真实 R2DBC 驱动写库,以独立 pgJDBC 连接观察数据;池容量设为一,以验证资源归还。没有用内存数据库或假事务管理器代替提交、回滚和连接释放。 事务资源依附于订阅上下文 普...
深入 Spring 35:WebFlux 的订阅、需求与阻塞边界
返回 Mono,为什么仍能阻塞服务器 一个控制器返回 Mono<String>,调用链上没有 Future.get(),响应也能正常到达。这些现象不足以证明请求处理没有阻塞。Mono.fromCallable 可以包装 JDBC 查询、文件读取或者任意同步等待;如果没有安排执行位置,订阅线程仍然执行这段 Callable。包装改变了计算的触发方式,没有改变计算的性质。 判断响应式路径,需要分别回答三个问题:谁发起订阅,谁请求数据,具体工作在哪个线程执行。一个请求里可能有多个 Publisher、多次线程切换和多个资源生命周期。只看方法签名,无法推出这些事实。 实验使用 Spring Framework 6.2.11、Reactor Core 3.7.11、Reactor Netty 1.2.10、Tomcat 10.1.46 和 JDK 21。两个真实 HTTP 服务器返回相同订单文本;独立订阅实验检查需求;受控阻塞实验检查单个事件循环被占用后的行为。入口是下载工程 reactive-lab 中的 blog.spring.reactive.Chapter35,完整源码...
深入 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...
