Spring Boot 4.1.1 深度源码解析-第 20 章 · WebFlux 与 Reactor Netty:响应式栈与编解码
本章定位 :卷五 · Web 栈的第四站。第 19 章讲了 servlet 栈,本章讲与之平行的响应式栈。
这两章的关系不是"上下篇",而是"镜像 "。Spring Boot 对 MVC 和 WebFlux 的自动配置 结构高度对称:同样的三层嵌套、同样的
@ConditionalOnMissingBean(WebFluxConfigurationSupport.class)、 同样的@AutoConfigureOrder(HIGHEST_PRECEDENCE + 10)、同样的afterName = ValidationAutoConfiguration。但差异才是重点:
HttpMessageConverter在 reactive 栈里根本不存在 , 取而代之的是Encoder/Decoder体系;没有 DispatcherServlet ,取而代之的是HttpHandler; 没有错误 Controller ,取而代之的是ErrorWebExceptionHandler。源码坐标 :
module/spring-boot-webflux、module/spring-boot-http-codec、module/spring-boot-reactor-netty、module/spring-boot-web-server(reactive 部分)。
0. 引子:同一个注解,两条完全不同的管线
java
@RestController
public class HelloController {
@GetMapping("/hello")
public Mono<Map<String, String>> hello() {
return Mono.just(Map.of("msg", "hi"));
}
}
把这段代码放进 servlet 应用(第 19 章)和 reactive 应用,注解一个字都不用改, 但底下走的是两套完全不同的代码:
scss
servlet 栈:
Tomcat → Filter 链 → DispatcherServlet
→ DispatcherServlet.doDispatch()
→ RequestMappingHandlerMapping.getHandler(request) ← 阻塞式查找
→ HandlerAdapter.handle(request, response, handler)
→ HttpMessageConverter.write(...) ← OutputStream
→ response.getOutputStream().write(...)
reactive 栈:
Netty → ReactorHttpHandlerAdapter
→ HttpHandler.handle(request, response) ← 返回 Mono<Void>
→ DispatcherHandler(不是 DispatcherServlet!)
→ RequestMappingHandlerMapping.getHandler(exchange) ← Mono<Handler>
→ HandlerResultHandler.write(...) ← Publisher<DataBuffer>
最本质的差别 :servlet 栈的 handle 返回 ModelAndView, reactive 栈的 handle 返回 Mono<Void> ------ 它不"返回结果",它"订阅一个副作用"。
这个差别一路传导到最上层:
- servlet 用
ServletOutputStream.write(byte[]),阻塞 - reactive 用
Publisher<DataBuffer>,背压 - servlet 的转换器接口是
HttpMessageConverter<T>(一次性读写) - reactive 的编解码器是
Encoder<T>/Decoder<T>(流式、可分片)
所以"为什么 reactive 侧不用 HttpMessageConverter"不是设计偏好,而是物理约束。
1. spring-boot-webflux 模块全景
第一定律(先查 *.imports):
vbnet
module/spring-boot-webflux/src/main/resources/META-INF/spring/
├── org.springframework.boot.autoconfigure.AutoConfiguration.imports (9 条)
│ ├── HttpHandlerAutoConfiguration
│ ├── ReactiveMultipartAutoConfiguration
│ ├── WebFluxAutoConfiguration
│ ├── WebFluxObservationAutoConfiguration
│ ├── WebSessionIdResolverAutoConfiguration
│ ├── actuate.endpoint.web.WebFluxHealthEndpointExtensionAutoConfiguration
│ ├── actuate.web.exchanges.WebFluxHttpExchangesAutoConfiguration
│ ├── actuate.web.mappings.WebFluxMappingsAutoConfiguration
│ └── error.ErrorWebFluxAutoConfiguration
└── org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports
├── actuate.web.WebFluxEndpointManagementContextConfiguration
└── actuate.web.WebFluxManagementChildContextConfiguration
与 webmvc 对比:
| webmvc | webflux | |
|---|---|---|
| 普通自动配置 | 3 | 5 |
| Actuator 相关 | 3 | 4 |
测试专用通道(AutoConfigureWebServer.imports) |
有 | 无 |
合计 .imports 条目 |
6 | 9 |
webflux 没有 AutoConfigureWebServer.imports ------ 第 19 章那个只有 DispatcherServletAutoConfiguration 的测试通道是 servlet 特有的。 reactive 的 @AutoConfigureWebTestClient 走的是另一条路(未核实其注册点)。
webflux 多出来的两个:
HttpHandlerAutoConfiguration------ servlet 栈没有对应物(DispatcherServlet 由 servlet 容器驱动)WebSessionIdResolverAutoConfiguration------ session 机制的差异
模块文件数(不含测试):webmvc 48 / webflux 51(第 18 章统计过)。
2. 结构对照:平行宇宙的两个自动配置
2.1 类级注解逐条对比
java
// MVC
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class },
afterName = "org.springframework.boot.validation.autoconfigure.ValidationAutoConfiguration")
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public final class WebMvcAutoConfiguration {
java
// WebFlux
@AutoConfiguration(
after = { ReactiveMultipartAutoConfiguration.class, WebSessionIdResolverAutoConfiguration.class,
CodecsAutoConfiguration.class },
afterName = "org.springframework.boot.validation.autoconfigure.ValidationAutoConfiguration")
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnClass(WebFluxConfigurer.class)
@ConditionalOnMissingBean({ WebFluxConfigurationSupport.class })
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public final class WebFluxAutoConfiguration {
逐条对照:
| 项 | MVC | WebFlux | 说明 |
|---|---|---|---|
after |
DispatcherServletAutoConfiguration、TaskExecutionAutoConfiguration |
ReactiveMultipartAutoConfiguration、WebSessionIdResolverAutoConfiguration、CodecsAutoConfiguration |
都表达了"我依赖的东西" |
afterName |
ValidationAutoConfiguration |
ValidationAutoConfiguration |
完全相同 |
@ConditionalOnWebApplication |
SERVLET |
REACTIVE |
唯一的本质差异 |
@ConditionalOnClass |
3 个类 | 1 个类 (WebFluxConfigurer) |
WebFlux 不需要 servlet API |
@ConditionalOnMissingBean |
WebMvcConfigurationSupport |
WebFluxConfigurationSupport |
对称 :@EnableWebFlux 让整个配置失效 |
@AutoConfigureOrder |
HIGHEST_PRECEDENCE + 10 |
HIGHEST_PRECEDENCE + 10 |
完全相同 |
@ImportRuntimeHints |
WebResourcesRuntimeHints |
WebResourcesRuntimeHints |
完全相同(同一个类!) |
@ConditionalOnClass(WebFluxConfigurer.class) 只有 1 个类 ------ 这反映了 WebFlux 的一个特点:它不依赖 servlet API , spring-webflux 的依赖树里没有 jakarta.servlet。 第 17 章讲 WebApplicationType 时说过:SERVLET 的判定需要 jakarta.servlet.Servlet 且 ConfigurableWebApplicationContext------ WebFlux 应用两个都不满足,正好落入 NONE 兜底,靠 WebFluxWebApplicationTypeDeducer 判成 REACTIVE。
WebResourcesRuntimeHints 是同一个类 (都在 org.springframework.boot.web 包下)------ 这是两个栈共享基础设施 的证据。第 03 章讲的 4.x 模块化改造把这类公共部分抽到了 spring-boot-web-server / 核心包。
2.2 内部结构的对称
java
MVC WebFlux
───────────────────────────────────────── ─────────────────────────────────────────
WebMvcAutoConfiguration WebFluxAutoConfiguration
├── hiddenHttpMethodFilter @Bean ├── hiddenHttpMethodFilter @Bean
├── formContentFilter @Bean │ (无对应,reactive 无此需求)
├── WebMvcAutoConfigurationAdapter ├── WebFluxConfig
│ implements WebMvcConfigurer │ implements WebFluxConfigurer
│ @Order(0) @Import(Enable...) │ @Order(0) @Import(Enable...)
├── EnableWebMvcConfiguration ├── EnableWebFluxConfiguration
│ extends DelegatingWebMvcConfiguration │ extends DelegatingWebFluxConfiguration
├── ResourceChainCustomizerConfiguration ├── ResourceChainCustomizerConfiguration
└── ProblemDetailsErrorHandlingConfiguration └── ProblemDetailsErrorHandlingConfiguration
├── WelcomePageConfiguration ← 多一个
连 configureApiVersioning 的实现都逐行相同:
java
// MVC
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
PropertyMapper map = PropertyMapper.get();
Apiversion properties = this.mvcProperties.getApiversion();
map.from(properties::getRequired).to(configurer::setVersionRequired);
...
}
// WebFlux
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
PropertyMapper map = PropertyMapper.get();
Apiversion properties = this.webFluxProperties.getApiversion();
map.from(properties::getRequired).to(configurer::setVersionRequired);
...
}
只有 properties 的来源不同 (WebMvcProperties vs WebFluxProperties), 其余一字不差 。这说明 API 版本控制是 Framework 层的公共能力, Boot 只是各接一份配置属性。
这也提示了一个重构机会:这段代码理论上可以抽成一个共享的
ApiVersioningCustomizer。Boot 没这么做,可能是为了保持两个模块的独立性 (第 03 章讲的模块化目标之一)。这是我的推测,不是源码事实。
2.3 五个关键差异
| # | 差异点 | MVC | WebFlux |
|---|---|---|---|
| 1 | 转换器接口 | HttpMessageConverter<T> |
Encoder<T> / Decoder<T> |
| 2 | 配置回调 | configureMessageConverters(ServerBuilder) |
configureHttpMessageCodecs(ServerCodecConfigurer) |
| 3 | 入口组件 | DispatcherServlet(容器驱动) |
HttpHandler(Boot 造 bean) |
| 4 | 错误处理 | BasicErrorController(@Controller) |
ErrorWebExceptionHandler(@Order(-1) bean) |
| 5 | 异步执行器 | configureAsyncSupport |
configureBlockingExecution |
第 5 点值得细看:
java
// MVC
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
if (this.beanFactory.containsBean(APPLICATION_TASK_EXECUTOR_BEAN_NAME)) {
Object taskExecutor = this.beanFactory.getBean(APPLICATION_TASK_EXECUTOR_BEAN_NAME);
if (taskExecutor instanceof AsyncTaskExecutor asyncTaskExecutor) {
configurer.setTaskExecutor(asyncTaskExecutor);
}
}
...
}
// WebFlux
@Override
public void configureBlockingExecution(BlockingExecutionConfigurer configurer) {
if (Threading.VIRTUAL.isActive(this.environment) && this.beanFactory
.containsBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) {
Object taskExecutor = this.beanFactory.getBean(APPLICATION_TASK_EXECUTOR_BEAN_NAME);
if (taskExecutor instanceof AsyncTaskExecutor asyncTaskExecutor) {
configurer.setExecutor(asyncTaskExecutor);
}
}
}
WebFlux 多了一个 Threading.VIRTUAL.isActive(this.environment) 条件。
Threading.VIRTUAL 是 Framework 6.1/7 的枚举,isActive(Environment) 检查 spring.threads.virtual.enabled(默认是否开启我未核实)。
语义差异:
- MVC 的
configureAsyncSupport是"异步请求 (@Async、Callable 返回值)用什么线程池" - WebFlux 的
configureBlockingExecution是"阻塞式 handler(返回非 Publisher 的 controller 方法) 在什么线程上执行"
WebFlux 里 controller 方法默认应该在 event loop 线程上执行,绝不能阻塞 。 但如果用户写了一个返回 String(而非 Mono<String>)的方法, Framework 需要把它调度到别的线程池------这就是 BlockingExecutionConfigurer 的用途。
只在虚拟线程开启时才配置,因为:
- 不开虚拟线程 → Framework 用默认的
Schedulers.boundedElastic()(Reactor 的线程池) - 开虚拟线程 → 用 Boot 的
applicationTaskExecutor(虚拟线程执行器), 这样阻塞调用不会耗尽平台线程
这是一个**"虚拟线程与响应式共存"**的设计,第 30 章会展开 spring.threads.virtual.*。
2.4 为什么 WebFlux 没有 DispatcherServletAutoConfiguration 的对应物?
servlet 栈需要单独的 DispatcherServletAutoConfiguration 是因为: DispatcherServlet 必须注册进 servlet 容器 (通过 ServletRegistrationBean), 且这个注册涉及 URL mapping、load-on-startup、multipart config 等容器级概念。
reactive 栈没有容器级的 servlet 注册 ------HttpHandler 只是一个 bean, 由 HttpHandlerAutoConfiguration 造出来,然后被 ReactiveWebServerApplicationContext 直接交给 ReactiveWebServerFactory.getWebServer(httpHandler)。
所以 WebFlux 侧的对应物是 HttpHandlerAutoConfiguration,但它简单得多 (只有一个 @Bean 方法,没有那些复杂的条件类)。
这是一个很有说服力的对比 :servlet 栈的复杂度很大一部分来自 "servlet 容器的 API 很笨重"(第 18 章反复看到的"阻止端口提前监听"也是这个原因)。
3. WebFluxAutoConfiguration 详解
3.1 hiddenHttpMethodFilter:只有一个
java
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnBooleanProperty("spring.webflux.hiddenmethod.filter.enabled")
OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
注意属性前缀是 spring.webflux.hiddenmethod.filter.enabled , MVC 那边是 spring.mvc.hiddenmethod.filter.enabled。 两个前缀不对称 (spring.mvc.* vs spring.webflux.*)------ 这是历史遗留:MVC 的配置一直在 spring.mvc 下, 而 WebFlux 出现时已经晚了,用了 spring.webflux。
OrderedHiddenHttpMethodFilter 在 webflux 模块里有自己的实现 (org.springframework.boot.webflux.filter.OrderedHiddenHttpMethodFilter), 而不是复用 webmvc 的那个------因为 reactive 的 filter 是 WebFilter, 不是 servlet 的 Filter。类型不同,只能各写一份。
3.2 WelcomePageConfiguration:一个返回 @Nullable 的 @Bean
java
@Configuration(proxyBeanMethods = false)
static class WelcomePageConfiguration {
@Bean
@Nullable RouterFunctionMapping welcomePageRouterFunctionMapping(ApplicationContext applicationContext,
WebFluxProperties webFluxProperties, WebProperties webProperties) {
String[] staticLocations = webProperties.getResources().getStaticLocations();
WelcomePageRouterFunctionFactory factory = new WelcomePageRouterFunctionFactory(
new TemplateAvailabilityProviders(applicationContext), applicationContext, staticLocations,
webFluxProperties.getStaticPathPattern());
RouterFunction<ServerResponse> routerFunction = factory.createRouterFunction();
if (routerFunction != null) {
RouterFunctionMapping routerFunctionMapping = new RouterFunctionMapping(routerFunction);
routerFunctionMapping.setOrder(1);
return routerFunctionMapping;
}
return null; // ★ 返回 null
}
}
一个 @Bean 方法可以返回 null ------这在 Boot 源码里很罕见。 返回 null 意味着不注册这个 bean。
为什么 MVC 那边不这么做?看第 19 章:MVC 的 welcomePageHandlerMapping 总是创建 , 即使 WelcomePage.UNRESOLVED,也是创建一个"没有 root handler"的空 mapping。 因为 WelcomePageHandlerMapping 的构造函数里, if (welcomePage != WelcomePage.UNRESOLVED) 才 setRootHandler------ 创建一个空的 mapping 是无害的。
而 reactive 侧如果创建了空的 RouterFunctionMapping,可能会干扰路由匹配 (推测 :空 router function 可能匹配所有请求或产生奇怪行为)。 所以 Boot 选择不注册。
⚠️ 未核实 :返回 null 的
@Bean在 Spring 里的确切行为。 我知道@Bean返回 null 会导致"不注册"(Framework 的ConfigurationClassBeanDefinitionReader里有if (beanInstance != null)之类的判断),但没有逐行确认。 另外@Nullable在这里是给 NullAway / IDE 看的(Boot 4 大量使用 JSpecify 的@Nullable), 不是给 Spring 看的。
routerFunctionMapping.setOrder(1)------对比 MVC 的 WelcomePageHandlerMapping.setOrder(2)。 数值不同(1 vs 2),因为两边的 handler mapping 体系里其他组件的 order 不同, 不能简单类比。
3.3 WebFluxConfig:注入了 Environment
java
static class WebFluxConfig implements WebFluxConfigurer {
private static final Log logger = LogFactory.getLog(WebFluxConfig.class);
// ★ 注意:这里用的是 WebFluxConfig.class,不是 WebFluxConfigurer.class
private final Environment environment;
private final Resources resourceProperties;
private final WebFluxProperties webFluxProperties;
private final ListableBeanFactory beanFactory;
private final ObjectProvider<HandlerMethodArgumentResolver> argumentResolvers;
private final ObjectProvider<CodecCustomizer> codecCustomizers;
private final ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizers;
private final ObjectProvider<ViewResolver> viewResolvers;
private final ObjectProvider<ApiVersionResolver> apiVersionResolvers;
private final ObjectProvider<ApiVersionParser<?>> apiVersionParser;
private final ObjectProvider<ApiVersionDeprecationHandler> apiVersionDeprecationHandler;
两处与 MVC 不同:
- logger 用的是本类
WebFluxConfig.class,而 MVC 用的是WebMvcConfigurer.class。 第 19 章我标注了 MVC 那个"可能是 copy-paste 的瑕疵"------ WebFlux 这里用对了,这反而佐证了 MVC 那边确实是不小心。 - 多注入了一个
Environment------就为了 2.3 节的Threading.VIRTUAL.isActive(environment)。
3.4 configureHttpMessageCodecs:编解码器的装配点
java
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
this.codecCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configurer));
}
与 MVC 的 configureMessageConverters(ServerBuilder builder) 结构完全一样------ 都是"把定制器链转发给 Framework 的 builder"。
区别只是 builder 的类型:
- MVC:
ServerBuilder(org.springframework.http.converter) - WebFlux:
ServerCodecConfigurer(org.springframework.http.codec)
这是"平行宇宙"最直接的证据:Boot 在这两个地方做的事情一模一样, 只是类型不同。真正复杂的逻辑在 Framework 侧。
configureArgumentResolvers 是 WebFlux 独有的(MVC 没有这个回调):
java
@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
this.argumentResolvers.orderedStream().forEach(configurer::addCustomResolver);
}
用户可以注册 HandlerMethodArgumentResolver bean 来扩展参数解析。
3.5 addResourceHandlers:两个独立的 if
java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
List<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizers =
this.resourceHandlerRegistrationCustomizers.orderedStream().toList();
String webjarsPathPattern = this.webFluxProperties.getWebjarsPathPattern();
if (!registry.hasMappingForPattern(webjarsPathPattern)) {
ResourceHandlerRegistration registration = registry.addResourceHandler(webjarsPathPattern)
.addResourceLocations("classpath:/META-INF/resources/webjars/");
configureResourceCaching(registration);
resourceHandlerRegistrationCustomizers.forEach((customizer) -> customizer.customize(registration));
}
String staticPathPattern = this.webFluxProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
ResourceHandlerRegistration registration = registry.addResourceHandler(staticPathPattern)
.addResourceLocations(this.resourceProperties.getStaticLocations());
configureResourceCaching(registration);
resourceHandlerRegistrationCustomizers.forEach((customizer) -> customizer.customize(registration));
}
}
结构差异(对比 MVC):
| MVC | WebFlux | |
|---|---|---|
| 重复检查 | 抽成 addResourceHandler(registry, pattern, ...) 私有方法,方法内 if (registry.hasMappingForPattern(pattern)) return; |
两处独立的 if |
| 定制器获取 | 构造期 getIfAvailable()(单个) |
使用期 orderedStream().toList()(多个) |
| webapp 根 | 加 ServletContextResource(servletContext, "/") |
无(没有 servlet context) |
MVC 抽了私有方法,WebFlux 写了两遍 ------这又是一处不对称。 WebFlux 版把定制器链放在方法内 获取且支持多个 , 比 MVC 版的"构造期取单个"更合理。推测 WebFlux 版是后来写的、写得更好, 但 MVC 版没跟着改(改动会影响公开行为,Boot 很保守)。
3.6 configureResourceCaching:cachePeriod 到 maxAge 的回退
java
private void configureResourceCaching(ResourceHandlerRegistration registration) {
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
WebProperties.Resources.Cache.Cachecontrol cacheControl = this.resourceProperties.getCache().getCachecontrol();
if (cachePeriod != null && cacheControl.getMaxAge() == null) {
cacheControl.setMaxAge(cachePeriod);
}
CacheControl httpCacheControl = cacheControl.toHttpCacheControl();
if (httpCacheControl != null) {
registration.setCacheControl(httpCacheControl);
}
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
}
关键逻辑 :cachePeriod != null && cacheControl.getMaxAge() == null ------ cachePeriod 是旧属性,cachecontrol.max-age 是新属性, 旧的只在新的没配时生效。这是标准的"废弃属性兼容"模式。
对比 MVC 版:
java
Integer cachePeriod = getSeconds(this.resourceProperties.getCache().getPeriod());
if (cachePeriod != null) {
registration.setCachePeriod(cachePeriod); // ← 直接 setCachePeriod
}
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (cacheControl != null) {
registration.setCacheControl(cacheControl);
}
MVC 版是先 setCachePeriod 再 setCacheControl(后者覆盖前者) , 而 WebFlux 版是在数据层做回退 (把 cachePeriod 写进 cacheControl.maxAge)。 两种实现都能达到"新属性优先"的效果,但 WebFlux 版更清晰: 只调一次 setter,不会有两个 setter 互相覆盖的隐含顺序。
registration.setCachePeriod(...) 是 deprecated API (Framework 7 待移除), 所以 WebFlux 版干脆不用它------又一次看到 forRemoval 驱动的实现差异。
3.7 EnableWebFluxConfiguration:与 MVC 版的高度对称
java
/**
* Configuration equivalent to {@code @EnableWebFlux}.
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ WebProperties.class, ServerProperties.class })
@ImportRuntimeHints(WebFluxValidatorRuntimeHints.class)
static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration
implements EmbeddedValueResolverAware {
连 Javadoc 都一样 ("Configuration equivalent to @EnableWebFlux", MVC 是 "equivalent to @EnableWebMvc")。
覆写的方法:
| 方法 | MVC 版 | WebFlux 版 |
|---|---|---|
createRequestMappingHandlerAdapter() |
✅ | ✅ |
createRequestMappingHandlerMapping() |
✅ | ✅ |
createExceptionHandlerExceptionResolver() |
✅ | ❌ 没有 |
webFluxConversionService() / mvcConversionService() |
✅ | ✅ |
webFluxValidator() / mvcValidator() |
✅ | ✅(逻辑逐行相同) |
localeContextResolver() / localeResolver() |
✅ | ✅ |
webSessionManager() |
❌ 无 | ✅ 有 |
webFluxApiVersionStrategy() / mvcApiVersionStrategy() |
✅ | ✅ |
两个独有/缺失:
- WebFlux 没有
createExceptionHandlerExceptionResolver()------ 因为 reactive 栈的异常处理走WebExceptionHandler(不是HandlerExceptionResolver), 而且ExceptionHandlerExceptionResolver是 servlet 专用类型。 - WebFlux 有
webSessionManager()------ servlet 的 session 由容器 管理, reactive 的 session 必须由应用 管理(WebSessionManagerbean)。
WebFluxRegistrations 也比 WebMvcRegistrations 少一个方法 : 只有 getRequestMappingHandlerMapping / getRequestMappingHandlerAdapter, 没有 getExceptionHandlerExceptionResolver------一一对应。
3.8 webSessionManager:MaxIdleTimeInMemoryWebSessionStore
java
@Bean
@ConditionalOnMissingBean(name = WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
WebSessionManager webSessionManager(ObjectProvider<WebSessionIdResolver> webSessionIdResolver) {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
Duration timeout = this.serverProperties.getReactive().getSession().getTimeout();
int maxSessions = this.serverProperties.getReactive().getSession().getMaxSessions();
MaxIdleTimeInMemoryWebSessionStore sessionStore = new MaxIdleTimeInMemoryWebSessionStore(timeout);
sessionStore.setMaxSessions(maxSessions);
webSessionManager.setSessionStore(sessionStore);
webSessionIdResolver.ifAvailable(webSessionManager::setSessionIdResolver);
return webSessionManager;
}
又一次"按名字协作" (第 19 章的第十九定律): @ConditionalOnMissingBean(name = WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)------ WebHttpHandlerBuilder 按固定名字找 session manager。
MaxIdleTimeInMemoryWebSessionStore 是一个内部静态类:
java
static final class MaxIdleTimeInMemoryWebSessionStore extends InMemoryWebSessionStore {
private final Duration timeout;
private MaxIdleTimeInMemoryWebSessionStore(Duration timeout) {
this.timeout = timeout;
}
@Override
public Mono<WebSession> createWebSession() {
return super.createWebSession().doOnSuccess(this::setMaxIdleTime);
}
private void setMaxIdleTime(WebSession session) {
session.setMaxIdleTime(this.timeout);
}
}
为什么要这个子类? 因为 InMemoryWebSessionStore 的 setMaxIdleTime 是全局的 (store 级别),而 Boot 想让它来自 server.reactive.session.timeout。 其实直接用 store.setMaxIdleTime(timeout) 也行......
⚠️ 推测 :真正的差别是------用子类可以在每个 session 创建时 设置, 而
InMemoryWebSessionStore是否有setMaxIdleTime(Duration)方法我未确认。 如果它有,这个子类就是多余的;如果它没有(只有 per-session 的 API), 子类就是必需的。自测方法 :读InMemoryWebSessionStore的 API。另一个可能的理由:
doOnSuccess的写法保证只在成功时设置, 而构造期 setMaxIdleTime 会在 store 还没就绪时被调用。
注意 server.reactive.session.*(不是 spring.*)------ session 配置属于服务器 配置,与第 18 章的 ServerProperties 同源。
3.9 localeContextResolver:又一处按名字
java
@Bean
@Override
@ConditionalOnMissingBean(name = WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME)
public LocaleContextResolver localeContextResolver() {
Locale locale = this.webProperties.getLocale();
if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
Assert.state(locale != null, "'locale' must not be null");
return new FixedLocaleContextResolver(locale);
}
AcceptHeaderLocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();
localeContextResolver.setDefaultLocale(locale);
return localeContextResolver;
}
与 MVC 的 localeResolver() 逻辑相同 ,但类型是 LocaleContextResolver(reactive 版)。 WebProperties.LocaleResolver.FIXED 枚举是两个栈共享的。
Assert.state(locale != null, "'locale' must not be null") ------ FIXED 模式下 spring.web.locale 必填,否则启动失败。 这是一个配置完整性检查,比让它静默用系统默认更好。
4. HttpHandlerAutoConfiguration:WebFlux 的"DispatcherServlet"
4.1 只有一个 @Bean
java
@AutoConfiguration(after = { WebFluxAutoConfiguration.class })
@ConditionalOnClass({ DispatcherHandler.class, HttpHandler.class })
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnMissingBean(HttpHandler.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public final class HttpHandlerAutoConfiguration {
@Configuration(proxyBeanMethods = false)
static class AnnotationConfig {
private final ApplicationContext applicationContext;
AnnotationConfig(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Bean
HttpHandler httpHandler(ObjectProvider<WebFluxProperties> propsProvider,
ObjectProvider<WebHttpHandlerBuilderCustomizer> handlerBuilderCustomizers) {
WebFluxProperties properties = propsProvider.getIfAvailable();
WebHttpHandlerBuilder handlerBuilder = WebHttpHandlerBuilder.applicationContext(this.applicationContext);
if (properties != null) {
handlerBuilder.defaultHtmlEscape(properties.getDefaultHtmlEscape());
}
handlerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(handlerBuilder));
HttpHandler httpHandler = handlerBuilder.build();
if (properties != null && StringUtils.hasText(properties.getBasePath())) {
Map<String, HttpHandler> handlersMap = Collections.singletonMap(properties.getBasePath(), httpHandler);
return new ContextPathCompositeHandler(handlersMap);
}
return httpHandler;
}
}
}
四个要点:
-
@ConditionalOnMissingBean(HttpHandler.class)在类级 ------ 用户可以自己提供一个HttpHandlerbean 完全接管。这是 WebFlux 的"最大粒度开关" (对应 MVC 的@ConditionalOnMissingBean(WebMvcConfigurationSupport.class))。 -
WebHttpHandlerBuilder.applicationContext(applicationContext)------ 这是关键:Framework 从容器里 收集WebExceptionHandler、WebFilter、WebSessionManager、LocaleContextResolver等,组装成一个HttpHandler。 所以这些 bean 的名字必须正确(3.8、3.9 节的按名字协作就是这个原因)。 -
propsProvider.getIfAvailable()而不是直接注入 ------ 因为WebFluxProperties可能没被@EnableConfigurationProperties注册 (比如用户排除了某些配置)。用了ObjectProvider+ 两处 null 检查。 -
basePath用ContextPathCompositeHandler包装 ------ 这是 reactive 版的server.servlet.context-path:javaMap<String, HttpHandler> handlersMap = Collections.singletonMap(properties.getBasePath(), httpHandler); return new ContextPathCompositeHandler(handlersMap);把"路径前缀 → handler"的映射交给一个路由型 handler。
4.2 basePath 的清洗
WebFluxProperties 里有一个 cleanBasePath 方法(第 74 行):
java
private @Nullable String cleanBasePath(@Nullable String basePath) { ... }
未读实现 ,但从名字推断是去掉首尾斜杠、处理空串。 与第 19 章 DispatcherServletPath.getServletUrlMapping() 的四条规则呼应------ 两个栈都有"把用户给的路径规范化"的逻辑。
⚠️ 未核实 :
cleanBasePath的具体实现与调用点(应该在getBasePath()里)。
4.3 defaultHtmlEscape
java
handlerBuilder.defaultHtmlEscape(properties.getDefaultHtmlEscape());
spring.webflux.default-html-escape 控制视图渲染时是否 HTML 转义, 默认 null(即不强制,交给视图引擎)。这是一个安全相关的默认值------ Boot 没有默认开启,因为会让已经自己转义的应用出现双重转义。
4.4 WebHttpHandlerBuilderCustomizer:唯一的用户扩展点
java
@FunctionalInterface
public interface WebHttpHandlerBuilderCustomizer {
void customize(WebHttpHandlerBuilder webHttpHandlerBuilder);
}
这是 WebFlux 栈最深层的 用户扩展点------能改 WebHttpHandlerBuilder 的一切 (加 filter、换 session manager、换 exception handler)。
对比 servlet 栈:没有完全对应的东西(DispatcherServlet 的定制要通过 WebMvcConfigurer 或 WebMvcRegistrations)。 reactive 栈的扩展点反而更集中------因为它的入口就是一个 bean。
5. CodecsAutoConfiguration:为什么不用 HttpMessageConverter
5.1 概念差异
| servlet 栈 | reactive 栈 | |
|---|---|---|
| 接口 | HttpMessageConverter<T> |
Encoder<T> / Decoder<T> |
| 读方法 | read(Class, HttpInputMessage) → T |
decode(Publisher<DataBuffer>, ...) → Flux<T> / Mono<T> |
| 写方法 | write(T, MediaType, HttpOutputMessage) → void |
encode(Publisher<T>, ...) → Flux<DataBuffer> |
| 返回类型 | 阻塞值 / void | Publisher |
| 背压 | 无 | 有(通过 Publisher) |
核心区别 :HttpMessageConverter.write 的签名是 void + HttpOutputMessage, 它必须自己把字节写完 ;Encoder.encode 返回 Flux<DataBuffer>, 它只是"声明"数据流,由订阅者决定何时消费。
所以 HttpMessageConverter 在 reactive 栈里根本没法用------ 第 0 节说的"物理约束"就是这个。
5.2 CodecCustomizer:函数式接口 + lambda 实现
java
@FunctionalInterface
public interface CodecCustomizer {
void customize(CodecConfigurer configurer);
}
Boot 自己用它时直接返 lambda:
java
@Bean
@Order(0)
@ConditionalOnBean(JsonMapper.class)
CodecCustomizer jacksonCodecCustomizer(JsonMapper jsonMapper) {
return (configurer) -> {
CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs();
defaults.jacksonJsonDecoder(new JacksonJsonDecoder(jsonMapper));
defaults.jacksonJsonEncoder(new JacksonJsonEncoder(jsonMapper));
};
}
对比第 19 章 JacksonJsonHttpMessageConvertersCustomizer(一个具名类) ------ reactive 侧用 lambda,servlet 侧用具名类。 原因:CodecCustomizer 是 @FunctionalInterface 且实现很短; 而 servlet 侧的 customizer 要同时实现两个接口 (ClientHttpMessageConvertersCustomizer + ServerHttpMessageConvertersCustomizer), lambda 做不到。这是接口设计导致的差异,不是随意的。
5.3 三个 JSON 配置类
kotlin
CodecsAutoConfiguration
├── JacksonJsonCodecConfiguration @ConditionalOnClass(JsonMapper)
│ └── @ConditionalOnProperty(preferred-json-mapper=jackson, matchIfMissing=true)
├── Jackson2JsonCodecConfiguration @ConditionalOnClass(ObjectMapper)
│ ├── @Conditional(NoJacksonOrJackson2Preferred) ← ★ 关键
│ └── @Deprecated(since="4.0.0", forRemoval=true)
├── KotlinxSerializationJsonCodecConfiguration @ConditionalOnClass(Json)
└── DefaultCodecsConfiguration
注意 JacksonJsonCodecConfiguration 用的是 @ConditionalOnBean(JsonMapper.class) 放在 @Bean 方法上,而不是像 servlet 侧那样放在内部类上:
java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JsonMapper.class)
@ConditionalOnProperty(name = "spring.http.codecs.preferred-json-mapper", havingValue = "jackson",
matchIfMissing = true)
static class JacksonJsonCodecConfiguration {
@Bean
@Order(0)
@ConditionalOnBean(JsonMapper.class) // ← 在方法级
CodecCustomizer jacksonCodecCustomizer(JsonMapper jsonMapper) { ... }
}
可以 :@ConditionalOnBean 在方法级也能生效(检查时该 bean 定义已注册, 因为 CodecsAutoConfiguration 排在 JacksonAutoConfiguration 之后)。 servlet 侧放在类级只是风格不同。
5.4 NoJacksonOrJackson2Preferred:AnyNestedCondition 的 OR 语义
java
static class NoJacksonOrJackson2Preferred extends AnyNestedCondition {
NoJacksonOrJackson2Preferred() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper")
static class NoJackson {
}
@ConditionalOnProperty(name = "spring.http.codecs.preferred-json-mapper", havingValue = "jackson2")
static class Jackson2Preferred {
}
}
AnyNestedCondition = 任一内部类条件成立即匹配 (第 15 章讲过 NoneNestedConditions 是取反, AnyNestedCondition 是或,AllNestedConditions 是与)。
所以 Jackson2 的 codec 配置生效条件是:
ini
(classpath 上没有 Jackson 3) 或 (preferred-json-mapper = jackson2)
这个逻辑很关键:
- 只有 Jackson 2 在 classpath → 第一项成立 → Jackson2 codec 生效 ✅
- 两个都在,默认(不配 preferred)→ 第一项不成立,第二项不成立 → Jackson2 codec 不生效 ✅
- 两个都在,显式
preferred-json-mapper=jackson2→ 第二项成立 → 生效 ✅
@ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper") 用字符串 ------ 因为 CodecsAutoConfiguration 不能编译期引用 Jackson 3 的类 (Jackson 3 是可选依赖)。这又是第 19 章第二十定律的应用。
注意字符串是 "tools.jackson.databind.json.JsonMapper"(带 .json.) , 而不是第 19 章常见 "tools.jackson.databind.JsonMapper"。
⚠️ 未核实 :Jackson 3 的
JsonMapper全限定名到底是哪个。 从JsonMapper.builder()的 import 推断应该是tools.jackson.databind.json.JsonMapper(Jackson 3 把 json 专用 mapper 放到了json子包)。这与 7.1 节的推断一致。
5.5 KotlinxSerializationJsonCodecConfiguration 的 hasAnyJsonSupport
java
@Bean
@Order(0)
@ConditionalOnBean(Json.class)
CodecCustomizer kotlinxJsonCodecCustomizer(Json json, ResourceLoader resourceLoader) {
ClassLoader classLoader = resourceLoader.getClassLoader();
boolean hasAnyJsonSupport = ClassUtils.isPresent("tools.jackson.databind.json.JsonMapper", classLoader)
|| ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
|| ClassUtils.isPresent("com.google.gson.Gson", classLoader);
return (configurer) -> {
CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs();
defaults.kotlinSerializationJsonEncoder(hasAnyJsonSupport ? new KotlinSerializationJsonEncoder(json)
: new KotlinSerializationJsonEncoder(json, (type) -> true));
defaults.kotlinSerializationJsonDecoder(hasAnyJsonSupport ? new KotlinSerializationJsonDecoder(json)
: new KotlinSerializationJsonDecoder(json, (type) -> true));
};
}
这段代码的意图 :如果 classpath 上已经有别的 JSON 库 (Jackson 3 / Jackson 2 / Gson), 那么 kotlinx.serialization 的 codec 应该只处理它能处理的类型 (默认行为); 如果没有任何其他 JSON 库 ,那么它要接管所有类型 ((type) -> true)。
KotlinSerializationJsonEncoder(json, (type) -> true) 的第二个参数是一个 **"我能不能处理这个类型"**的谓词------传 (type) -> true 表示"什么都行"。
这是一个典型的"兜底"逻辑:kotlinx 序列化在没有竞争对手时包揽一切, 有竞争对手时退居二线。
注意 ClassUtils.isPresent 用的三个字符串类名------Gson 也是可选依赖, 同样只能用字符串检查。
5.6 DefaultCodecsConfiguration:两个全局设置
java
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HttpCodecsProperties.class)
static class DefaultCodecsConfiguration {
@Bean
DefaultCodecCustomizer defaultCodecCustomizer(HttpCodecsProperties httpCodecProperties) {
return new DefaultCodecCustomizer(httpCodecProperties.isLogRequestDetails(),
httpCodecProperties.getMaxInMemorySize());
}
static final class DefaultCodecCustomizer implements CodecCustomizer, Ordered {
private final boolean logRequestDetails;
private final @Nullable DataSize maxInMemorySize;
@Override
public void customize(CodecConfigurer configurer) {
PropertyMapper map = PropertyMapper.get();
CodecConfigurer.DefaultCodecs defaultCodecs = configurer.defaultCodecs();
defaultCodecs.enableLoggingRequestDetails(this.logRequestDetails);
map.from(this.maxInMemorySize).asInt(DataSize::toBytes).to(defaultCodecs::maxInMemorySize);
}
@Override
public int getOrder() {
return 0;
}
}
}
spring.http.codecs.* 只有两个属性:
| 属性 | 默认 | 作用 |
|---|---|---|
log-request-details |
false | DEBUG 打印表单数据、TRACE 打印请求头(可能泄露敏感信息) |
max-in-memory-size |
null(各 codec 默认,通常 256K) | 聚合输入流时可缓冲的最大字节数 |
maxInMemorySize 是一个 DoS 防护 ------ 对应 servlet 侧 Jackson 的 StreamReadConstraints(第 19 章 7.4 节)。 两个栈都有"限制单次请求能缓冲多少"的配置,但放在不同前缀下:
- servlet:
spring.jackson.factory.constraints.*(Jackson 层) - reactive:
spring.http.codecs.max-in-memory-size(codec 层)
map.from(...).asInt(DataSize::toBytes) ------ PropertyMapper 的 asInt 转换 (DataSize.toBytes() 返回 long,需要转 int)。若超过 Integer.MAX_VALUE 会怎样? 未核实 (asInt 大概会抛异常或截断)。
DefaultCodecCustomizer 没有 @Order 注解但有 getOrder() == 0 ------ 实现 Ordered 接口而非用注解。与 JacksonJsonCodecCustomizer 的 @Order(0) 混用。
5.7 为什么 servlet 的配置叫 spring.http.converters,reactive 叫 spring.http.codecs?
因为它们的类型不同 :一个是"转换器"(converter),一个是"编解码器"(codec)。 Boot 选择用属性前缀区分 ,而不是共用一个 spring.http.json.*。
这带来一个实践上的坑 : spring.http.converters.preferred-json-mapper 与 spring.http.codecs.preferred-json-mapper 是两个不同的属性,一个应用里如果同时有 servlet 和 reactive(罕见但可能), 要配两次。
5.8 CodecsAutoConfiguration 同时服务 server 与 client
回头看它的类级条件:
java
@AutoConfiguration(afterName = { "...JacksonAutoConfiguration", "...Jackson2AutoConfiguration" })
@ConditionalOnClass({ CodecConfigurer.class, WebClient.class })
public final class CodecsAutoConfiguration {
WebClient.class 出现在 @ConditionalOnClass 里 ------ 这说明 CodecsAutoConfiguration 不只是给 WebFlux 服务器用的, 它还要给 WebClient(HTTP 客户端)用。
CodecCustomizer 的 Javadoc 明确写了这一点:
java
/**
* Callback interface that can be used to customize codecs configuration for an HTTP
* client and/or server with a {@link CodecConfigurer}.
*/
CodecConfigurer 有两个子类:
| 子类 | 用途 | 由谁创建 |
|---|---|---|
ServerCodecConfigurer |
WebFlux 服务端(读写请求/响应) | Framework 的 WebFluxConfigurationSupport |
ClientCodecConfigurer |
WebClient 客户端 |
WebClient.builder() 内部 |
Boot 的 CodecCustomizer 接口用的是父类型 CodecConfigurer , 所以同一个定制器既能定制服务端也能定制客户端。
这带来一个实践上非常重要的后果:
yaml
spring:
http:
codecs:
max-in-memory-size: 10MB
这一行同时影响:
- WebFlux 服务器解码请求体时能缓冲多少
WebClient接收响应时能缓冲多少
第 2 点是很多人的痛点:WebClient 的默认限制是 256KB, 超过会抛 DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144。 解决办法就是配 spring.http.codecs.max-in-memory-size ------ 哪怕你的应用根本不是 WebFlux 应用 (纯 servlet 应用里用 WebClient 调外部服务,照样生效)。
⚠️ 这一点极易被忽略 :很多人以为要自己 WebClient.builder().codecs(...) 手动配, 其实 Boot 的自动配置已经做了------前提是 CodecsAutoConfiguration 生效, 而它的条件只是"CodecConfigurer 和 WebClient 在 classpath", 与 WebApplicationType 无关 (没有 @ConditionalOnWebApplication 注解)。
也就是说:在 servlet 应用里,
CodecsAutoConfiguration也会生效 , 只是它的ServerCodecConfigurer部分没人用(WebFluxAutoConfiguration不生效), 而ClientCodecConfigurer部分被WebClient用上了。
为什么 configureHttpMessageCodecs 的参数是 ServerCodecConfigurer(子类型)?
java
// WebFluxConfig
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
this.codecCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configurer));
}
因为 WebFluxConfigurer 的这个回调只管服务端 。客户端侧的装配点在别处 (WebClientCustomizer 或 WebClient.Builder 的自动配置)。
⚠️ 未核实 :
WebClient.Builder的自动配置在哪里、它是否也应用了CodecCustomizer链。 从第 19 章HttpMessageConvertersAutoConfiguration同时有ClientHttpMessageConvertersCustomizer和ServerHttpMessageConvertersCustomizer来看,客户端侧应该也有类似的机制。自测方法 :在 servlet 应用里配spring.http.codecs.max-in-memory-size=10MB,然后用 WebClient 下载一个 1MB 的响应看是否还报错。
5.9 三个 CodecCustomizer 的 order 全是 0 意味着什么
本章出现了三个(四个,如果算上用户自定义的)CodecCustomizer:
| 定制器 | @Bean |
order | 作用 |
|---|---|---|---|
DefaultCodecCustomizer |
无注解,实现 Ordered |
getOrder() == 0 |
logRequestDetails + maxInMemorySize |
jacksonCodecCustomizer |
@Order(0) |
0 | 换掉 JSON 的 encoder/decoder |
jackson2CodecCustomizer |
@Order(0) |
0 | 同上(Jackson 2) |
kotlinxJsonCodecCustomizer |
@Order(0) |
0 | 同上(kotlinx) |
defaultPartHttpMessageReaderCustomizer |
@Order(0) |
0 | multipart reader 的限制 |
全是 0。 相同 order 时 orderedStream() 按什么排?
按 bean 定义的注册顺序 (AnnotationAwareOrderComparator 是稳定的, OrderComparator 对相等的元素保持原序)。而 bean 定义顺序又取决于:
- 自动配置类的排序(
AutoConfigurationSorter) - 同一配置类内
@Bean方法的声明顺序
这里有一个潜在问题 :DefaultCodecCustomizer 设置 maxInMemorySize, 而 jacksonCodecCustomizer 替换 JSON 的 decoder。 如果顺序反了会怎样?
java
defaultCodecs.jacksonJsonDecoder(new JacksonJsonDecoder(jsonMapper)); // 替换 decoder
...
defaultCodecs.maxInMemorySize(size); // 设置缓冲上限
maxInMemorySize 是 DefaultCodecs 上的一个"全局"设置 ------ 它会被应用到之后注册的 decoder 上(Framework 的实现细节)。 如果先设 maxInMemorySize 再换 decoder,新 decoder 可能拿不到这个值。
⚠️ 未核实 :
CodecConfigurer.DefaultCodecs.maxInMemorySize(int)的语义 是"记录一个值并在后续jacksonJsonDecoder(...)时应用"还是"遍历已有 decoder 设置"。 这两种实现下,顺序的敏感性是相反的:
- 若是"记录值,后续应用" → 必须先
maxInMemorySize后 换 decoder- 若是"遍历已有 decoder" → 必须先 换 decoder 后
maxInMemorySizeBoot 能正常工作说明实际顺序是对的 。 自测方法 :配置
spring.http.codecs.max-in-memory-size=10MB, 然后 POST 一个 1MB 的 JSON,看是否报DataBufferLimitException。
这给我们一个方法论提醒 : 当多个定制器作用于同一个有状态对象 时,即使 order 相同也可能有隐含的顺序依赖。 Boot 用「全部 @Order(0)」表达"这些定制器之间没有优先级差异"------ 但"没有优先级"不等于"顺序无关"。
5.10 一个实践配方:给 WebFlux 换掉 JSON 库
把本章与第 19 章的知识组合起来,假设你想让 WebFlux 用 Gson 而不是 Jackson:
yaml
spring:
http:
codecs:
preferred-json-mapper: gson # ← 如果 Boot 支持的话
⚠️ 未核实 :
CodecsAutoConfiguration里没有 Gson 的分支! 我读到的只有 Jackson(3)、Jackson2(2)、kotlinx 三个。 对比第 19 章 servlet 侧的HttpMessageConvertersAutoConfiguration有 5 个@Import(含GsonHttpMessageConvertersConfiguration)。所以 reactive 栈的
preferred-json-mapper可能只支持jackson/jackson2/ kotlinx (kotlinx 那条还没有preferred检查)。 这是一个真实的能力差异 ,值得单独记一笔: servlet 栈支持 5 种 JSON 库,reactive 栈只支持 3 种。
替代方案 :自己注册一个 CodecCustomizer bean:
java
@Bean
CodecCustomizer gsonCodecCustomizer(Gson gson) {
return (configurer) -> {
configurer.defaultCodecs().jacksonJsonEncoder(...) // 没有 gson 的 API
};
}
实际上 ServerCodecConfigurer.DefaultCodecs 只提供了 jacksonJsonEncoder/Decoder、kotlinSerializationJsonEncoder/Decoder 等固定槽位, 没有"任意 codec"的槽位 。要加自定义 codec 得用 customCodecs():
java
@Bean
CodecCustomizer myCodecCustomizer() {
return (configurer) -> configurer.customCodecs().writer(new MyEncoder());
}
⚠️ 未核实 :
customCodecs()注册的 writer 与默认 writer 的优先级关系。 通常是"自定义优先",但未确认。
6. ReactiveMultipartAutoConfiguration:configureDefaultCodec 的类型分派
java
@AutoConfiguration
@ConditionalOnClass({ DefaultPartHttpMessageReader.class, WebFluxConfigurer.class })
@ConditionalOnWebApplication(type = Type.REACTIVE)
@EnableConfigurationProperties(ReactiveMultipartProperties.class)
public final class ReactiveMultipartAutoConfiguration {
@Bean
@Order(0)
CodecCustomizer defaultPartHttpMessageReaderCustomizer(ReactiveMultipartProperties multipartProperties) {
return (configurer) -> configurer.defaultCodecs().configureDefaultCodec((codec) -> {
if (codec instanceof DefaultPartHttpMessageReader defaultPartHttpMessageReader) {
PropertyMapper map = PropertyMapper.get();
map.from(multipartProperties::getMaxInMemorySize)
.asInt(DataSize::toBytes)
.to(defaultPartHttpMessageReader::setMaxInMemorySize);
map.from(multipartProperties::getMaxHeadersSize)
.asInt(DataSize::toBytes)
.to(defaultPartHttpMessageReader::setMaxHeadersSize);
map.from(multipartProperties::getMaxDiskUsagePerPart)
.as(DataSize::toBytes)
.to(defaultPartHttpMessageReader::setMaxDiskUsagePerPart);
map.from(multipartProperties::getMaxParts).to(defaultPartHttpMessageReader::setMaxParts);
map.from(multipartProperties::getFileStorageDirectory)
.as(Paths::get)
.to((dir) -> configureFileStorageDirectory(defaultPartHttpMessageReader, dir));
map.from(multipartProperties::getHeadersCharset).to(defaultPartHttpMessageReader::setHeadersCharset);
}
else if (codec instanceof PartEventHttpMessageReader partEventHttpMessageReader) {
... // 类似,但没有 fileStorageDirectory,且 maxDiskUsagePerPart → setMaxPartSize
}
});
}
configureDefaultCodec(Consumer<HttpMessageReader<?>>) 是 Framework 提供的 "遍历所有默认 codec,对匹配类型的做处理"的 API。Boot 用它做类型分派 ------ 因为同一个配置(spring.webflux.multipart.*)要应用到两种不同的 reader 实现:
| reader | 特点 | 差异 |
|---|---|---|
DefaultPartHttpMessageReader |
一次性把整个 part 读进来 | 有 setMaxDiskUsagePerPart、支持 fileStorageDirectory |
PartEventHttpMessageReader |
流式,按 PartEvent 分片 |
只有 setMaxPartSize、无 fileStorageDirectory |
为什么两种都存在? PartEventHttpMessageReader 支持 @RequestPart Flux<PartEvent> 这种流式处理大文件的写法, DefaultPartHttpMessageReader 支持传统的 Mono<FilePart>。 Framework 的 ServerCodecConfigurer 默认两个都注册 ,所以 Boot 要都配置。
PropertyMapper 的三种转换:
java
map.from(x).asInt(DataSize::toBytes) // → int
map.from(x).as(DataSize::toBytes) // → long
map.from(x).as(Paths::get) // → Path
asInt 与 as 的区别是目标类型(int vs long)。 as(Paths::get) 把字符串路径转成 Path------这是属性绑定之外的二次转换 , Binder 本身能做 String → Path 吗?大概能(Boot 有 Path 的 converter), 但这里显式写了,可能是为了统一在这里处理异常。
⚠️ 未核实 :
configureFileStorageDirectory的实现(方法体被截断了)。 从名字推断它会创建目录并校验可写性。自测方法:读该方法剩余部分。
spring.webflux.multipart.* 的四个限制(max-in-memory-size / max-headers-size / max-disk-usage-per-part / max-parts)全部是 DoS 防护------ 文件上传是经典的攻击面(zip bomb、大量小 part 耗尽内存)。
7. WebSessionIdResolverAutoConfiguration:Cookie 的 PropertyMapper
java
@AutoConfiguration
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnClass({ WebSessionManager.class, Mono.class })
@EnableConfigurationProperties({ WebFluxProperties.class, ServerProperties.class })
public final class WebSessionIdResolverAutoConfiguration {
private final ServerProperties serverProperties;
WebSessionIdResolverAutoConfiguration(ServerProperties serverProperties, WebFluxProperties webFluxProperties) {
this.serverProperties = serverProperties;
}
@Bean
@ConditionalOnMissingBean
WebSessionIdResolver webSessionIdResolver() {
CookieWebSessionIdResolver resolver = new CookieWebSessionIdResolver();
String cookieName = this.serverProperties.getReactive().getSession().getCookie().getName();
if (StringUtils.hasText(cookieName)) {
resolver.setCookieName(cookieName);
}
resolver.addCookieInitializer(this::initializeCookie);
return resolver;
}
private void initializeCookie(ResponseCookieBuilder builder) {
Cookie cookie = this.serverProperties.getReactive().getSession().getCookie();
PropertyMapper map = PropertyMapper.get();
map.from(cookie::getDomain).to(builder::domain);
map.from(cookie::getPath).to(builder::path);
map.from(cookie::getHttpOnly).to(builder::httpOnly);
map.from(cookie::getSecure).to(builder::secure);
map.from(cookie::getMaxAge).to(builder::maxAge);
map.from(cookie::getPartitioned).to(builder::partitioned);
map.from(cookie::getSameSite).as(SameSite::attributeValue).always().to(builder::sameSite);
}
}
两个值得注意的点:
-
构造函数注入了
webFluxProperties但没用 (字段都没保存)。 声明它只是为了触发WebFluxProperties的@EnableConfigurationProperties? 不对------@EnableConfigurationProperties已经声明了,不需要注入。 这看起来是一个无用参数。可能的原因:早期版本用过,后来删了逻辑但没删参数。 未核实。这种"注入但不使用"在 Boot 里不多见,值得记一笔。
-
map.from(cookie::getSameSite).as(SameSite::attributeValue).always().to(builder::sameSite)------ 只有这一行有.always()。PropertyMapper的always()表示"即使源值为 null 也调用目标 "。 为什么 SameSite 要 always?因为SameSite可能有一个"未设置"的语义 需要显式传给 builder(比如把SameSite设为null表示"不输出 SameSite 属性")。 而其他属性(domain、path 等)null 时不调用即可(保持 builder 默认)。⚠️ 未核实 :
always()在这里的确切动机。 从 API 语义推断如上,但未读SameSite与ResponseCookieBuilder.sameSite的实现。
spring.reactive.session.cookie.* 支持 partitioned(CHIPS)与 same-site------ 这些是较新的浏览器安全特性,说明 Boot 4.1 的 cookie 配置跟上了标准演进。
8. 错误处理:ErrorWebFluxAutoConfiguration
8.1 只有两个 bean,但 @Order(-1)
java
@AutoConfiguration(before = WebFluxAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnClass(WebFluxConfigurer.class)
@EnableConfigurationProperties(WebProperties.class)
public final class ErrorWebFluxAutoConfiguration {
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
@Order(-1)
ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes, WebProperties webProperties,
ObjectProvider<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext) {
DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(errorAttributes,
webProperties.getResources(), webProperties.getError(), applicationContext);
exceptionHandler.setViewResolvers(viewResolvers.orderedStream().toList());
exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
return exceptionHandler;
}
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
}
注意 errorWebExceptionHandler 的 @ConditionalOnMissingBean 没有 value ------ 它检查的是方法返回类型 ErrorWebExceptionHandler , 即"容器里没有 ErrorWebExceptionHandler 类型的 bean 时才创建"。
@Order(-1) 的含义 :WebExceptionHandler 是有顺序的链, -1 让它排在几乎最前面 (Framework 的 HttpWebHandlerAdapter 按 order 排序调用)。 这样异常能被它捕获并处理,而不是冒泡到 Netty 变成 500 空响应。
对比 MVC 的 BasicErrorController:它是一个 @Controller , 靠 /error 路径和容器的 error page 机制工作。 两个栈的错误处理机制完全不同:
| MVC | WebFlux | |
|---|---|---|
| 机制 | 错误页面(容器转发到 /error) |
WebExceptionHandler 链 |
| 类型 | @Controller |
@Order(-1) 的 bean |
| 注册 | 需要 ErrorPageRegistrar 把 /error 注册给容器 |
不需要 (就是个 bean,被 WebHttpHandlerBuilder 收集) |
| 输出 | view 或 JSON(靠 produces) |
靠 setMessageWriters 注入 codec |
setMessageWriters(serverCodecConfigurer.getWriters()) ------ 错误处理器自己也需要序列化 JSON,所以它直接借用 ServerCodecConfigurer 的 writer 列表。 这是"reactive 栈里 codec 是共享资源"的又一体现。
8.2 before = WebFluxAutoConfiguration 与 MVC 一致
java
@AutoConfiguration(before = WebFluxAutoConfiguration.class)
MVC 那边也是 @AutoConfiguration(before = WebMvcAutoConfiguration.class), 注释写"so that the error View is available"。 WebFlux 这边没有注释 ------因为 reactive 侧没有 BeanNameViewResolver 的依赖问题 (view resolver 是通过 ObjectProvider<ViewResolver> 注入的,不是条件 bean)。
这是一个"同样的 before,不同的理由" ------ 提示我们:看到两个栈有相同的注解时,先确认理由是否相同。
8.3 SearchStrategy.CURRENT(与 MVC 一致)
两个 bean 都用 search = SearchStrategy.CURRENT, 理由与第 19 章 4.2 节相同:让 Actuator 子上下文有自己的错误处理。
9. Reactor Netty:从 HttpHandler 到端口
9.1 NettyReactiveWebServerAutoConfiguration
java
@AutoConfiguration
@ConditionalOnClass({ ReactiveHttpInputMessage.class, HttpServer.class })
@ConditionalOnWebApplication(type = Type.REACTIVE)
@EnableConfigurationProperties(NettyServerProperties.class)
@Import({ ReactiveWebServerConfiguration.class, ReactorResourceFactoryConfiguration.class })
public final class NettyReactiveWebServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean(ReactiveWebServerFactory.class)
NettyReactiveWebServerFactory nettyReactiveWebServerFactory(ReactorResourceFactory resourceFactory,
ObjectProvider<NettyRouteProvider> routes, ObjectProvider<NettyServerCustomizer> serverCustomizers) {
NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory();
serverFactory.setResourceFactory(resourceFactory);
routes.orderedStream().forEach(serverFactory::addRouteProviders);
serverFactory.getServerCustomizers().addAll(serverCustomizers.orderedStream().toList());
return serverFactory;
}
@Bean
NettyReactiveWebServerFactoryCustomizer nettyWebServerFactoryFactoryCustomizer(Environment environment,
ServerProperties serverProperties, NettyServerProperties nettyProperties) {
return new NettyReactiveWebServerFactoryCustomizer(environment, serverProperties, nettyProperties);
}
}
@Import({ ReactiveWebServerConfiguration.class, ReactorResourceFactoryConfiguration.class }) ------ 与第 18 章 servlet 侧的 ServletWebServerConfiguration 对应。 ReactiveWebServerConfiguration 是 spring-boot-web-server 模块里的类 (第 18 章读过),它注册 ReactiveWebServerFactoryCustomizer 等。
三个"收集用户扩展"的注入:
| 注入 | 作用 | 扩展点类型 |
|---|---|---|
ReactorResourceFactory |
共享的 Netty 资源(event loop、buffer pool) | 可替换 |
ObjectProvider<NettyRouteProvider> |
额外路由(gRPC 等) | 4.x 新增 |
ObjectProvider<NettyServerCustomizer> |
直接改 HttpServer |
逃生舱 |
routes.orderedStream().forEach(serverFactory::addRouteProviders) ------ 注意 addRouteProviders(NettyRouteProvider...) 是可变参数, 在循环里一次加一个 (而不是 addRouteProviders(stream.toArray()))。 效果一样,但后者只调一次。这是风格问题,不影响行为。
9.2 ReactorResourceFactory:共享资源
java
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ReactorNettyProperties.class)
public static class ReactorResourceFactoryConfiguration {
@Bean
@ConditionalOnMissingBean
ReactorResourceFactory reactorResourceFactory(ReactorNettyProperties configurationProperties) {
ReactorResourceFactory reactorResourceFactory = new ReactorResourceFactory();
if (configurationProperties.getShutdownQuietPeriod() != null) {
reactorResourceFactory.setShutdownQuietPeriod(configurationProperties.getShutdownQuietPeriod());
}
return reactorResourceFactory;
}
}
spring.reactor.netty.shutdown-quiet-period 只有一个属性------ ReactorNettyProperties 是我在本系列里见过的最小的 @ConfigurationProperties 类。
为什么要共享 ReactorResourceFactory? 因为 Netty 的 event loop group 和 buffer pool 是重量级资源 (每个 JVM 一套就够)。 如果主应用和 Actuator 子上下文各建一套,会翻倍占用线程。 共享一个 bean 让两边复用。
这是"子上下文复用主上下文资源 "的一个例子, 与第 19 章 8.1 节 JacksonComponentModule 向上遍历父 BeanFactory 的思路呼应。
ReactorNettyConfigurations 的 Javadoc:
java
/**
* Configurations for Reactor Netty. Those should be {@code @Import} in a regular
* auto-configuration class.
*/
public final class ReactorNettyConfigurations {
private ReactorNettyConfigurations() { }
...
}
"should be @Import in a regular auto-configuration class" ------ 这是一个给外部模块用的工具类 :如果第三方想写一个基于 Reactor Netty 的服务器模块, 可以 @Import(ReactorResourceFactoryConfiguration.class) 复用这套配置。
这与第 19 章的 JacksonAutoConfiguration 内部类不同------ 那些是 static class(包可见),这个是 public static class。 可见性差异反映了"是否对外提供"。
9.3 createHttpServer() 的七步
java
private HttpServer createHttpServer() {
HttpServer server = HttpServer.create().bindAddress(this::getListenAddress); // ①
Ssl ssl = getSsl();
if (Ssl.isEnabled(ssl)) {
server = customizeSslConfiguration(server, ssl); // ②
}
if (getCompression() != null && getCompression().getEnabled()) {
CompressionCustomizer compressionCustomizer = new CompressionCustomizer(getCompression());
server = compressionCustomizer.apply(server); // ③
}
server = server.protocol(listProtocols()).forwarded(this.useForwardHeaders); // ④
return applyCustomizers(server); // ⑤
}
① bindAddress(this::getListenAddress) ------ 传的是一个 Supplier<InetSocketAddress>, 不是地址本身。这允许延迟解析(配置可能在之后才就绪)。
java
private InetSocketAddress getListenAddress() {
if (getAddress() != null) {
return new InetSocketAddress(getAddress().getHostAddress(), getPort());
}
return new InetSocketAddress(getPort());
}
注意 getAddress().getHostAddress() ------ getAddress() 返回 InetAddress, 再取它的 getHostAddress() 字符串。为什么不直接 new InetSocketAddress(addr, port)? 因为那样会用 InetAddress 的主机名做解析。用字符串避免了一次反向 DNS 查找。
这个推断的置信度中等。未核实。
② SSL :SslServerCustomizer + addBundleUpdateHandler。
java
private void addBundleUpdateHandler(@Nullable String serverName, @Nullable String bundleName,
SslServerCustomizer customizer) {
if (StringUtils.hasText(bundleName)) {
SslBundles sslBundles = getSslBundles();
Assert.state(sslBundles != null, "'sslBundles' must not be null");
sslBundles.addBundleUpdateHandler(bundleName,
(sslBundle) -> customizer.updateSslBundle(serverName, sslBundle));
}
}
SSL Bundle 热更新 (第 30 章的主题)------证书轮换时不用重启服务器。 customizer.updateSslBundle(serverName, sslBundle) 把新证书推进去。 serverName 对应 SNI:一个服务器可以有多套证书(按域名区分)。
④ protocol(listProtocols()):
java
private HttpProtocol[] listProtocols() {
List<HttpProtocol> protocols = new ArrayList<>();
protocols.add(HttpProtocol.HTTP11);
if (getHttp2() != null && getHttp2().isEnabled()) {
if (getSsl() != null && getSsl().isEnabled()) {
protocols.add(HttpProtocol.H2); // HTTP/2 over TLS
}
else {
protocols.add(HttpProtocol.H2C); // HTTP/2 over cleartext
}
}
return protocols.toArray(new HttpProtocol[0]);
}
H2 vs H2C 的选择完全取决于 SSL 是否启用 ------ 这是 HTTP/2 规范的要求:浏览器只支持 h2(带 TLS), h2c(明文)主要用于内部服务间通信。
HTTP/1.1 总是被加入 (protocols.add(HttpProtocol.HTTP11))------ 因为 Netty 用 ALPN 协商,需要 HTTP/1.1 作为降级选项。
⑤ applyCustomizers:
java
private HttpServer applyCustomizers(HttpServer server) {
for (NettyServerCustomizer customizer : this.serverCustomizers) {
server = customizer.apply(server);
}
return server;
}
serverCustomizers 是 LinkedHashSet------保序 + 去重。 去重意味着同一个 customizer 实例不会被应用两次。
9.4 getWebServer(HttpHandler):与第 18 章的衔接
java
@Override
public WebServer getWebServer(HttpHandler httpHandler) {
HttpServer httpServer = createHttpServer();
ReactorHttpHandlerAdapter handlerAdapter = new ReactorHttpHandlerAdapter(httpHandler);
NettyWebServer webServer = createNettyWebServer(httpServer, handlerAdapter, this.lifecycleTimeout,
getShutdown());
webServer.setRouteProviders(this.routeProviders);
return webServer;
}
ReactorHttpHandlerAdapter 是关键的一层: 它把 Framework 的 HttpHandler(Mono<Void> handle(ServerHttpRequest, ServerHttpResponse)) 适配成 Reactor Netty 的 BiFunction<HttpServerRequest, HttpServerResponse, Mono<Void>>。
这就是 servlet 栈里 DispatcherServlet 的位置------ 一个"框架模型 ↔ 服务器模型"的适配器。
webServer.setRouteProviders(this.routeProviders) 在构造后 再设置------ 因为 NettyWebServer 的构造函数参数里没有它(第 18 章读过: new NettyWebServer(httpServer, handlerAdapter, lifecycleTimeout, shutdown, resourceFactory))。
这是一个可以吐槽的设计 :为什么不把 routeProviders 也放进构造参数? 可能是因为 NettyRouteProvider 是 4.0 新增的,加构造参数会破坏兼容性 (NettyWebServer 是 public 类)。推测,未核实。
9.5 NettyRouteProvider:route vs handle
第 18 章读过 NettyWebServer 的:
java
DisposableServer startHttpServer() {
HttpServer server = this.httpServer;
if (this.routeProviders.isEmpty()) {
server = server.handle(this.handler); // 所有请求走同一个 handler
}
else {
server = server.route(this::applyRouteProviders); // 允许注册自定义路由
}
...
}
NettyRouteProvider 的定义:
java
/**
* Function that can add new routes to an {@link HttpServerRoutes} instance.
*
* @since 4.0.0
* @see NettyReactiveWebServerFactory
*/
@FunctionalInterface
public interface NettyRouteProvider extends Function<HttpServerRoutes, HttpServerRoutes> {
}
它直接继承了 JDK 的 Function !所以 @FunctionalInterface 其实是冗余的 (Function 本身就是函数式接口)------但写上能让 IDE 更明确地提示。
handle vs route 的区别(Reactor Netty API):
handle(BiFunction<request, response, Publisher<Void>>): 所有请求交给同一个函数route(Consumer<HttpServerRoutes>): 可以按路径/方法/头部注册多个路由,不匹配的走默认
为什么默认用 handle? 因为 WebFlux 的 DispatcherHandler 自己做路由 , Netty 层不需要再路由一次(双重路由 = 浪费 )。 只有当用户要注册绕过 WebFlux 的 路由时(比如 gRPC 的 HTTP/2 端点、 Actuator 的特殊路径)才需要 route 模式。
这是第 18 章第十八定律(多实现的差异点 = 底层 API 能力差)的一个补充案例 : Tomcat/Jetty 的 servlet 模型没有 "绕过 servlet 容器直接注册路由"的能力, 所以只有 Netty 有 NettyRouteProvider。
9.6 NettyReactiveWebServerFactoryCustomizer
java
@Override
public void customize(NettyReactiveWebServerFactory factory) {
factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders());
PropertyMapper map = PropertyMapper.get();
map.from(this.nettyProperties::getConnectionTimeout)
.to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout));
map.from(this.nettyProperties::getIdleTimeout).to((idleTimeout) -> customizeIdleTimeout(factory, idleTimeout));
map.from(this.nettyProperties::getMaxKeepAliveRequests)
.to((maxKeepAliveRequests) -> customizeMaxKeepAliveRequests(factory, maxKeepAliveRequests));
if (this.serverProperties.getHttp2().isEnabled()) {
map.from(this.serverProperties.getMaxHttpRequestHeaderSize())
.to((size) -> customizeHttp2MaxHeaderSize(factory, size.toBytes()));
}
customizeRequestDecoder(factory, map);
}
注意它把配置转成 NettyServerCustomizer 加进 factory,而不是直接 setter:
java
private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) {
factory.addServerCustomizers((httpServer) -> httpServer.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
(int) connectionTimeout.toMillis()));
}
因为 Netty 的这些选项(ChannelOption.*、httpRequestDecoder 等) 不在 AbstractReactiveWebServerFactory 的抽象属性里 ------ 它们是 Netty 特有的。所以 Boot 用 NettyServerCustomizer 把它们"降级存储", 在 applyCustomizers 阶段才应用。
这是一个通用的模式:抽象层没有的属性 → 用 customizer 承载。
getOrDeduceUseForwardHeaders() 的推断逻辑:
java
private boolean getOrDeduceUseForwardHeaders() {
if (this.serverProperties.getForwardHeadersStrategy() == null) {
CloudPlatform platform = CloudPlatform.getActive(this.environment);
return platform != null && platform.isUsingForwardHeaders();
}
return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE);
}
三态:
server.forward-headers-strategy未配 → 推断:看当前云平台是否用 forward headers- 配了
NATIVE→ true - 配了别的(
FRAMEWORK/NONE)→ false
CloudPlatform.getActive(environment) 是第 11 章讲过的云平台检测 (spring.main.cloud-platform 或自动检测 K8s/CF/Heroku)。 isUsingForwardHeaders() 表示"该平台会在请求里注入 X-Forwarded-* 头"。
这是一个很实用的默认行为 :在 K8s/云平台上,应用通常在反向代理后面, X-Forwarded-* 必须被信任,否则生成的 URL(重定向、HATEOAS 链接)会是内网地址。 Boot 自动开启,省了一堆"为什么重定向到了 10.x.x.x"的问题。
9.7 两个前缀的不对称:server.netty vs spring.reactor.netty
java
@ConfigurationProperties("server.netty")
public class NettyServerProperties { ... } // connection-timeout / idle-timeout / max-keep-alive-requests
@ConfigurationProperties("spring.reactor.netty")
public class ReactorNettyProperties { ... } // shutdown-quiet-period
两个 Netty 相关配置用了不同的前缀 :一个是 server.netty(在 server.* 命名空间下, 与其他服务器配置同源),另一个是 spring.reactor.netty(在 spring.* 下)。
这个不对称大概有这样的历史/语义依据:
server.netty.*是服务器实例相关的(连接超时、空闲超时)spring.reactor.netty.*是 Reactor Netty 库本身相关的(全局资源的关闭静默期)
但从使用者角度看这很容易记混 。我把它列出来是因为: Boot 的配置前缀不是完全正交的,server.* 与 spring.* 的分界有历史包袱。 第 19 章也见过:spring.web.resources.*(静态资源)与 server.error.*(错误页)分属两边。
9.8 ReactiveWebServerConfiguration:reactive 侧的服务器装配
第 18 章读过 servlet 侧的 ServletWebServerConfiguration,这里是它的 reactive 对照:
java
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ServerProperties.class)
@Import(ReactiveWebServerConfiguration.BeanPostProcessorsRegistrar.class)
public class ReactiveWebServerConfiguration {
@Bean
ReactiveWebServerFactoryCustomizer reactiveWebServerFactoryCustomizer(ServerProperties serverProperties,
ObjectProvider<SslBundles> sslBundles) {
return new ReactiveWebServerFactoryCustomizer(serverProperties, sslBundles.getIfAvailable());
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
/**
* Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via
* {@link ImportBeanDefinitionRegistrar} for early registration.
*/
static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware { ... }
}
BeanPostProcessorsRegistrar 与 servlet 版完全一样 (同样的注释、同样的 registerSyntheticBeanIfMissing)------第 18 章 13.3 节讲过为什么用 ImportBeanDefinitionRegistrar 而不是 @Bean(BPP 必须早于其他 bean 就位)。
ReactiveWebServerFactoryCustomizer 的 7 个映射:
java
@Override
public void customize(ConfigurableReactiveWebServerFactory factory) {
PropertyMapper map = PropertyMapper.get();
map.from(this.serverProperties::getPort).to(factory::setPort);
map.from(this.serverProperties::getAddress).to(factory::setAddress);
map.from(this.serverProperties::getSsl).to(factory::setSsl);
map.from(this.serverProperties::getCompression).to(factory::setCompression);
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
map.from(() -> this.sslBundles).to(factory::setSslBundles);
}
注意最后一行 map.from(() -> this.sslBundles) ------ 用 lambda 而不是方法引用,因为 sslBundles 是字段 而非 getter。 PropertyMapper.from(Supplier<T>) 接受任意 Supplier。
对比 servlet 版的 ServletWebServerFactoryCustomizer (第 18 章读过): 它还有 contextPath、displayName、session.*、tomcat.* 等更多映射, 因为 servlet 容器的可配项多得多。reactive 侧只有 7 项------ 又一次看到"servlet 的复杂度来自容器 API 笨重"。
9.9 forward-headers-strategy 的三种取值:NONE / NATIVE / FRAMEWORK
这是本章最值得单独记住的一个配置项。三种策略分布在两个地方:
| 取值 | 生效位置 | 机制 |
|---|---|---|
NATIVE |
NettyReactiveWebServerFactoryCustomizer.getOrDeduceUseForwardHeaders() |
交给服务器 (Netty)处理:server.forwarded(true),由 Netty 改写 request 的 scheme/host/port |
FRAMEWORK |
ReactiveWebServerConfiguration.forwardedHeaderTransformer() |
交给 Framework :注册 ForwardedHeaderTransformer bean,在 WebFlux 的 filter 链里改写 |
NONE |
都不注册 | 不处理 X-Forwarded-*,生成的 URL 用内网地址 |
为什么要有两种?
NATIVE:性能好 (在协议层就改了),但只对 Netty 有效 (Tomcat/Jetty 有没有对应的 native 支持?未核实)FRAMEWORK:通用(不依赖服务器),但多一层处理
而且 NATIVE 是 Netty 的推断默认值 : 9.6 节的 getOrDeduceUseForwardHeaders() 在未显式配置时, 若检测到云平台会用 forward headers,就返回 true------即默认按 NATIVE 处理。
这就形成了一个有趣的状态: 不配 server.forward-headers-strategy 时,Netty 走推断(可能 NATIVE); 显式配 framework 时走 Framework。 两者可以同时生效(Netty 改一遍,Framework 再改一遍)------
⚠️ 未核实 :同时生效会不会导致重复改写(比如 URL 里出现两次协议前缀)。 从
getOrDeduceUseForwardHeaders()的逻辑看, 显式配了FRAMEWORK时它返回false(因为不等于NATIVE), 所以不会 同时生效。这个推断置信度较高。
10. 完整时序:从 run() 到第一个响应
把本章与第 17、18 章的内容串起来,一个 WebFlux + Netty 应用的完整启动与首个请求:
scss
═════════════ 启动阶段 ═════════════
① SpringApplication.run()
② WebApplicationType.deduce() → REACTIVE
(靠 WebFluxWebApplicationTypeDeducer,核心兜底只给 SERVLET/NONE) ← 第 17 章
③ ApplicationContextFactory 创建
AnnotationConfigReactiveWebServerApplicationContext ← 第 17 章
④ refresh() → onRefresh() → createWebServer()
⑤ createWebServer()
⑥ 拿 ReactiveWebServerFactory bean
= NettyReactiveWebServerFactory(由 NettyReactiveWebServerAutoConfiguration 创建)
(已被 NettyReactiveWebServerFactoryCustomizer 定制过)
⑦ 拿 HttpHandler bean
= ContextPathCompositeHandler 或 WebHttpHandlerBuilder.build() 的结果
(由 HttpHandlerAutoConfiguration 创建,内部收集了 DispatcherHandler、
WebExceptionHandler、WebFilter、WebSessionManager...)
⑧ factory.getWebServer(httpHandler)
→ createHttpServer()(七步)
→ new ReactorHttpHandlerAdapter(httpHandler)
→ new NettyWebServer(...)
→ webServer.setRouteProviders(...)
═════════════ refresh() 后半程 ═════════════
⑨ WebServerManager 初始化(DelayedInitializationHttpHandler 装真实 handler)
⑩ WebServerStartStopLifecycle.start() → webServer.start()
→ startHttpServer() → bindNow() ← ★ 端口在这里才绑定
→ 打印 "Netty started on port 8080" ← 第 18 章
═════════════ 第一个请求 ═════════════
⑪ Netty event loop 收到字节 → HttpServerRequest
⑫ routeProviders 为空 → server.handle(ReactorHttpHandlerAdapter)
⑬ ReactorHttpHandlerAdapter → HttpHandler.handle(...)
⑭ (若有 basePath)ContextPathCompositeHandler 按前缀路由
⑮ DispatcherHandler.handle(exchange)
⑯ RequestMappingHandlerMapping.getHandler(exchange) → Mono<Object>
⑰ HandlerAdapter.handle(...) → Mono<HandlerResult>
⑱ HandlerResultHandler → 用 ServerCodecConfigurer 的 writer 编码
⑲ JacksonJsonEncoder(包装了 JsonMapper)→ Flux<DataBuffer>
⑳ Netty 写回响应
对比 servlet 栈的关键差异:
| 环节 | servlet | reactive |
|---|---|---|
| 谁驱动请求 | servlet 容器(Tomcat 的线程池) | Netty event loop |
| 入口组件 | DispatcherServlet(容器持有) |
HttpHandler(一个 bean) |
| 路由 | HandlerMapping(同步查找) |
HandlerMapping(返回 Mono) |
| 编码 | HttpMessageConverter.write(..., OutputStream) |
Encoder → Flux<DataBuffer> |
| 背压 | 无 | Flux 天然支持 |
DelayedInitializationHttpHandler (第 18 章读过)在这里的作用是: 第 ⑦ 步时 HttpHandler 可能还没完全就绪(bean 还在创建中), 所以先放一个哨兵,到第 ⑨ 步才装真实的。 这解决的是"循环依赖" :WebServerFactory 需要 HttpHandler, 而 HttpHandler 的构建需要整个容器。
11. 观测:WebFluxObservationAutoConfiguration------没有 Filter
java
@AutoConfiguration(afterName = {
"...SimpleMetricsExportAutoConfiguration",
"...ObservationAutoConfiguration" })
@ConditionalOnClass({ Observation.class, MeterRegistry.class })
@ConditionalOnBean({ ObservationRegistry.class, MeterRegistry.class })
@ConditionalOnWebApplication(type = Type.REACTIVE)
@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
public final class WebFluxObservationAutoConfiguration {
private final ObservationProperties observationProperties;
@Bean
@Order(0)
MaximumAllowableTagsMeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties) {
String meterNamePrefix = this.observationProperties.getHttp().getServer().getRequests().getName();
int maxUriTags = metricsProperties.getWeb().getServer().getMaxUriTags();
return new MaximumAllowableTagsMeterFilter(meterNamePrefix, "uri", maxUriTags);
}
@Bean
@ConditionalOnMissingBean(ServerRequestObservationConvention.class)
DefaultServerRequestObservationConvention defaultServerRequestObservationConvention() {
return new DefaultServerRequestObservationConvention(
this.observationProperties.getHttp().getServer().getRequests().getName());
}
}
与 MVC 版的最大区别:没有 FilterRegistrationBean。
第 19 章 MVC 版注册了 ServerHttpObservationFilter(一个 servlet Filter, order HIGHEST_PRECEDENCE + 1)。WebFlux 版只注册了两个 bean : 一个 MeterFilter(限制 tag 基数)和一个 ServerRequestObservationConvention(命名约定)。
为什么不需要 Filter? 因为观测能力已经内建在 WebFlux 里 了------ Framework 的 DispatcherHandler/RouterFunctionMapping 自带 observation 支持, 只要容器里有 ObservationRegistry bean 就会自动启用。
这是一个很好的对照:
| MVC | WebFlux | |
|---|---|---|
| 观测实现 | 外挂 Filter (ServerHttpObservationFilter) |
内建(Framework 自带) |
| Boot 提供 | filter 注册 + 命名约定 + tag 限制 | 只有命名约定 + tag 限制 |
判据 :当 Boot 的自动配置只注册配置对象而不注册功能组件 时, 通常意味着功能已经内建在框架里。这是一个判断"Boot 在这里做了多少事"的快捷方法。
@ConditionalOnClass({ Observation.class, MeterRegistry.class }) + @ConditionalOnBean({...}) ------ 类检查 + bean 检查双重条件。bean 检查是必要的: Micrometer 在 classpath 上不代表用户启用了观测(可能只用了 metrics 没用 observation)。
12. WebFluxProperties 与 WebMvcProperties 的字段对比
从两个 grep 的结果看字段结构:
WebMvcProperties(spring.mvc) |
WebFluxProperties(spring.webflux) |
|
|---|---|---|
| 日期格式 | format.* |
format.* ✅ 相同 |
| 问题详情 | problemdetails.enabled |
problemdetails.enabled ✅ 相同 |
| API 版本 | apiversion.* |
apiversion.* ✅ 相同 |
| 静态路径 | static-path-pattern = /** |
static-path-pattern = /** ✅ 相同 |
| webjars | webjars-path-pattern = /webjars/** |
webjars-path-pattern = /webjars/** ✅ 相同 |
| servlet 路径 | servlet.path、servlet.load-on-startup |
❌ 无 |
| 异步 | async.request-timeout |
❌ 无(reactive 不需要) |
| 视图 | view.prefix/view.suffix |
❌ 无 |
| 内容协商 | contentnegotiation.* |
❌ 无 |
| 路径匹配 | pathmatch.matching-strategy |
❌ 无(reactive 只有 PathPattern) |
| 消息码 | messageCodesResolverFormat |
❌ 无 |
| DispatcherServlet 开关 | dispatch-trace/options-request、publish-request-handled-events、log-request-details |
❌ 无 |
| base path | ❌ 无(用 server.servlet.context-path) |
✅ base-path |
| HTML 转义 | ❌ 无 | ✅ default-html-escape |
WebMvcProperties 有 39 个 private 字段,WebFluxProperties 明显更少。
差异的根源很清楚:
- MVC 多的:都是 servlet 特有的(servlet 路径、异步、视图解析、内容协商、路径匹配策略)
- WebFlux 多的 :
base-path(因为没有容器的 context-path 概念)、default-html-escape
"视图"相关配置 MVC 有而 WebFlux 没有 ------ 但 WebFlux 也支持 视图(ViewResolver、ViewResolutionResultHandler)。 WebFlux 没有 view.prefix/suffix 是因为它的默认视图解析器不是 InternalResourceViewResolver(那是 JSP 时代的产物)。
13. 两个栈的选择与共存:实践决策表
13.1 @ConditionalOnWebApplication 的互斥保证了什么
WebMvcAutoConfiguration 是 type = SERVLET,WebFluxAutoConfiguration 是 type = REACTIVE。 两者互斥 ------所以即使 classpath 上同时有 spring-webmvc 和 spring-webflux, 也只会有一套 Web 自动配置生效。
结合第 17 章的结论(MVC Deducer @Order(10) 先于 WebFlux Deducer @Order(20)):
| classpath | WebApplicationType |
生效的自动配置 |
|---|---|---|
| 只有 spring-webmvc | SERVLET |
MVC |
| 只有 spring-webflux | REACTIVE |
WebFlux |
| 两者都有 | SERVLET |
MVC (4.x 靠 @Order 短路) |
| 两者都没有 | NONE |
都不生效(无 Web 服务器) |
"两者都有 → SERVLET" 是最容易踩的一条。 很多人以为"引了 webflux 就是响应式应用"------不对 , 只要 spring-boot-starter-web(或 spring-webmvc)在 classpath 上,就是 servlet 应用。
想强制切换:
yaml
spring:
main:
web-application-type: reactive
第 17 章讲过:spring.main.web-application-type 能覆盖编程式设置, 因为 bindToSpringApplication 在 createApplicationContext 之前执行。
13.2 五个常见坑
坑 1:引了 spring-boot-starter-webflux,但应用其实是 SERVLET
症状:@EnableWebFlux、RouterFunction、WebFlux 的 WebFilter 全部不生效, 且没有任何报错。
原因:WebFluxAutoConfiguration 的 @ConditionalOnWebApplication(type = REACTIVE) 不满足。 诊断:看 /actuator/conditions 里 WebFluxAutoConfiguration 的 negative matches (第 16 章讲过)。
坑 2:以为 WebClient 需要 WebFlux 应用
不需要 。如 5.8 节所述,CodecsAutoConfiguration 没有 @ConditionalOnWebApplication, 在纯 servlet 应用里也会生效,WebClient 完全可用。 这是 Boot 里"客户端能力独立于服务器类型"的一个体现。
坑 3:spring.http.converters.* 配了但没效果
如果你的应用是 REACTIVE,那么生效的是 spring.http.codecs.*, spring.http.converters.* 完全不起作用 (HttpMessageConvertersAutoConfiguration 用 NotReactiveWebApplicationCondition 把自己排除了,第 19 章 6.2 节)。
反过来也一样:两个前缀容易记混。
坑 4:spring.mvc.* 与 spring.webflux.* 的命名不对称
第 19 章 3.2 节与本章 3.1 节都提到:
- MVC 用
spring.mvc.hiddenmethod.filter.enabled - WebFlux 用
spring.webflux.hiddenmethod.filter.enabled
同样是 problemdetails:
spring.mvc.problemdetails.enabledspring.webflux.problemdetails.enabled
前缀一个是 spring.mvc,一个是 spring.webflux ------ 如果你从 MVC 迁移到 WebFlux,所有 spring.mvc.* 都要改成 spring.webflux.* (而且不是一一对应,见第 12 节的字段对比表)。
坑 5:@ConditionalOnMissingBean(WebFluxConfigurationSupport) 被 @EnableWebFlux 触发
与 MVC 的 @EnableWebMvc 一样,用户写了 @EnableWebFlux 会让 整个 WebFluxAutoConfiguration 失效(包括静态资源、欢迎页、编解码器定制)。 这是"要么全接管、要么全交给 Boot"的二元选择。
13.3 决策建议
不是本章源码事实,而是基于本章内容的工程判断:
| 场景 | 建议 |
|---|---|
| 传统 CRUD + 阻塞 JDBC | MVC (虚拟线程 + spring.threads.virtual.enabled=true) |
| 高并发 + 少量线程(R2DBC、Mongo reactive) | WebFlux |
| 需要 SSE / WebSocket 长连接 | 两者皆可 ,WebFlux 的 Flux 返回更自然 |
| 只调用外部 HTTP(用 WebClient) | MVC 即可(WebClient 不需要 WebFlux 应用) |
| 已有大量阻塞库(JPA、Redis 同步客户端) | MVC(WebFlux 里调阻塞库会毁掉吞吐) |
最容易被忽略的一条 :WebFlux 的收益来自"全链路非阻塞 "。 只要有一处阻塞(JPA、RestTemplate、Thread.sleep), 整个 event loop 会被卡住,性能比 MVC 还差 。 所以 3.5 节的 configureBlockingExecution 才要配一个线程池来兜底。
12.5 模块文件分布:51 个文件都干了什么
spring-boot-webflux 主源码(不含测试)按包统计:
| 包 | 文件数 | 内容 |
|---|---|---|
webflux/(顶层) |
2 | WebFluxWebApplicationTypeDeducer + package-info |
webflux/autoconfigure/ |
14 | 5 个普通自动配置 + 属性类 + 注册接口 + 定制器接口 |
webflux/autoconfigure/error/ |
4 | ErrorWebFluxAutoConfiguration + 3 个异常处理器 |
webflux/autoconfigure/actuate/** |
5 | 健康检查扩展、HTTP exchanges、映射端点 |
webflux/error/ |
4 | ErrorAttributes / DefaultErrorAttributes / ErrorWebExceptionHandler |
webflux/filter/ |
3 | OrderedWebFilter / OrderedHiddenHttpMethodFilter |
webflux/actuate/** |
8 | 端点 handler mapping、HTTP exchanges 记录器 |
几个观察:
error出现了两次 :webflux.error(4 个)与webflux.autoconfigure.error(4 个)。 前者是公共 API (ErrorAttributes接口等),后者是自动配置 。 这与第 03 章讲的 4.x 模块化改造一致:API 与自动配置分离到不同包。filter包只有 3 个文件------对比 servlet 栈的 filter 机制(由容器管理), reactive 的 filter 就是普通 bean,Boot 不需要提供多少基础设施。actuate相关占了 13 个文件(约 25%)------ Actuator 在 WebFlux 模块里的分量很重,第 25 章会展开。
OrderedWebFilter (webflux/filter/):
java
public class OrderedWebFilter implements WebFilter, Ordered { ... }
它是 HiddenHttpMethodFilter 的排序包装(3.1 节用到 OrderedHiddenHttpMethodFilter)。 为什么需要它? 因为 HiddenHttpMethodFilter 本身不实现 Ordered , 而 WebHttpHandlerBuilder 收集 filter 时需要按 order 排序。 Boot 包一层加上 order------这是"给第三方类补接口"的标准手法。
14. 事实清单
本章从源码直接确认的事实:
spring-boot-webflux有 9 条.imports条目(5 个普通自动配置 + 4 个 Actuator 相关), 比 webmvc 多,且没有AutoConfigureWebServer.imports测试通道。WebFluxAutoConfiguration与WebMvcAutoConfiguration的类级注解几乎逐条对称 : 同样的afterName = ValidationAutoConfiguration、同样的HIGHEST_PRECEDENCE + 10、 同样的@ImportRuntimeHints(WebResourcesRuntimeHints.class)(同一个类)。@ConditionalOnWebApplication的SERVLETvsREACTIVE是两者唯一的本质差异;@ConditionalOnClass分别是 3 个类 vs 1 个 类(WebFluxConfigurer)。configureApiVersioning的实现在两边逐行相同,只有 properties 来源不同。configureBlockingExecution(WebFlux)比configureAsyncSupport(MVC) 多一个Threading.VIRTUAL.isActive(environment)条件 ------ 只在虚拟线程开启时才配BlockingExecutionConfigurer。WebFluxAutoConfiguration的 logger 用WebFluxConfig.class, MVC 的用WebMvcConfigurer.class(MVC 那边看起来是 copy-paste 瑕疵)。WelcomePageConfiguration.welcomePageRouterFunctionMapping是一个可能返回 null 的@Bean------reactive 侧选择"不注册"而不是注册一个空 mapping;MVC 侧则总是注册。RouterFunctionMapping.setOrder(1)(MVC 的WelcomePageHandlerMapping是 2)。WebFluxConfig比 MVC Adapter 多注入Environment(只为Threading.VIRTUAL)。addResourceHandlers两边都检查hasMappingForPattern, 但 MVC 抽了私有方法、WebFlux 写了两个独立if; WebFlux 支持多个 定制器(orderedStream()),MVC 只取一个 (getIfAvailable())。configureResourceCaching(WebFlux)在数据层 做cachePeriod → cacheControl.maxAge回退, MVC 则是先setCachePeriod再setCacheControl(后者覆盖)。WebFluxRegistrations比WebMvcRegistrations少getExceptionHandlerExceptionResolver------因为 reactive 栈没有ExceptionHandlerExceptionResolver(servlet 专用类型)。EnableWebFluxConfiguration有webSessionManager()(MVC 无), 因为 servlet 的 session 由容器管理、reactive 的必须自己管。webSessionManager与localeContextResolver都用@ConditionalOnMissingBean(name = WebHttpHandlerBuilder.Xxx_BEAN_NAME)------ 又一次"按名字协作"。HttpHandlerAutoConfiguration只有一个@Bean方法, 类级@ConditionalOnMissingBean(HttpHandler.class)是 WebFlux 的最大粒度开关。WebHttpHandlerBuilder.applicationContext(applicationContext)是 从容器收集WebExceptionHandler/WebFilter/WebSessionManager的机制------ 这解释了 13 里为什么要按名字。spring.webflux.base-path通过ContextPathCompositeHandler实现 (servlet 侧对应server.servlet.context-path)。CodecsAutoConfiguration的CodecCustomizer是@FunctionalInterface, Boot 自己用 lambda 实现;servlet 侧的 customizer 因为要同时实现Client/Server两个接口,只能用具名类。NoJacksonOrJackson2Preferred extends AnyNestedCondition------ Jackson2 codec 在"(无 Jackson 3)或(preferred=jackson2)"时生效。@ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper")用字符串 (Codecs 模块不编译期依赖 Jackson)。注意全限定名带.json.子包。- kotlinx 序列化的 codec 在无任何 JSON 库 时用
(type) -> true谓词接管所有类型。 spring.http.codecs.*只有两个属性:log-request-details(默认 false) 与max-in-memory-size(默认 null,各 codec 默认 256K)。- servlet 的
spring.http.converters.preferred-json-mapper与 reactive 的spring.http.codecs.preferred-json-mapper是两个不同属性。 ReactiveMultipartAutoConfiguration用configureDefaultCodec做类型分派, 同时配置DefaultPartHttpMessageReader与PartEventHttpMessageReader; 后者没有fileStorageDirectory且用setMaxPartSize(不是setMaxDiskUsagePerPart)。WebSessionIdResolverAutoConfiguration构造函数注入了webFluxProperties但没有使用(未保存到字段)------疑似遗留参数。initializeCookie里 7 个PropertyMapper映射中, 只有sameSite用了.always()。ErrorWebFluxAutoConfiguration的errorWebExceptionHandler是@Order(-1)的WebExceptionHandlerbean(MVC 用的是@Controller+ 容器 error page)。- 它直接
setMessageWriters(serverCodecConfigurer.getWriters())------ 借用 codec 的 writer,体现"reactive 栈 codec 是共享资源"。 NettyReactiveWebServerAutoConfiguration收集三类扩展:ReactorResourceFactory、NettyRouteProvider、NettyServerCustomizer。ReactorNettyConfigurations.ReactorResourceFactoryConfiguration是public static(Javadoc 明说 "should be@Importin a regular auto-configuration class"), 与第 19 章那些包可见的内部类不同------可见性反映了是否对外提供。spring.reactor.netty.*只有shutdown-quiet-period一个属性 (本系列见过的最小@ConfigurationProperties类)。createHttpServer()的协议选择:HTTP/1.1 总是加入 , HTTP/2 按 SSL 启用与否选H2(TLS)或H2C(明文)。getListenAddress()用getAddress().getHostAddress()(字符串 ) 而不是直接传InetAddress。serverCustomizers是LinkedHashSet------保序 + 去重。NettyRouteProvider extends Function<HttpServerRoutes, HttpServerRoutes>------ 直接继承 JDK 的Function,上面的@FunctionalInterface是冗余的。- 默认用
server.handle(...)(单一 handler), 只有注册了NettyRouteProvider才切到server.route(...)。 NettyReactiveWebServerFactoryCustomizer把 Netty 特有选项 转成NettyServerCustomizer加进 factory(因为抽象层没有这些属性)。getOrDeduceUseForwardHeaders()三态:未配时看CloudPlatform.getActive(environment).isUsingForwardHeaders()。- Netty 相关有两个前缀 :
server.netty.*(服务器实例)与spring.reactor.netty.*(库本身)。 WebFluxObservationAutoConfiguration不注册 Filter ------ 观测能力内建在 WebFlux 里,Boot 只提供命名约定与MaximumAllowableTagsMeterFilter。WebMvcProperties有 39 个private字段,WebFluxProperties明显更少; 多出来的是 servlet 特有项,WebFlux 独有base-path与default-html-escape。
15. 本章未逐行核实 / 待你实测的项
| # | 待核实项 | 验证方法 |
|---|---|---|
| 1 | @Bean 方法返回 null 的确切行为 |
读 Framework 的 ConfigurationClassBeanDefinitionReader;写一个返回 null 的 @Bean 测试 |
| 2 | WebFluxProperties.cleanBasePath() 的实现与调用点 |
读该文件第 74 行附近 |
| 3 | MaxIdleTimeInMemoryWebSessionStore 是否必需 |
读 InMemoryWebSessionStore 是否有 setMaxIdleTime(Duration) |
| 4 | configureFileStorageDirectory 的实现 |
读 ReactiveMultipartAutoConfiguration 剩余部分 |
| 5 | WebSessionIdResolverAutoConfiguration 的无用构造参数 |
git blame 看是否曾经用过 |
| 6 | always() 在 sameSite 上的动机 |
读 SameSite 与 ResponseCookieBuilder.sameSite |
| 7 | Jackson 3 的 JsonMapper 全限定名(tools.jackson.databind.json.JsonMapper) |
import 语句已间接证实,但值得确认 |
| 8 | PropertyMapper.asInt 溢出 Integer.MAX_VALUE 时的行为 |
配一个 4GB 的 max-in-memory-size 试 |
| 9 | getListenAddress() 用 getHostAddress() 的理由 |
可能是避免反向 DNS;未确认 |
| 10 | NettyWebServer 为何不把 routeProviders 放进构造参数 |
git log 看是否是 4.0 新增时为了兼容 |
| 11 | Threading.VIRTUAL.isActive 对应的属性名与默认值 |
读 Framework 的 Threading 枚举 |
| 12 | spring.webflux.multipart.* 的默认值 |
读 ReactiveMultipartProperties |
| 13 | NettyServerProperties 的完整属性列表 |
读该文件 |
| 14 | ServerCodecConfigurer 的 writer/reader 列表如何组装 |
Framework 源码,第 20 章范围外 |
| 15 | ContextPathCompositeHandler 的行为(多路径映射?) |
Framework 源码 |
| 16 | @AutoConfigureWebTestClient 的注册点(对比 MVC 的 AutoConfigureWebServer) |
全仓 grep |
| 17 | 第 19 章遗留的 ServerBuilder.withJsonConverter 语义 |
仍未解决 |
| 18 | spring.threads.virtual.enabled 与 BlockingExecutionConfigurer 的完整交互 |
第 30 章 |
16. 方法论:本章沉淀的三条定律
15.1 第二十二定律:平行实现的不对称处 = 历史包袱或底层约束
Boot 对两个栈的自动配置高度对称;凡是出现不对称的地方, 要么是底层 API 约束(必有理由),要么是历史遗留(可能无理由)。区分二者的方法是: 如果去掉不对称会导致功能缺失或类型不匹配 → 底层约束;否则 → 历史遗留。
本章的不对称清单:
| 不对称 | 判定 | 理由 |
|---|---|---|
createExceptionHandlerExceptionResolver 只有 MVC 有 |
底层约束 | reactive 没有 HandlerExceptionResolver 类型 |
webSessionManager 只有 WebFlux 有 |
底层约束 | servlet 的 session 由容器管 |
NettyRouteProvider 只有 Netty 有 |
底层约束 | servlet 容器没有"绕过 servlet 注册路由"的能力 |
CodecCustomizer 用 lambda,ConvertersCustomizer 用具名类 |
底层约束(接口设计) | 后者要同时实现两个接口 |
属性前缀 spring.mvc vs spring.webflux |
历史遗留 | WebFlux 出现得晚 |
| MVC 抽私有方法、WebFlux 写两遍 | 历史遗留 | 后者更好但前者没跟改 |
MVC 的 logger 用 WebMvcConfigurer.class |
历史遗留(bug) | WebFlux 用对了 |
server.netty vs spring.reactor.netty |
半历史半语义 | 一个管实例、一个管库 |
判据的价值 :读源码时遇到"这里为什么和那边不一样", 先按这个二分法分类------能省下大量"是不是我理解错了"的自我怀疑, 也能快速定位哪些是真 bug(可以去提 issue)。
15.2 第二十三定律:@Bean 返回 null 是"条件注册"的隐藏形式
当"注册一个无效的空对象"有副作用时,Boot 会选择让
@Bean方法返回 null 来"不注册"; 当空对象无害时,就照常注册。判断依据是"这个对象是否参与匹配/路由"。
本章的证据对:
| 做法 | 原因 | |
|---|---|---|
WelcomePageHandlerMapping(MVC) |
总是注册 (内部 if 决定是否 setRootHandler) |
HandlerMapping 不匹配就返回 null,无害 |
RouterFunctionMapping(WebFlux) |
返回 null 不注册 | 空 router function 可能干扰路由匹配 |
第 19 章还有一个佐证:beanNameViewResolver 用 @ConditionalOnBean(View.class) 控制注册 (也是一种条件注册),因为无 View bean 时它是纯粹的浪费。
三种"条件注册"的手段(Boot 都会用):
@ConditionalOnXxx(首选,但对"值依赖"无能为力)@Bean方法返回 null(运行时才知道)- 注册一个空对象(最省事,但要求空对象无害)
15.3 第二十四定律:Boot 只注册"配置对象"= 功能内建在框架里
看到某个自动配置只注册配置类/约定类而没有任何功能组件(Filter、Controller、Handler)时, 说明该能力已经内建在 Framework 里,Boot 只负责"按约定配一个默认值"。
本章的证据:WebFluxObservationAutoConfiguration 只注册 DefaultServerRequestObservationConvention + MaximumAllowableTagsMeterFilter, 而 MVC 版还注册了 ServerHttpObservationFilter------ 因为 MVC 的观测需要外挂 Filter,WebFlux 的观测是内建的。
这条定律的用法 :评估"Boot 在这里到底做了多少事"。 如果看到一堆 @ConditionalOnMissingBean + 默认值对象, 那 Boot 的贡献就是"命名约定 + 默认值",真正的功能要去 Framework 里找。
反过来也成立:如果一个能力在 Framework 里没有、Boot 却提供了 , 那一定会有一个功能组件 的 @Bean(Filter、Controller、Listener...)。
17. 下一章预告
第 21 章进入 gRPC 新纪元 :spring-boot-grpc-server / spring-boot-grpc-client。 这是 Boot 4 全新加入的一等公民能力(3.x 完全没有),核心问题包括:
- gRPC 服务如何与
WebApplicationType共存(gRPC 是 HTTP/2,但不是 servlet 也不是 WebFlux?) spring-boot-grpc-server的自动配置结构(是否复用了第 18 章的WebServer抽象?)GrpcServerFactory/NettyGrpcServerFactory------这是NettyRouteProvider的真正用途吗?- 客户端侧的
GrpcClientFactory、channel 管理与spring.grpc.client.* spring-boot-grpc-test模块
第 18 章遗留的"NettyRouteProvider 的具体使用场景"与 第 20 章 9.5 节的"绕过 WebFlux 注册路由"猜测,应该会在那里得到答案。