Skip to content

WebMvcConfigurer

WebMvcConfigurer 是 Spring MVC 提供的一个配置接口,用于自定义 Spring MVC 的配置。它定义了回调方法来自定义通过 @EnableWebMvc 启用的基于 Java 的 Spring MVC 配置。

NOTE

WebMvcConfigurer 是一个接口,所有方法都提供了默认实现(空实现),因此你只需要重写需要自定义的方法即可。

概述

WebMvcConfigurer 接口允许开发者在 Spring Boot 应用中自定义 Web MVC 配置,而无需完全接管 Spring MVC 的自动配置。

1. 路径匹配配置 - configurePathMatch

配置 HandlerMapping 路径匹配选项,如是否使用解析的 PathPatterns 或字符串模式匹配。

kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configurePathMatch(configurer: PathMatchConfigurer) {
        configurer
            .setUseTrailingSlashMatch(false) // 不匹配尾部斜杠
            .setUseSuffixPatternMatch(false) // 不使用后缀模式匹配
            .addPathPrefix("/api") { controller ->
                controller.packageName.startsWith("com.example.api")
            }
    }
}

TIP

路径匹配配置主要用于统一 API 路径规范,比如为所有 API 添加统一前缀。

2. 拦截器配置 - addInterceptors

添加 Spring MVC 生命周期拦截器,用于控制器方法调用和资源处理器请求的前后处理。

kotlin
@Component
class AuthInterceptor : HandlerInterceptor {
    override fun preHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any
    ): Boolean {
        val token = request.getHeader("Authorization")
        if (token.isNullOrBlank()) {
            response.status = HttpStatus.UNAUTHORIZED.value()
            return false
        }
        return true
    }
}

@Configuration
class WebConfig : WebMvcConfigurer {

    @Autowired
    private lateinit var authInterceptor: AuthInterceptor

    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(authInterceptor)
            .addPathPatterns("/api/**") // 拦截所有 /api/ 开头的请求
            .excludePathPatterns("/api/login", "/api/register") // 排除登录和注册
    }
}

IMPORTANT

拦截器的执行顺序很重要,先注册的拦截器先执行。可以通过 order() 方法设置执行顺序。

3. 静态资源处理 - addResourceHandlers

配置静态资源处理器,用于提供图片、JavaScript、CSS 等静态文件服务。

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        // 配置文件上传目录
        registry.addResourceHandler("/uploads/**")
            .addResourceLocations("file:./uploads/")
            .setCachePeriod(3600) // 缓存1小时

        // 配置静态资源
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
            .setCachePeriod(86400) // 缓存24小时
            .resourceChain(true)
            .addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
    }
}
配置静态资源处理器

实际业务场景在电商网站中,商品图片通常存储在服务器的特定目录下,通过配置静态资源处理器可以直接通过 URL 访问这些图片,无需编写专门的文件下载接口。

4. CORS 跨域配置 - addCorsMappings

配置全局跨域请求处理,支持前后端分离架构。

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://localhost:3000", "https://example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600) // 预检请求缓存时间
    }
}

WARNING

在生产环境中,不要使用 allowedOrigins("*")allowCredentials(true) 的组合,这会带来安全风险。

5. 视图控制器 - addViewControllers

配置简单的自动化控制器,无需编写控制器逻辑。

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun addViewControllers(registry: ViewControllerRegistry) {
        // 首页重定向
        registry.addViewController("/").setViewName("redirect:/index")

        // 直接返回视图
        registry.addViewController("/about").setViewName("about")

        // 返回状态码
        registry.addViewController("/404").setStatusCode(HttpStatus.NOT_FOUND)

        // 重定向
        registry.addRedirectViewController("/old-page", "/new-page")
    }
}

6. 消息转换器配置

configureMessageConverters - 配置消息转换器

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
        // 添加自定义 JSON 转换器
        val jackson2Converter = MappingJackson2HttpMessageConverter()
        val objectMapper = ObjectMapper()
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
        jackson2Converter.objectMapper = objectMapper

        converters.add(jackson2Converter)
    }
}

