设计模式 00:可工作的报价程序为何难修改
一家只售卖合成商品 paper 的教学商店,订单行单价 50.00、数量 2。现有 Before.quote 给出 100.00,Before.receiptTotal 也给出 100.00。两个结果都对。新增会员九折后,两条路径仍各自做 unitPrice × quantity:报价和收据会同时给出错误的 100.00,而需要的是 90.00。能计算旧订单,不代表容易一致地修改新规则。 本篇先建立可执行的行为基线,只解决折扣规则在两个入口重复的问题。不预建支付接口、工厂和其他模式:目前没有这些变化需求。 先界定行为,再讨论改法 工程位于 examples/design-patterns/,执行 ./mvnw -B -ntp verify。order-core 的 OrderLine 是报价输入,记录 SKU、两位小数且非负的单价、正整数数量和客户类型;这里的金额均约定为同一币种的教学金额,不处理跨币种兑换。BigDecimal 直接构造自十进制字符串;九折先做乘法,再按 HALF_UP 舍入到两位小数。这是本案例的舍入规则,并非所有支付系统的统一规则。 原行为契约对 Befor...
深入 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 E07:从 6.2 到 7.x 的兼容矩阵与行为迁移
编译通过以后,响应仍可能改变 一个返回普通 Java Bean 的接口,升级后仍然返回 200,字段值没有变化,JSON 字段顺序却改变了。另一个返回 record 的接口,在同一组版本中保持原来的顺序。把迁移结果概括成“Jackson 3 会把所有 JSON 按字母排序”,无法解释这两个实际响应。 框架升级同时涉及依赖集合、Java API、自动配置与对外协议。依赖树解决的是哪些类型被加载,编译解决的是源码是否接受新 API,HTTP 回归才检查客户端收到什么。任何单层结果都不足以替代其余几层。 这组实验使用两个独立 Maven POM,分别编译同一份契约探针,再启动两个独立 JVM。旧版本不会因为新版本需要某个依赖而被调整,新版本也没有通过手工覆盖 Spring 补丁号拼出一个未经验证的集合。结果只属于下面冻结的历史组合,不是当前生产升级推荐。 先冻结版本集合,再讨论迁移 两组工程都使用 JDK 21.0.11。旧工程继承 Boot 3.5.6,新工程继承 Boot 4.0.0;Framework、Tomcat、Jackson 和 Bean Validation 的实际版本...
深入 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 E03:Repository 代理如何叠加查询与事务
接口没有实现类,事务却不能省略归属 继承 JpaRepository 后,saveAndFlush 可以把实体写入数据库;自定义 findByLabel 能执行查询;增加一个带 @Modifying 的更新方法,却可能立即抛出事务异常。这三个方法都从同一个 Repository 引用调用,默认事务行为仍然不同。 Repository 接口描述的操作由代理、查询执行器与基础实现共同完成。事务配置是这条调用链中的另一层规则,不会因为一个接口继承 JpaRepository,就自动给每个后来声明的方法赋予相同事务属性。 实验固定 JDK 21、Boot 3.5.6 BOM、Framework 6.2.11、Spring Data JPA/Commons 3.5.4、Hibernate 6.6.29.Final、pgJDBC 42.7.7 和 PostgreSQL 18.0。独立 ecosystem-lab 使用这组实际解析依赖,不复用主线 ORM 实验中另一个 Hibernate 补丁版本,也不把两个版本的结果混合。 Repository 代理上叠加了哪些职责 实验声明 Purcha...
深入 Spring E02:Spring Security 的过滤器、授权与方法安全
一个 403 不能说明身份验证失败 同一个管理员账户,GET 请求成功,POST 请求返回 403。用户名和密码完全相同,差异可能来自 CSRF 校验。另一个已经通过认证的读者账户访问管理员地址,也会得到 403,但这次失败来自授权规则。只有状态码,不能定位安全链中的具体拒绝位置。 安全处理至少包含凭证识别、身份认证、资源授权以及与浏览器请求相关的保护。它们使用不同输入,在不同位置失败。把所有拒绝都交给 ControllerAdvice,或者为了让测试变绿而同时关闭 CSRF 和 CORS,会掩盖这些边界。 实验固定 Boot 3.5.6、Framework 6.2.11、Security 6.5.5、Tomcat 10.1.46 和 JDK 21,由 Boot BOM 管理兼容版本。服务只在实验期间监听随机端口,使用 Java HttpClient 发送实际 HTTP。账户 reader/admin 和 {noop}pw 仅用于可复现断言;这里验证的是处理机制,真实服务还需要合适的密码编码、TLS 和账户管理。 容器 Filter 链怎样进入 Securi...
深入 Spring E01:AspectJ weaving 与代理之外的连接点
同一次自调用,通知为何会出现两种结果 outer() 调用同一个对象的 inner()。Spring 代理可以通知外部进入的 outer,却不会因此再次经过代理来通知目标对象内部的 inner。换成 AspectJ 编译期织入,inner 方法体内的通知调用已经存在于 class 文件中,内部调用也会执行它。 这个差异来自通知代码所在的位置。代理要求调用穿过一个额外对象;织入会改写选定类的字节码。只看到 @Aspect 注解,无法判断使用了哪一种机制;Spring 对该注解的支持仍然可以完全基于代理。 实验固定 JDK 21、Spring Framework 6.2.11 和 AspectJ 1.9.22.1。独立的 ecosystem-lab 使用 Boot 3.5.6 管理其他依赖,显式把 aspectjtools、aspectjrt、aspectjweaver 统一冻结到 1.9.22.1。版本组合是教学复现基线。AspectJ 官方兼容表列出这一版本支持 Java 22,编译器运行最低要求为 JDK 17;实验使用 -21 输出 Java 21 字节码。AspectJ 兼...
深入 Spring 43:订单应用的六个综合验收场景
一个订单结果需要跨过哪些边界 HTTP 返回 201、数据库出现订单、提交回调执行、异步通知结束,是四个不同的观测结果。请求返回异常时,数据库可能已经提交;客户端超时时,服务器也可能仍在处理。验收把这些结果分别记录,才能判断错误发生在绑定、业务、事务、异步任务还是传输阶段。 综合应用保留此前实验使用的 Spring MVC、JDBC 事务、真实 PostgreSQL、ExecutorService 和 JDK HttpClient。固定 Boot 3.5.6、Framework 6.2.11、JDK 21 与 PostgreSQL 18.0,代码位于 final-lab/。它是围绕同一订单模型的可执行验收应用,没有增加消息中间件、分布式锁或新的编排平台。 独立 JUnit 类 Chapter43Test 通过真实 HTTP 请求和额外数据库连接观察应用,执行正常下单、并发幂等、业务回滚、异步失败、HTTP 超时、关闭六个场景,共 21 条具名断言。数据库写入限定在 spring_final.orders,订单键带本轮 UUID 前缀,清理只删除本次创建的键。 装配固定之后,先确...

