WebMvcConfigurer / CORS / MVC 配置家族
概述
Spring Boot 的 Web MVC 自动配置通过 WebMvcAutoConfiguration 和 EnableWebMvcConfiguration 提供了一系列 MVC 基础设施 Bean。WebMvcConfigurer 是用户自定义 MVC 配置的核心接口,提供 15 个默认方法覆盖拦截器、CORS、视图控制器、内容协商、消息转换器等各个方面。
本文将深入拆解 WebMvcConfigurer / CORS / MVC 配置家族的 10 个关键细节,涵盖 WebMvcConfigurer 接口设计、DelegatingWebMvcConfiguration 委托模式、自动配置 Bean、CORS 配置与校验、内容协商策略、路径匹配配置、视图控制器注册等核心内容。
本文基于 Spring Boot 3.2.5 + Spring Framework 6.1.6 源码分析。
DispatcherServlet 的注册流程可参考 DispatcherServlet 注册。
1. WebMvcConfigurer 的 15 个默认空方法
WebMvcConfigurer 是 Spring MVC 配置的核心接口,定义了 15 个 default 空方法(Java 8+ 特性),让用户按需覆盖特定的配置。
public interface WebMvcConfigurer {
// 1. 自定义路径匹配策略
default void configurePathMatch(PathMatchConfigurer configurer) {}
// 2. 自定义内容协商策略
default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {}
// 3. 异步请求配置
default void configureAsyncSupport(AsyncSupportConfigurer configurer) {}
// 4. 默认 Servlet 处理(将未匹配的请求转发到 Servlet 容器)
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {}
// 5. 自定义格式化/转换服务
default void addFormatters(FormatterRegistry registry) {}
// 6. 自定义拦截器
default void addInterceptors(InterceptorRegistry registry) {}
// 7. 静态资源处理
default void addResourceHandlers(ResourceHandlerRegistry registry) {}
// 8. CORS 映射
default void addCorsMappings(CorsRegistry registry) {}
// 9. 视图控制器(免 Controller 的页面跳转)
default void addViewControllers(ViewControllerRegistry registry) {}
// 10. Bean 名称视图解析器
default void configureViewResolvers(ViewResolverRegistry registry) {}
// 11. 参数解析器扩展
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {}
// 12. 返回值处理器扩展
default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {}
// 13. HTTP 消息转换器扩展
default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {}
// 14. HTTP 消息转换器扩展(允许追加到默认转换器之后)
default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {}
// 15. 异常解析器扩展
default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {}
// 16. 异常解析器扩展(允许追加)
default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {}
// 17. 获取验证器
default Validator getValidator() { return null; }
// 18. 获取 MessageCodesResolver
default MessageCodesResolver getMessageCodesResolver() { return null; }
}典型自定义示例:
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/register", "/css/**", "/js/**");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://example.com")
.allowedMethods("GET", "POST")
.allowCredentials(true);
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
registry.addViewController("/login").setViewName("login");
}
}2. DelegatingWebMvcConfiguration 的委托模式
DelegatingWebMvcConfiguration 是 WebMvcConfigurationSupport 的子类,通过委托模式将容器中所有 WebMvcConfigurer Bean 的配置聚合起来。
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
// 注入容器中所有 WebMvcConfigurer Bean
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers = new ArrayList<>(configurers);
// 按 @Order 排序
this.configurers.sort(AnnotationAwareOrderComparator.INSTANCE);
}
}
// 委托:路径匹配配置
@Override
protected void configurePathMatch(PathMatchConfigurer configurer) {
for (WebMvcConfigurer configurer : this.configurers) {
configurer.configurePathMatch(configurer);
}
}
// 委托:内容协商配置
@Override
protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
for (WebMvcConfigurer configurer : this.configurers) {
configurer.configureContentNegotiation(configurer);
}
}
// 委托:CORS 映射
@Override
protected void addCorsMappings(CorsRegistry registry) {
for (WebMvcConfigurer configurer : this.configurers) {
configurer.addCorsMappings(registry);
}
}
// 委托:拦截器注册
@Override
protected void addInterceptors(InterceptorRegistry registry) {
for (WebMvcConfigurer configurer : this.configurers) {
configurer.addInterceptors(registry);
}
}
// ... 所有 configureXxx / addXxx 方法都采用相同的委托模式
}委托模式示意:
多个 WebMvcConfigurer 实现(按 @Order 排序):
├── MyWebMvcConfig (order=1) ← 先执行
├── AnotherMvcConfig (order=2) ← 后执行
└── ...
↓ @Autowired 注入
DelegatingWebMvcConfiguration
↓ 继承
WebMvcConfigurationSupport
↓ 提供 @Bean 定义
EnableWebMvcConfiguration(Spring Boot 扩展)
↓ 提供更多 @Bean配置聚合的过程:
addInterceptors() 被调用
↓
DelegatingWebMvcConfiguration.addInterceptors(registry)
↓
遍历所有 WebMvcConfigurer
↓
configurer1.addInterceptors(registry) ← 添加拦截器 A
configurer2.addInterceptors(registry) ← 添加拦截器 B
↓
InterceptorRegistry 中累积了所有拦截器
↓
最终注入到 RequestMappingHandlerMapping 的 HandlerExecutionChain3. WebMvcAutoConfiguration.EnableWebMvcConfiguration 的 8 个 @Bean
EnableWebMvcConfiguration 是 Spring Boot 自动配置的核心内部类,提供了 8 个关键 Bean 定义。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebMvcProperties.class)
public static class EnableWebMvcConfiguration
extends DelegatingWebMvcConfiguration
implements ResourceLoaderAware {
// Bean 1: RequestMappingHandlerMapping — 请求映射处理器
@Bean
@Primary
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
// 创建 RequestMappingHandlerMapping,设置注册顺序
// 内部调用 getInterceptors() 获取所有拦截器
return super.requestMappingHandlerMapping(contentNegotiationManager,
conversionService, resourceUrlProvider);
}
// Bean 2: RequestMappingHandlerAdapter — 请求处理适配器
@Bean
@Override
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {
// 设置参数解析器、返回值处理器、消息转换器、绑定初始化器等
return super.requestMappingHandlerAdapter(contentNegotiationManager,
conversionService, validator);
}
// Bean 3: HandlerExceptionResolver 组合
@Bean
@Override
public HandlerExceptionResolver handlerExceptionResolver(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
// 创建 ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、
// DefaultHandlerExceptionResolver 三个异常解析器
return super.handlerExceptionResolver(contentNegotiationManager);
}
// Bean 4: ViewResolver 组合
@Bean
@Override
public ViewResolver mvcViewResolver(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
// 创建 ViewResolverComposite(包含 InternalResourceViewResolver、BeanNameViewResolver 等)
return super.mvcViewResolver(contentNegotiationManager);
}
// Bean 5: 欢迎页 HandlerMapping
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(
ApplicationContext applicationContext,
ResourceLoader resourceLoader,
@Value("${spring.web.resources.add-mappings:true}") boolean addMappings) {
// 将 index.html 映射到 "/"
return new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext),
applicationContext, getResourceHandlerMapping(),
this.mvcProperties.getStaticPathPattern());
}
// Bean 6: ContentNegotiationManager
@Bean
@Override
public ContentNegotiationManager mvcContentNegotiationManager() {
// 创建内容协商管理器(Accept 头 → 参数 → 后缀 → 默认)
return super.mvcContentNegotiationManager();
}
// Bean 7: FormattingConversionService
@Bean
@Override
public FormattingConversionService mvcConversionService() {
// 创建带格式化能力的类型转换服务
return super.mvcConversionService();
}
// Bean 8: Validator
@Bean
@Override
public Validator mvcValidator() {
// 获取验证器(Hibernate Validator 等)
return super.mvcValidator();
}
}Bean 的依赖关系:
RequestMappingHandlerMapping
├── ContentNegotitationManager ───────────┐
├── FormattingConversionService │
└── ResourceUrlProvider │
│
RequestMappingHandlerAdapter │
├── ContentNegotiationManager ←────────────┘
├── FormattingConversionService
└── Validator
│
HandlerExceptionResolver ←─────────────────────┘
└── ContentNegotiationManager
ViewResolver
└── ContentNegotiationManager
WelcomePageHandlerMapping(独立,无特殊依赖)4. CorsRegistration.allowedOrigins("*") 的 CORS 配置
CorsRegistry 和 CorsRegistration 提供了链式 API 配置 CORS。
public class CorsRegistry {
// 路径 → CorsRegistration 的映射
private final List<CorsRegistration> registrations = new ArrayList<>();
public CorsRegistration addMapping(String pathPattern) {
CorsRegistration registration = new CorsRegistration(pathPattern);
this.registrations.add(registration);
return registration;
}
// 构建所有 CorsConfiguration 并注册到 UrlBasedCorsConfigurationSource
public Map<String, CorsConfiguration> getCorsConfigurations() {
Map<String, CorsConfiguration> configs = new LinkedHashMap<>();
for (CorsRegistration registration : this.registrations) {
configs.put(registration.getPathPattern(), registration.getCorsConfiguration());
}
return configs;
}
}
public class CorsRegistration {
private final String pathPattern;
private final CorsConfiguration config = new CorsConfiguration();
public CorsRegistration(String pathPattern) {
this.pathPattern = pathPattern;
}
// 允许的来源(可以设置多个)
public CorsRegistration allowedOrigins(String... origins) {
this.config.setAllowedOrigins(Arrays.asList(origins));
return this;
}
// 允许的来源模式(支持通配符 *,Spring 5.3+ 推荐)
public CorsRegistration allowedOriginPatterns(String... patterns) {
this.config.setAllowedOriginPatterns(Arrays.asList(patterns));
return this;
}
// 允许的 HTTP 方法
public CorsRegistration allowedMethods(String... methods) {
this.config.setAllowedMethods(Arrays.asList(methods));
return this;
}
// 允许的请求头
public CorsRegistration allowedHeaders(String... headers) {
this.config.setAllowedHeaders(Arrays.asList(headers));
return this;
}
// 暴露给客户端的响应头
public CorsRegistration exposedHeaders(String... headers) {
this.config.setExposedHeaders(Arrays.asList(headers));
return this;
}
// 是否允许携带凭证(Cookie、Authorization 头等)
public CorsRegistration allowCredentials(boolean allowCredentials) {
this.config.setAllowCredentials(allowCredentials);
return this;
}
// 预检请求的缓存时间(秒)
public CorsRegistration maxAge(long maxAge) {
this.config.setMaxAge(maxAge);
return this;
}
public CorsConfiguration getCorsConfiguration() {
return this.config;
}
}allowedOrigins("*") vs allowedOriginPatterns("*"):
| 配置 | 示例 | 说明 |
|---|---|---|
allowedOrigins("*") | 允许所有来源 | 不允许与 allowCredentials(true) 同时使用 |
allowedOriginPatterns("*") | 允许所有来源 | 支持通配符模式,可与 allowCredentials(true) 同时使用 |
allowedOrigins("https://example.com") | 指定来源 | 精确匹配,可与 allowCredentials(true) 同时使用 |
完整示例:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("*") // 通配符模式(可与 credentials 共用)
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true) // 允许携带 Cookie
.exposedHeaders("X-Custom-Header")
.maxAge(3600); // 预检缓存 1 小时
}
}生成的 CorsConfiguration:
CorsConfiguration {
allowedOrigins = null, // allowedOriginPatterns 优先
allowedOriginPatterns = ["*"], // 通配符模式
allowedMethods = ["GET", "POST", "PUT", "DELETE"],
allowedHeaders = ["*"],
exposedHeaders = ["X-Custom-Header"],
allowCredentials = true,
maxAge = 3600L
}5. DefaultCorsProcessor.handleInternal() 的 6 步校验
DefaultCorsProcessor 是 CORS 请求的实际处理者,负责对跨域请求进行 6 步校验。
public class DefaultCorsProcessor implements CorsProcessor {
@Override
public boolean process(@Nullable CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
// 1. 检查是否是 CORS 请求(Origin 头)
if (!CorsUtils.isCorsRequest(request)) {
return true; // 非跨域请求 → 直接通过
}
// 2. 处理预检请求(OPTIONS)
if (CorsUtils.isPreFlightRequest(request)) {
return handleInternal(new SmartHttpServletRequest(request), response, config, true);
}
// 3. 处理实际跨域请求
return handleInternal(new SmartHttpServletRequest(request), response, config, false);
}
// 6 步校验核心
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
CorsConfiguration config, boolean isPreFlight) throws IOException {
// 步骤 1: checkOrigin — 验证 Origin 是否在允许列表中
if (config.getAllowedOrigins() != null) {
String requestOrigin = request.getHeaders().getOrigin();
if (!config.checkOrigin(requestOrigin)) {
// Origin 不匹配 → 返回 403
rejectRequest(response);
return false;
}
}
// 步骤 2: checkMethods — 验证 HTTP 方法(预检请求的 Access-Control-Request-Method)
if (isPreFlight) {
String requestMethod = request.getHeaders().getFirst("Access-Control-Request-Method");
if (requestMethod != null && !config.checkMethod(requestMethod)) {
rejectRequest(response);
return false;
}
}
// 步骤 3: checkHeaders — 验证请求头(预检请求的 Access-Control-Request-Headers)
if (isPreFlight) {
List<String> requestHeaders = request.getHeaders()
.getOrEmpty("Access-Control-Request-Headers");
for (String requestHeader : requestHeaders) {
if (!config.checkHeader(requestHeader)) {
rejectRequest(response);
return false;
}
}
}
// 步骤 4: checkCredentials — 处理凭证(Cookie/Authorization)
if (config.getAllowCredentials() != null && config.getAllowCredentials()) {
// allowedOrigins 为 * 时不能与 allowCredentials=true 一起使用
// (会抛出 IllegalArgumentException)
}
// 步骤 5: addCorsHeaders — 添加 CORS 响应头
// 设置 Access-Control-Allow-Origin / Allow-Methods / Allow-Headers / Allow-Credentials / Max-Age
// 和 Exposed-Headers
response.getHeaders().addAll(config.checkHeaders(request, isPreFlight));
// 步骤 6: 预检请求返回 200 OK(无实际响应体)
if (isPreFlight) {
response.setStatusCode(HttpStatus.OK);
return false; // 不继续传递(截断)
}
return true; // 实际请求继续执行
}
// 拒绝请求:返回 403 Forbidden
protected void rejectRequest(ServerHttpResponse response) throws IOException {
response.setStatusCode(HttpStatus.FORBIDDEN);
}
}6 步校验流程图:
CORS 请求到达
↓
是否是跨域请求(有 Origin 头)?
├── 否 → 直接放行
└── 是
↓
是否是预检请求(OPTIONS + Access-Control-Request-Method)?
├── 是 → 预检处理
│ ├── ① checkOrigin ← Origin 是否允许
│ ├── ② checkMethods ← Access-Control-Request-Method 是否允许
│ ├── ③ checkHeaders ← Access-Control-Request-Headers 是否允许
│ ├── ④ checkCredentials ← 凭证配置是否合法
│ └── ⑤ addCorsHeaders + ⑥ 返回 200 OK
│
└── 否 → 实际请求处理
├── ① checkOrigin
├── ④ checkCredentials
└── ⑤ addCorsHeaders + ⑥ 继续执行业务逻辑6. UrlBasedCorsConfigurationSource.getCorsConfiguration() 路径匹配
UrlBasedCorsConfigurationSource 是 CorsConfigurationSource 的基于路径的实现,负责根据请求 URL 查找匹配的 CORS 配置。
public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource {
// URL 路径 → CorsConfiguration 映射(使用 AntPathMatcher 规则)
private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
// 路径匹配器(默认 AntPathMatcher)
private PathMatcher pathMatcher = new AntPathMatcher();
// 注册 CORS 配置
public void registerCorsConfiguration(String path, CorsConfiguration config) {
this.corsConfigurations.put(path, config);
}
// 根据请求查找匹配的 CorsConfiguration
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
// 1. 获取请求的 URI 路径
String lookupPath = UrlPathHelper.defaultInstance.getLookupPathForRequest(request);
// 2. 遍历所有注册的路径模式
for (Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
String registeredPath = entry.getKey();
// 3. 使用 AntPathMatcher 匹配
if (this.pathMatcher.match(registeredPath, lookupPath)) {
// 4. 合并路径匹配参数(提取路径变量)
CorsConfiguration config = entry.getValue().combine(null);
// 如果有路径变量,添加到属性中
// ...
return config;
}
}
// 没有匹配的 → 返回 null
return null;
}
}路径匹配示例:
// 注册的 CORS 配置
corsConfigurations.put("/api/**", corsConfig1);
corsConfigurations.put("/admin/**", corsConfig2);
corsConfigurations.put("/public/**", corsConfig3);
// 请求路径匹配
/api/users → 匹配 /api/** → 返回 corsConfig1
/api/orders/123 → 匹配 /api/** → 返回 corsConfig1
/admin/settings → 匹配 /admin/** → 返回 corsConfig2
/public/data → 匹配 /public/** → 返回 corsConfig3
/health → 无匹配 → 返回 null(不处理 CORS)CorsFilter 中的使用:
// CorsFilter 使用 UrlBasedCorsConfigurationSource
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
CorsFilter filter = new CorsFilter(source);7. CorsFilter 的 doFilterInternal()
CorsFilter 是一个 OncePerRequestFilter,在 DispatcherServlet 之前拦截 OPTIONS 预检请求。
public class CorsFilter extends OncePerRequestFilter {
private final CorsConfigurationSource configSource;
public CorsFilter(CorsConfigurationSource configSource) {
this.configSource = configSource;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 获取匹配的 CORS 配置
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
// 2. 没有 CORS 配置 → 直接放行
if (corsConfiguration == null) {
filterChain.doFilter(request, response);
return;
}
// 3. 使用 DefaultCorsProcessor 处理
boolean isValid = this.processor.process(corsConfiguration, request, response);
// 4. 如果处理返回 false(预检请求或验证失败),不再继续传递
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
// 预检请求已经被 DefaultCorsProcessor 返回了 200 OK
// 实际请求 CORS 校验失败时,response 已经返回了 403
return;
}
// 5. 处理通过 → 继续过滤器链
filterChain.doFilter(request, response);
}
}过滤器链中的位置:
CorsFilter
↓ 拦截 OPTIONS 预检请求,处理 CORS 头
CharacterEncodingFilter
↓ 设置请求/响应编码
HiddenHttpMethodFilter
↓ 处理 _method 参数(REST 风格表单)
FormContentFilter
↓ 处理 PUT/DELETE 请求的表单内容
DispatcherServlet
↓
HandlerMapping → HandlerAdapter → ControllerCorsFilter vs HandlerMapping 内部的 CORS 处理:
| 特性 | CorsFilter | HandlerMapping CORS |
|---|---|---|
| 位置 | 在 DispatcherServlet 之前 | 在 DispatcherServlet 内部 |
| 覆盖范围 | 所有请求(含静态资源) | 仅映射到 HandlerMethod 的请求 |
| 配置方式 | FilterRegistrationBean | WebMvcConfigurer.addCorsMappings() |
| OPTIONS 预检 | 完全处理,不会到达 DispatcherServlet | 经 DispatcherServlet → HandlerMapping 处理 |
| 适用场景 | 全局 CORS 策略 | Controller 级别或路径特定的 CORS |
Spring Boot 自动注册 CorsFilter:
// Spring Boot 不会自动注册 CorsFilter
// 用户需要显式声明
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOriginPatterns("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}8. ContentNegotiationManager.resolveMediaTypes()
ContentNegotiationManager 负责根据客户端的请求确定响应的媒体类型。
public class ContentNegotiationManager
implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
private final List<ContentNegotiationStrategy> strategies;
private final Set<MediaTypeFileExtensionResolver> fileExtensionResolvers;
public ContentNegotiationManager(ContentNegotiationStrategy... strategies) {
this.strategies = Arrays.asList(strategies);
}
// 解析请求的媒体类型(按策略链依次尝试)
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this.strategies) {
// 每个策略尝试解析
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.isEmpty() || mediaTypes.contains(MediaType.ALL)) {
// 策略返回空或 */* → 尝试下一个策略
continue;
}
return mediaTypes;
}
// 所有策略都失败 → 返回默认 application/json
return Collections.singletonList(MediaType.APPLICATION_JSON);
}
}3 种内容协商策略:
// 策略 1: Accept 头协商(默认)
public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request) {
// 从请求头 Accept 获取
// Accept: application/json, text/html;q=0.9
// → [application/json, text/html]
String acceptHeader = request.getHeaderValue("Accept");
if (acceptHeader == null) {
return MEDIA_TYPE_ALL_LIST; // 返回 */*
}
return MediaType.parseMediaTypes(acceptHeader);
}
}
// 策略 2: URL 参数协商(配置启用)
// ?format=json → application/json
public class ParameterContentNegotiationStrategy implements ContentNegotiationStrategy {
private final Map<String, MediaType> mediaTypes = new HashMap<>();
public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
// 参数名 → MediaType 映射
// "json" → "application/json"
// "xml" → "application/xml"
this.mediaTypes.putAll(mediaTypes);
}
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request) {
String value = request.getParameter("format");
if (value == null) return MEDIA_TYPE_ALL_LIST;
MediaType mediaType = this.mediaTypes.get(value);
return (mediaType != null) ? Collections.singletonList(mediaType) : MEDIA_TYPE_ALL_LIST;
}
}
// 策略 3: 路径扩展名协商(配置启用)
// /api/users.json → application/json
public class PathExtensionContentNegotiationStrategy implements ContentNegotiationStrategy {
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request) {
String path = request.getNativeRequest(HttpServletRequest.class).getRequestURI();
String extension = StringUtils.getFilenameExtension(path);
if (extension == null) return MEDIA_TYPE_ALL_LIST;
MediaType mediaType = getMediaTypeForExtension(extension);
return (mediaType != null) ? Collections.singletonList(mediaType) : MEDIA_TYPE_ALL_LIST;
}
}协商策略链:
// Spring Boot 默认配置的协商策略
@Bean
public ContentNegotiationManager mvcContentNegotiationManager() {
List<ContentNegotiationStrategy> strategies = new ArrayList<>();
// 策略 1: Accept 头
strategies.add(new HeaderContentNegotiationStrategy());
// 策略 2: URL 参数(如果配置了 spring.mvc.contentnegotiation.favor-parameter=true)
if (favorParameter) {
ParameterContentNegotiationStrategy strategy =
new ParameterContentNegotiationStrategy(mediaTypes);
strategies.add(strategy);
}
// 策略 3: 路径扩展(如果配置了 spring.mvc.contentnegotiation.favor-path-extension=true)
if (favorPathExtension) {
strategies.add(new PathExtensionContentNegotiationStrategy(mediaTypes));
}
return new ContentNegotiationManager(strategies);
}ContentNegotiationConfigurer 自定义:
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
.favorParameter(true) // 启用 ?format=json
.parameterName("format") // 参数名
.ignoreAcceptHeader(false) // 同时保留 Accept 头
.defaultContentType(MediaType.APPLICATION_JSON) // 默认响应类型
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
}
}9. PathMatchConfigurer 的 setUseTrailingSlashMatch(true)
PathMatchConfigurer 控制请求路径的匹配策略。
public class PathMatchConfigurer {
private Boolean trailingSlashMatch; // 是否启用尾部斜杠匹配
private Boolean caseSensitiveMatch; // 是否大小写敏感
private Map<String, Boolean> pathExtensionMatches; // 路径扩展名匹配
private Boolean mergedSlashes; // 是否合并连续斜杠
// 启用尾部斜杠匹配:/users 匹配 /users/
public PathMatchConfigurer setUseTrailingSlashMatch(Boolean trailingSlashMatch) {
this.trailingSlashMatch = trailingSlashMatch;
return this;
}
// 启用大小写不敏感匹配
public PathMatchConfigurer setUseCaseSensitiveMatch(Boolean caseSensitiveMatch) {
this.caseSensitiveMatch = caseSensitiveMatch;
return this;
}
// 启用后缀模式匹配:/users.json 匹配 /users
public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean suffixPatternMatch) {
this.suffixPatternMatch = suffixPatternMatch;
return this;
}
}路径匹配选项的影响:
// 默认配置
trailingSlashMatch = false // 不启用:/users 不匹配 /users/
caseSensitiveMatch = true // 大小写敏感:/Users 不匹配 /users
mergedSlashes = false // 不合并://users 不匹配 /users
// 启用 trailingSlashMatch = true 后
@RequestMapping("/users")
// 以下路径都匹配:
GET /users → 匹配
GET /users/ → 匹配(尾部斜杠)
GET /users// → 不匹配(不合并连续斜杠)
// 启用 caseSensitiveMatch = false 后
GET /Users → 匹配
GET /USERS → 匹配
// Spring Boot 默认配置(WebMvcProperties)
spring.mvc.pathmatch.matching-strategy=path-pattern-parser // 使用 PathPatternParser
// 旧版使用 AntPathMatcher(Spring Boot 2.x 默认)PathPatternParser 的匹配规则:
// Spring Boot 3.x 默认使用 PathPatternParser
// 相对于 AntPathMatcher 的差异:
// PathPatternParser(新)
/Users → 不匹配 /users(严格大小写敏感)
/api/** → 匹配 /api/v1/users
/** → 匹配所有
// AntPathMatcher(旧)
/Users → 匹配 /users(默认不区分大小写)
// 可配置 setCaseSensitive(false)10. ViewControllerRegistry.addViewController("/login").setViewName("login") 的注册原理
ViewControllerRegistry 允许在不编写 Controller 的情况下直接将 URL 映射到 View。
public class ViewControllerRegistry {
private final List<ViewControllerRegistration> registrations = new ArrayList<>();
// 注册视图控制器
public ViewControllerRegistration addViewController(String urlPath) {
ViewControllerRegistration registration = new ViewControllerRegistration(urlPath);
// 排序按 URL 路径长度逆序(更具体的路径优先)
registration.setOrder(this.registrations.size());
this.registrations.add(registration);
return registration;
}
// 构建为 HandlerMapping
public HandlerMapping buildHandlerMapping() {
if (this.registrations.isEmpty()) return null;
// 创建 SimpleUrlHandlerMapping
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
handlerMapping.setOrder(1); // 优先级高于 @RequestMapping
// 注册所有视图控制器
Map<String, Object> urlMap = new LinkedHashMap<>();
for (ViewControllerRegistration registration : this.registrations) {
// 创建 ParameterizableViewController(继承 AbstractController)
ParameterizableViewController controller = new ParameterizableViewController();
controller.setViewName(registration.getViewName());
controller.setStatusCode(registration.getStatusCode());
urlMap.put(registration.getUrlPath(), controller);
}
handlerMapping.setUrlMap(urlMap);
return handlerMapping;
}
}
// ParameterizableViewController — 免 Controller 的视图跳转
public class ParameterizableViewController extends AbstractController {
@Nullable
private String viewName; // 要渲染的视图名
@Override
public ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
// 直接返回 ModelAndView,没有业务逻辑
return new ModelAndView(this.viewName);
}
}注册原理:
// 用户配置
registry.addViewController("/login").setViewName("login");
// 等价于以下 Controller:
@Controller
public class LoginViewController {
@GetMapping("/login")
public String login() {
return "login"; // 转发到 login.html / login.jsp
}
}
// 内部实现等价于:
// SimpleUrlHandlerMapping
// urlMap = {"/login" → ParameterizableViewController{viewName="login"}}
// 当请求 /login 到达时:
// ① SimpleUrlHandlerMapping.getHandler("/login")
// ② → 找到 ParameterizableViewController
// ③ → ParameterizableViewController.handleRequest()
// ④ → return new ModelAndView("login")状态码设置:
registry.addViewController("/login")
.setViewName("login")
.setStatusCode(HttpStatus.OK); // 200 OK
registry.addRedirectViewController("/old-path", "/new-path")
.setStatusCode(HttpStatus.MOVED_PERMANENTLY); // 301 永久重定向
registry.addStatusController("/maintenance")
.setStatusCode(HttpStatus.SERVICE_UNAVAILABLE); // 503 维护页面支持的视图控制器类型:
| 类型 | 方法 | 说明 |
|---|---|---|
| 普通视图 | addViewController("/path").setViewName("view") | 直接渲染视图 |
| 重定向 | addRedirectViewController("/old", "/new") | RedirectView,默认 302 |
| 状态码 | addStatusController("/path", HttpStatus.SC) | 直接返回状态码 |
总结
WebMvcConfigurer / CORS / MVC 配置家族的 10 个细节点总结如下:
| # | 细节点 | 核心类/机制 |
|---|---|---|
| ① | WebMvcConfigurer 的 15 个默认空方法 | 覆盖拦截器、CORS、视图控制器、内容协商、消息转换器等所有 MVC 配置的 default 接口 |
| ② | DelegatingWebMvcConfiguration 的委托模式 | @Autowired WebMvcConfigurer 集合 → 排序 → 逐一委托调用 |
| ③ | EnableWebMvcConfiguration 的 8 个 @Bean | requestMappingHandlerMapping / requestMappingHandlerAdapter / welcomePageHandlerMapping / ContentNegotiationManager / FormattingConversionService / Validator / HandlerExceptionResolver / ViewResolver |
| ④ | CorsRegistration.allowedOrigins("*") 的 CORS 配置 | CorsConfiguration → allowedOrigins / allowedOriginPatterns / methods / headers / credentials / maxAge |
| ⑤ | DefaultCorsProcessor.handleInternal() 的 6 步校验 | checkOrigin → checkMethods → checkHeaders → checkCredentials → addCorsHeaders → 返回 200 OK |
| ⑥ | UrlBasedCorsConfigurationSource.getCorsConfiguration() 路径匹配 | AntPathMatcher.match("/api/**", requestPath) 查找对应配置 |
| ⑦ | CorsFilter 的 doFilterInternal() | 在 DispatcherServlet 前拦截 OPTIONS 预检请求,返回 200 OK,不继续传递 |
| ⑧ | ContentNegotiationManager.resolveMediaTypes() | Accept 头 → parameter(format=json)→ pathExtension(.json)→ 默认 application/json |
| ⑨ | PathMatchConfigurer 的 setUseTrailingSlashMatch(true) | /users 匹配 /users/ 的尾部斜杠策略 |
| ⑩ | ViewControllerRegistry.addViewController("/login").setViewName("login") | ParameterizableViewController → SimpleUrlHandlerMapping 注册 |