extendMessageConverters - 扩展消息转换器

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun extendMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
        // 自定义 String 转换器,支持更多字符编码
        val stringConverter = StringHttpMessageConverter(StandardCharsets.UTF_8)
        stringConverter.setWriteAcceptCharset(false)

        // 插入到列表开头,优先级更高
        converters.add(0, stringConverter)
    }
}

TIP

configureMessageConverters 会覆盖默认转换器,而 extendMessageConverters 是在默认转换器基础上进行扩展,推荐使用后者。

7. 异常解析器配置

configureHandlerExceptionResolvers - 配置异常解析器

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun configureHandlerExceptionResolvers(resolvers: MutableList<HandlerExceptionResolver>) {
        resolvers.add(CustomExceptionResolver())
    }
}

class CustomExceptionResolver : HandlerExceptionResolver {
    override fun resolveException(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any?,
        ex: Exception
    ): ModelAndView? {
        when (ex) {
            is IllegalArgumentException -> {
                response.status = HttpStatus.BAD_REQUEST.value()
                response.writer.write("""{"error": "参数错误", "message": "${ex.message}"}""")
                response.contentType = "application/json;charset=UTF-8"
                return ModelAndView()
            }
            else -> return null
        }
    }
}

extendHandlerExceptionResolvers - 扩展异常解析器

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun extendHandlerExceptionResolvers(resolvers: MutableList<HandlerExceptionResolver>) {
        // 添加自定义异常解析器到默认解析器列表中
        resolvers.add(0, GlobalExceptionResolver())
    }
}

8. 参数解析器和返回值处理器

addArgumentResolvers - 添加参数解析器

kotlin
// 自定义参数解析器,用于解析当前用户信息
class CurrentUserArgumentResolver : HandlerMethodArgumentResolver {

    override fun supportsParameter(parameter: MethodParameter): Boolean {
        return parameter.hasParameterAnnotation(CurrentUser::class.java)
    }

    override fun resolveArgument(
        parameter: MethodParameter,
        mavContainer: ModelAndViewContainer?,
        webRequest: NativeWebRequest,
        binderFactory: WebDataBinderFactory?
    ): Any? {
        val token = webRequest.getHeader("Authorization")
        // 根据 token 解析用户信息
        return getUserFromToken(token)
    }

    private fun getUserFromToken(token: String?): User? {
        // 实际业务逻辑
        return User(id = 1, name = "张三")
    }
}

@Configuration
class WebConfig : WebMvcConfigurer {

    override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
        resolvers.add(CurrentUserArgumentResolver())
    }
}

// 使用示例
@RestController
class UserController {

    @GetMapping("/profile")
    fun getProfile(@CurrentUser user: User): ResponseEntity<User> {
        return ResponseEntity.ok(user)
    }
}

addReturnValueHandlers - 添加返回值处理器

kotlin
class ApiResponseReturnValueHandler : HandlerMethodReturnValueHandler {

    override fun supportsReturnType(returnType: MethodParameter): Boolean {
        return returnType.hasMethodAnnotation(ApiResponse::class.java)
    }

    override fun handleReturnValue(
        returnValue: Any?,
        returnType: MethodParameter,
        mavContainer: ModelAndViewContainer,
        webRequest: NativeWebRequest
    ) {
        mavContainer.isRequestHandled = true
        val response = webRequest.getNativeResponse(HttpServletResponse::class.java)

        val result = mapOf(
            "code" to 200,
            "message" to "success",
            "data" to returnValue
        )

        response?.apply {
            contentType = "application/json;charset=UTF-8"
            writer.write(ObjectMapper().writeValueAsString(result))
        }
    }
}

9. 格式化器配置 - addFormatters

