深入理解 Spring WebFlux 与 Reactor Netty:从 Tomcat 到 EventLoop 的思维跃迁
引子:从 Tomcat 说起 Spring MVC 的请求路径非常直观: 浏览器发请求 → Tomcat 接收 → 线程池分配线程 → DispatcherServlet 找到 Controller → 方法执行 → 返回响应。 在 Tomcat 的世界里,一切都有明确的”角色”: Connector:负责 TCP 连接、HTTP 解析。 线程池:一个请求分配一个线程。 Filter:在请求到达 DispatcherServlet 之前、响应返回客户端之前,做预处理和后处理。 CoyoteAdapter:把底层请求适配成 Servlet 请求。 DispatcherServlet:找到对应的 Controller。 Controller:执行业务逻辑,阻塞等待数据库或远程调用,然后返回。 这个模型可以概括为:一个请求,一个线程,从头跑到尾。 换成 Spring WebFlux + Reactor Netty 之后,这些角色似乎消失了。取而代之的是: EventLoop Channel ChannelPipeline ChannelHandler Mono、Flux subs...
Spring 文件下载全方案深度解析
一、一个下载请求,到底卡住了什么? 当我们说”大文件下载把服务搞挂了”,很多人第一反应是磁盘慢、网络慢。但真正先被拖垮的,通常是请求处理线程。 在 Spring MVC + Tomcat 的默认模型中,一个 HTTP 请求从进入 Tomcat 到响应完全写回客户端,全程占用同一个 Tomcat 工作线程。这个线程来自 Tomcat 的线程池,默认最大约 200。如果有一个 1GB 的文件需要下载,这个线程要等到最后一个字节写入 Socket 之后才能归还。这期间它无法处理任何其他请求。200 个这样的并发下载,就能让整个服务不可用。 但问题不止一个。当数据从磁盘流向网卡时,还隐藏着第二个瓶颈:数据在内核态与用户态之间被反复拷贝。你的 Java 程序充当了一个低效的中间人,数据被来回搬运,白白消耗 CPU 和内存带宽。 所以,一个文件下载请求,同时面对两个独立的瓶颈: 瓶颈一:线程占用。 谁在写数据,写了多久,线程池就被占用了多久。 瓶颈二:数据拷贝。 数据从磁盘到网卡,经过了多少次无谓的 CPU 搬运。 Spring 提供的所有方案,以及底层 I/O 优化技术,本质上都在回答...
Spring MVC 中那些“不走寻常路”的返回类型
一、从“普通返回”说起 在 Spring MVC 中,最熟悉的控制器返回值是这样的: 1234@GetMapping("/user")public User getUser() { return userService.findById(1L);} Spring 拿到 User 对象,用 Jackson 序列化成 JSON,写入响应。响应是一次性的、同步的、内容完整的。 这个过程的背后,是 Tomcat 从线程池中分配了一个工作线程,这个线程从接收请求、执行 Controller、序列化结果、写入响应,一直到响应写完,才归还给线程池。 问题在于:如果 findById 需要 30 秒(比如调用一个慢速的 AI 服务),那么这个工作线程就要被占用 30 秒。Tomcat 默认最大线程数是 200,意味着 200 个这样的请求就能把 Tomcat 打满,第 201 个请求只能排队等待。 Spring MVC 提供了一系列特殊返回类型来应对不同的响应模式。它们从不同维度偏离了“普通序列化返回”: 时间维度:结果不是立即产生的,需要异步等待...
Nacos是怎么样和Jasypt结合的?
一、从一个尴尬的场景说起 假设你是一个微服务项目的开发,某天安全部门发来一份整改通知: 检查发现,Nacos 配置中心中 spring.datasource.password 为明文存储,存在敏感信息泄露风险,请于本周内完成整改。 你打开 Nacos 控制台一看: 12345spring: datasource: url: jdbc:mysql://10.64.3.151:3306/cpc username: root password: MySecret@123 # ← 赤裸裸地躺在这里 改就改呗,把密码挪到环境变量里?但运维说不行,环境变量在 K8s 里也是明文;放到 K8s Secret?那还得改部署流程。折腾一圈,同事丢过来一个方案:用 Jasypt 加密,Nacos 里只存密文。 于是配置变成了这样: 123spring: datasource: password: ENC(Q9PNYb+QmZOd9cdlX8b3zXK7S3eGKsQlY5z2sCewoRo=) 应用启动后,程序拿到的是明文 MySecret@123,但 Nacos...
Spring 接入点:跟着容器过一生
我总是记不住 Spring 的接入点,一个字,乱! BeanFactoryPostProcessor、BeanPostProcessor、ApplicationListener、ApplicationContextInitializer、SmartInitializingSingleton、SmartLifecycle……名字都认识,但一到用的时候就乱: 到底谁先谁后?谁改配置?谁改 Bean?谁监听启动?谁负责关闭? 后来我接了一个多租户任务平台,叫 TaskPilot,需求一串: 启动前从配置中心拉租户配置; 每个租户动态注册一个数据源; 给任务处理器加审计代理; 所有任务处理器创建完后,校验任务类型不能重复; 应用启动完成后预热缓存; MQ 消费者要随容器启动,关闭时优雅停机。 做完这个项目,我突然发现: Spring 接入点根本不用背,它们就是容器一生中的一个个插槽。 而且除了“插槽”,还有两条暗线贯穿始终: Aware 线:Bean 在生命周期里认领容器资源; 事件线:容器一生都在广播,谁想听就听。 下面跟着 TaskPilot 的生命周期走一遍。 先看总图:...
Spring自定义Mybatis数据源,到底发生了什么?
在之前的文章中,我们为了在 Spring 中使用多数据源,引入了如下的数据源代码 12345678910111213141516171819202122232425262728293031323334353637@Configuration@MapperScan(sqlSessionTemplateRef = "pgsqlSqlSessionTemplate", basePackages = {"com.chinamobile.cmss.gzzx.framework.base.mapper", "com.chinamobile.cmss.gzzx.cmdb.console.mapper.postgresql"})public class PostgreSQLConfig { @Bean(name = "pgsqlDataSource") @Primary @ConfigurationProperties(pre...
Spring中Mybatis、MybatisPlus是怎么样结合工作的?
引子:一个常见的困惑 在 Spring Boot 项目中,我们几乎每天都在用 MyBatis 或 MyBatis-Plus 操作数据库。只要在 Mapper 接口上写几个方法,在 Service 里 @Autowired 注入,就能直接调用。一切看起来理所当然。 但当你开始追问下面这些问题时,事情就变得有意思了: UserMapper 只是一个接口,没有实现类,为什么能被注入? BaseMapper 里的 insert、selectById 方法,我从来没有写过 SQL,它们是怎么执行的? @MapperScan 到底做了什么?它和 MyBatis-Plus 重写的 MybatisMapperRegistry 是怎么关联上的? MybatisPlusAutoConfiguration 和 @MapperScan 是什么关系? 多数据源时为什么还要配置 sqlSessionFactoryRef 和 sqlSessionTemplateRef? MyBatis-Plus 的 saveBatch 为什么性能不理想?有没有真正的批量插入方案? 这篇文章就沿着这些问题,把 Spri...
SpringBoot 请求链路(三):从方法执行到响应写完
引子:参数都对,为什么还是 500 / 406 / 空响应 方法签名对了,参数也齐了,InvocableHandlerMethod.doInvoke() 那一行执行——从这一刻起,出错的方式彻底变了:不再是 400,而是 500、415、406,或者状态码 200 但 body 一个字都没有。 异常、返回值、异步、写回,这四件事常被当成”框架内部细节”,可线上事故恰恰密集地堆在这里。 本篇接第二篇(参数已就绪、拦截器已放行),覆盖系列导读里的第十~十五节和结语:反射调用与异常解析、返回值处理器与内容协商、异步请求的三根线程、响应从 OutputBuffer 到 Socket 的最后一跳,最后收在扩展点速查表和 Debug 指南上。 本篇要回答的问题: Controller 抛出的异常,最终是谁把它变成 500 或 400 的?优先级怎么排? @ResponseBody 是怎么把对象变成 JSON 的?406 为什么出在返回值阶段? 异步请求为什么会有三根线程?为什么 ThreadLocal、MDC、SecurityContext 会丢? “返回了”为什么不等于”发出去了”...
SpringBoot 请求链路(二):从 DispatcherServlet 到方法调用
引子:所有分岔都发生在一个方法里 DispatcherServlet 是个 HttpServlet,service() → doGet() 那一串都是规范规定的样板代码。真正开始干活只有一行:doDispatch(request, response)。 你后来遇到的大部分现象——404、405、400、406,以及”拦截器没执行”“参数解析器选了别人”——都是这一个方法内部某一步的结果。 它值得逐行读,而不是背五个步骤的名字。 本篇接第一篇(FilterChain 已经放行),覆盖系列导读里的第六~九节:doDispatch() 的完整流程、HandlerMapping 如何把 URL 变成一个 HandlerMethod、27 个参数解析器怎么排队、拦截器的顺序与中断语义。 本篇要回答的问题: doDispatch() 按什么顺序做事?哪一步失败是 404,哪一步是 400? 一个 URL 是怎么匹配到方法的?为什么线上生效的路由顺序和配置文件里写的顺序不一样? 27 个内置参数解析器的先后顺序是怎么定的?自己注册的为什么会被”抢走”? @RequestParam 和 ...
SpringBoot 请求链路(一):请求是怎么到 Tomcat 的
引子:端口是通的,请求却没进来 nc -zv 10.0.1.7 8080 通了,curl 却挂在那里,等满 30 秒超时。登录机器一看,jstack 里 http-nio-8080-exec-* 开了两百个线程,CPU 只有 12%,而应用日志里一行请求记录都没有。 这时候”服务挂了”和”服务好好的”两个结论都不对。端口通只说明内核里有程序在 accept(),不说明请求被谁接住、更不说明它被处理了。 从 socket 到 Controller,中间要跨两次边界,每一次都会换掉执行它的线程;搞不清这两次跨界,报错就只能靠猜。 本篇接系列导读那张链路图,从第 ④ 步走到第 ⑥ 步(二~五节):请求怎么从网卡进到 Tomcat 的 Worker 线程,Filter 链又是怎么把它送进 Spring 的。源码基准同导读——Spring Framework 6.1.8 / Spring Boot 3.3.x / Tomcat 10.1.x。 本篇要回答的问题: 三次握手都成功了,请求为什么还是可能”没进来”? Acceptor、Poller、Worker 三拨线程各管什么?为什么...