深入 Spring 32:Scheduled 的时间基准、重叠与关闭
任务运行25秒,每10秒调度意味着什么 一条任务配置每10秒执行,实际运行25秒。下一次开始于第10秒、第25秒还是第35秒?回答取决于时间基准和执行器。固定频率描述计划时刻,固定延迟描述前一次完成之后的等待时间;计划到期也不保证立即拿到工作线程。 第二个问题同样需要限定条件:线程池有两条线程,同一个调度方法会不会重叠?单个周期注册与同一方法上的两个独立注册具有不同的约束。把“方法相同”误当成“注册相同”,容易遗漏并发执行。 实验 blog.spring.Chapter32 固定 Spring Framework 6.2.11 和 JDK 21。时间公式使用固定 Instant、固定 Clock 与 Asia/Shanghai 时区;实际调度器用门闩控制重叠,用有界等待观察结束。没有依靠机器负载估计时间精度。 注解注册发生在容器阶段 @EnableScheduling 注册负责扫描的 ScheduledAnnotationBeanPostProcessor。它查找适用方法,将同步方法转换为可调用的调度任务,交给注册器及调度器。Bean 初始化只是发现和注册的一部分,实际运行时点...
深入 Spring 31:Async 的提交、执行与失败观察
返回 Future 时,任务可能还没有开始 订单接口调用一个标记 @Async 的方法,获得 CompletableFuture,随即返回成功。这个动作能够证明什么?只能证明提交路径返回了一个句柄。线程池可能正在执行别的任务,新任务仍留在队列里;稍后的业务执行也可能失败。若执行器已饱和,提交本身就可能抛异常,连这个句柄都拿不到。 把异步流程拆成提交、开始和完成三个时点,错误位置就容易定位。提交拒绝发生在调用线程,目标方法异常发生在工作线程,结果观察则由 Future 或异常处理器承担。它们不应共用一条“异步失败”的无阶段日志。 实验基线为 Spring Framework 6.2.11、JDK 21,源码固定到 4c134254642d88e058aa004bdaf44168e1be7bb2。入口 blog.spring.Chapter31 使用真实的 Spring 异步代理、容量有限的 ThreadPoolTaskExecutor 和 PostgreSQL 18.0 事务。没有依赖 Spring Boot 的线程池自动配置。 注解如何变成执行器提交 启用 @EnableAsy...
深入 Spring 30:MVC 异步超时取消与资源回收
响应已经超时,后台为何还在工作 客户端收到 503,服务器的一项外部生产任务仍停在等待中。稍后任务结束,尝试把结果写回 DeferredResult,却得到 false。请求超时只结束了可向客户端交付结果的窗口,没有自动取得并终止应用自行创建的任务。 相似的误判也发生在断连场景:客户端关闭连接后,服务器不一定立即知道;通常要到读写触及网络错误时才能发现。即使发现了断连,也需要业务代码释放持有的资源。Servlet 异步把请求线程释放出来,并没有把这些生命周期合并成一个自动完成的对象。 本篇使用 Spring Framework 6.2.11,固定源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2,JDK 21 与 Tomcat 10.1.46。完整实验 MvcLab 30 运行真实容器,分别观察 HTTP 响应、重新分派、任务状态及资源计数。断连负例使用真实 Socket 发送 RST,不以取消模拟 Future 代替网络故障。 Callable 与 DeferredResult 的任务归属 Callable 返回值把任务交给 MVC 的...
深入 Spring 29:返回值消息转换与响应提交边界
方法返回对象以后仍可能失败 控制器正常返回一个对象,不代表客户端已经收到成功响应。对象可能找不到满足 Accept 的表示,getter 可能在序列化时抛出异常,写入网络时也可能断开连接。方法返回、转换完成与 HTTP 交换结束,是三个可分别失败的阶段。 反方向也成立:服务端后来抛出 IOException,客户端仍可能已经收到 200 和部分正文。响应头一旦提交,异常处理器无法把已经传出的 200 改成 500。仅靠控制器异常率或最终 Java 异常类型,不能还原客户端看到的协议结果。 本篇固定 Spring Framework 6.2.11,源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2,JDK 21、Tomcat 10.1.46、Jackson 2.18.4。MvcLab 29 分别使用 JDK HttpClient 与原始 Socket 验证完整响应及提交后的部分响应,完整代码在下载工程 mvc-lab。 返回值处理器先于消息转换器 ServletInvocableHandlerMethod 得到 Java 返回值后,交给 H...
深入 Spring 28:校验与异常发生在哪个处理阶段
同样是 400,异常类型为何不同 {bad、{"name":"","quantity":0} 和 ?n=oops 都是错误输入,但没有在同一阶段失败。第一个连 JSON 对象都不能形成,第二个可以形成对象却不符合约束,第三个不能转换成参数声明的整数。用一个笼统的“参数错误”遮住阶段差异,客户端也许仍能识别失败,服务端却难以决定应该修正文、字段值还是转换规则。 控制器方法直接在参数上声明 @Min(1) 时,又可能得到 HandlerMethodValidationException,而不是处理 @Valid @RequestBody 时熟悉的 MethodArgumentNotValidException。只捕获后一种异常的统一处理器,会遗漏同一接口体系中另一条合法的校验路径。 本篇基线为 Spring Framework 6.2.11,固定源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2,JDK 21、Tomcat 10.1.46、Hibernat...
深入 Spring 27:查询参数表单与 JSON 的绑定边界
同一个 DTO 为什么得到不同权限字段 表单接口设置了 name、quantity 两个允许绑定的字段,提交 admin=true 后,DTO 的 admin 仍为 false。把请求换成 JSON,使用同一个 DTO 与同一个 InitBinder,admin 却成为 true。类名相同不意味着对象由同一机制填充;允许字段的配置必须对应实际的数据进入路径。 Spring MVC 的参数解析由有序的 HandlerMethodArgumentResolver 处理。@RequestParam 从请求参数中取值,@ModelAttribute 建立模型对象并进行绑定,@RequestBody 读取正文并交给消息转换器。它们最终都产生 Java 方法实参,却经过不同的构造、转换和错误处理路径。 实验冻结 Spring Framework 6.2.11、JDK 21、Tomcat 10.1.46、Jackson 2.18.4,固定源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2。完整实现位于下载工程 mvc-lab,执行入口 MvcLab 27...
深入 Spring 26:路径方法与媒体类型怎样选择处理器
相近的请求为什么得到不同错误码 GET /hello 成功,POST /hello 返回 405,GET /hello/ 却返回 404。向 /json 发送正文也不保证进入控制器:请求声明 text/plain,而处理器只消费 JSON 时,结果为 415。路径只是匹配条件之一;几个失败发生在不同条件上,状态码正好保留了这种差别。 如果把这些错误全转成“接口不存在”,调用者无法判断该换路径、换方法还是换 Content-Type。排查时也容易把网关改写、容器解码、MVC 匹配和消息转换混在一起。必须先确定请求在哪一层被拒绝,再解释其状态码。 实验使用 JDK 21、Spring Framework 6.2.11、Tomcat 10.1.46,固定源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2。MvcLab 的 26 分支通过真实 HTTP 验证七种请求行为,另直接操作真实 RequestMappingHandlerMapping 验证重复注册失败。后一个场景属于注册阶段,不伪装成客户端请求结果。 映射在启动时建立 RequestMa...
深入 Spring 25:DispatcherServlet 与请求的多次分派
一个请求为什么出现两次拦截日志 接口只被客户端调用一次,拦截器却记录了两次 preHandle,两条日志还来自不同线程。直接按日志条数计算请求量,会把一次异步请求计成两次;直接把第二次处理认定为重试,又会错判业务是否执行了两遍。Servlet 的一次 HTTP 交换可以包含多个分派,分派次数、控制器调用次数与业务调用次数需要分别计数。 Spring MVC 的主要入口是 DispatcherServlet,但它不拥有完整的 HTTP 生命周期。Tomcat 先接收并解析请求,选择 Servlet 和 Filter 链;进入 DispatcherServlet 后,才由 MVC 选择处理器、解析参数、调用控制器并处理返回值。服务方法上的 Spring AOP 还要等调用真正经过代理对象时才进入。这三个扩展点重叠,却没有相同的覆盖范围。 实验固定 Spring Framework 6.2.11,源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2,JDK 21 与嵌入式 Tomcat 10.1.46。完整程序是下载工程 mvc-lab/src/m...
深入 Spring 24:JDBC、ORM 与多个事务管理器的资源边界
同一个方法里的两条写入,为什么只回滚一条 一个方法先用 JdbcTemplate 插入订单,再从连接池直接取得连接插入审计记录,最后标记回滚。订单消失,审计记录仍在。这不需要跨数据库才能发生:同一连接池提供的两条物理连接,就足以产生两个不同的事务结果。 事务管理器协调具体资源。代码位置相邻、使用相同 JDBC URL、位于同一个 Spring Bean,都不能证明两个操作参加了同一个事务。检查事务时,需要把管理器、资源键、连接取得路径以及数据库会话对应起来。 本文固定 Spring Framework 6.2.11、JDK 21.0.11、PostgreSQL 18.0、pgJDBC 42.7.7、HikariCP 5.0.1。ORM 使用独立工程冻结 Hibernate ORM 6.6.15.Final 和 Jakarta Persistence 3.1.0,避免把 ORM 依赖加入全部基础章节。Hibernate 6.6 的官方兼容矩阵列出 Jakarta Persistence 3.1 和 Java 21;这些版本在本章实际组合运行,不表示使用了最新版本。Hibernate...
深入 Spring 23:提交回调与事务事件的保证边界
数据已经提交,调用为什么还能失败 订单事务提交后发送通知,通知代码抛出异常。调用方得到失败响应,却在下一次查询中发现订单已经存在。这两项结果可以同时成立:异常来自提交后的回调,而订单提交已经结束。把“方法抛异常”直接等同于“数据库回滚”,会让重试重复创建订单。 事务事件提供的是阶段绑定。它能把监听器安排在提交前、提交后或回滚后,却不替外部系统持久保存通知,也不自动重试。要判断一次业务是否成功,至少应分开观察数据库终态、回调是否调用、异步任务是否完成和接收方是否确认。本文实验只涉及前三项,没有外部消息接收方。 实验固定 Spring Framework 6.2.11、JDK 21.0.11、PostgreSQL 18.0、pgJDBC 42.7.7 和 HikariCP 5.0.1。源码固定提交 4c134254642d88e058aa004bdaf44168e1be7bb2;完整程序是 examples/spring-framework-lab/src/main/java/blog/spring/Chapter23.java。数据库结果由单独建立的连接读取,避免把当前事务自己的可...