添加自定义的转换器和格式化器,用于类型转换。

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        // 日期格式化器
        registry.addFormatter(object : Formatter<LocalDateTime> {
            override fun print(obj: LocalDateTime, locale: Locale): String {
                return obj.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
            }

            override fun parse(text: String, locale: Locale): LocalDateTime {
                return LocalDateTime.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
            }
        })

        // 字符串到枚举的转换器
        registry.addConverter(object : Converter<String, UserStatus> {
            override fun convert(source: String): UserStatus {
                return UserStatus.valueOf(source.uppercase())
            }
        })
    }
}

10. 异步支持配置 - configureAsyncSupport

配置异步请求处理选项。

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun configureAsyncSupport(configurer: AsyncSupportConfigurer) {
        configurer
            .setDefaultTimeout(30000) // 30秒超时
            .setTaskExecutor(asyncTaskExecutor()) // 自定义线程池
            .registerCallableInterceptors(AsyncRequestInterceptor()) // 注册拦截器
    }

    @Bean
    fun asyncTaskExecutor(): TaskExecutor {
        val executor = ThreadPoolTaskExecutor()
        executor.corePoolSize = 10
        executor.maxPoolSize = 50
        executor.queueCapacity = 100
        executor.threadNamePrefix = "async-"
        executor.initialize()
        return executor
    }
}

class AsyncRequestInterceptor : CallableProcessingInterceptor {
    override fun beforeConcurrentHandling(
        request: NativeWebRequest,
        task: Callable<*>
    ) {
        // 异步处理前的逻辑
    }

    override fun afterCompletion(
        request: NativeWebRequest,
        task: Callable<*>
    ) {
        // 异步处理完成后的逻辑
    }
}

完整配置示例

以下是一个完整的 WebMvcConfigurer 配置示例,展示了在实际项目中的综合应用:

kotlin
@Configuration
@EnableWebMvc
class WebMvcConfig : WebMvcConfigurer {

    @Autowired
    private lateinit var authInterceptor: AuthInterceptor

    @Autowired
    private lateinit var logInterceptor: LogInterceptor

    override fun configurePathMatch(configurer: PathMatchConfigurer) {
        configurer
            .setUseTrailingSlashMatch(false)
            .addPathPrefix("/api/v1") { controller ->
                controller.packageName.startsWith("com.example.api.v1")
            }
    }

    override fun addInterceptors(registry: InterceptorRegistry) {
        // 日志拦截器
        registry.addInterceptor(logInterceptor)
            .addPathPatterns("/**")
            .order(1)

        // 认证拦截器
        registry.addInterceptor(authInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns("/api/auth/**", "/api/public/**")
            .order(2)
    }

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        // 上传文件访问
        registry.addResourceHandler("/uploads/**")
            .addResourceLocations("file:./uploads/")
            .setCachePeriod(3600)

        // 静态资源
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
            .setCachePeriod(86400)
    }

    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://localhost:3000")
            .allowedMethods("*")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600)
    }

    override fun addViewControllers(registry: ViewControllerRegistry) {
        registry.addViewController("/").setViewName("redirect:/index.html")
        registry.addViewController("/admin").setViewName("admin/index")
    }

    override fun extendMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
        // 自定义 JSON 转换器
        val jackson2Converter = MappingJackson2HttpMessageConverter()
        val objectMapper = ObjectMapper().apply {
            configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
            registerModule(JavaTimeModule())
        }
        jackson2Converter.objectMapper = objectMapper
        converters.add(0, jackson2Converter)
    }

    override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
        resolvers.add(CurrentUserArgumentResolver())
        resolvers.add(PageableArgumentResolver())
    }

    override fun extendHandlerExceptionResolvers(resolvers: MutableList<HandlerExceptionResolver>) {
        resolvers.add(0, GlobalExceptionResolver())
    }

    override fun configureAsyncSupport(configurer: AsyncSupportConfigurer) {
        configurer
            .setDefaultTimeout(30000)
            .setTaskExecutor(asyncTaskExecutor())
    }

    @Bean
    fun asyncTaskExecutor(): TaskExecutor {
        val executor = ThreadPoolTaskExecutor()
        executor.corePoolSize = 10
        executor.maxPoolSize = 50
        executor.queueCapacity = 100
        executor.threadNamePrefix = "mvc-async-"
        executor.initialize()
        return executor
    }
}
kotlin
@Component
class AuthInterceptor : HandlerInterceptor {

    override fun preHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any
    ): Boolean {
        val token = request.getHeader("Authorization")

        if (token.isNullOrBlank() || !token.startsWith("Bearer ")) {
            response.status = HttpStatus.UNAUTHORIZED.value()
            response.contentType = "application/json;charset=UTF-8"
            response.writer.write("""{"error": "未授权访问"}""")
            return false
        }

        // 验证 token 逻辑
        val user = validateToken(token.substring(7))
        if (user == null) {
            response.status = HttpStatus.UNAUTHORIZED.value()
            response.contentType = "application/json;charset=UTF-8"
            response.writer.write("""{"error": "Token 无效"}""")
            return false
        }

        // 将用户信息存储到请求中
        request.setAttribute("currentUser", user)
        return true
    }

    private fun validateToken(token: String): User? {
        // 实际的 token 验证逻辑
        return if (token == "valid-token") {
            User(id = 1, name = "张三")
        } else null
    }
}

@Component
class LogInterceptor : HandlerInterceptor {

    private val logger = LoggerFactory.getLogger(LogInterceptor::class.java)

    override fun preHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any
    ): Boolean {
        val startTime = System.currentTimeMillis()
        request.setAttribute("startTime", startTime)

        logger.info("请求开始: {} {}", request.method, request.requestURI)
        return true
    }

    override fun afterCompletion(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any,
        ex: Exception?
    ) {
        val startTime = request.getAttribute("startTime") as Long
        val endTime = System.currentTimeMillis()
        val duration = endTime - startTime

        logger.info(
            "请求完成: {} {} - 状态码: {} - 耗时: {}ms",
            request.method,
            request.requestURI,
            response.status,
            duration
        )

        if (ex != null) {
            logger.error("请求异常: ", ex)
        }
    }
}

最佳实践

1. 配置优先级

IMPORTANT

理解不同配置方法的优先级关系:

  • configure* 方法会替换默认配置
  • extend* 方法会在默认配置基础上扩展
  • add* 方法会添加到现有配置中

2. 性能考虑

kotlin
@Configuration
class PerformanceWebConfig : WebMvcConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
            .setCachePeriod(86400) // 设置缓存时间
            .resourceChain(true) // 启用资源链
            .addResolver(VersionResourceResolver().addContentVersionStrategy("/**")) // 版本控制
    }

    override fun configureAsyncSupport(configurer: AsyncSupportConfigurer) {
        configurer.setDefaultTimeout(30000) // 合理设置超时时间
    }
}

3. 安全配置

kotlin
@Configuration
class SecurityWebConfig : WebMvcConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://trusted-domain.com") // 明确指定域名
            .allowedMethods("GET", "POST", "PUT", "DELETE") // 限制 HTTP 方法
            .allowCredentials(true)
            .maxAge(3600)
    }
}

CAUTION

在生产环境中,务必:

  • 明确指定 CORS 允许的域名,避免使用 *
  • 限制允许的 HTTP 方法
  • 设置合理的缓存时间和超时时间
  • 对敏感接口进行适当的访问控制

总结

WebMvcConfigurer 是 Spring MVC 中非常重要的配置接口,它提供了灵活而强大的方式来自定义 Web 应用的行为。通过合理使用各种配置方法,可以构建出高性能、安全、易维护的 Web 应用。

TIP

记住这些要点

  1. 选择合适的方法:根据需求选择 configure*extend*add* 方法
  2. 注意执行顺序:拦截器、转换器的顺序很重要
  3. 性能优化:合理设置缓存、超时时间
  4. 安全考虑:谨慎配置 CORS、静态资源访问
  5. 代码复用:将通用配置抽取为独立的配置类

通过掌握 WebMvcConfigurer 的各种配置方法,你可以更好地控制 Spring MVC 的行为,构建出符合业务需求的高质量 Web 应用。