专栏文章
专栏文章
SpringMVC 系列
1. SpringMVC 系列 #01:Spring MVC 原理

SpringMVC 系列 #01:Spring MVC 原理

发布于 2026-05-26 09:43 👁 8 次阅读
#源码解析#java#spring#spring-mvc

本文以前端控制器模式为切入点,逐层剖析 DispatcherServlet 的 9 大特殊 Bean、doDispatch 核心流程、视图解析链与拦截器机制,结合 Spring Boot 自动配置和常见坑点,给出生产实践建议。


目录

章节 说明
整体架构:前端控制器模式 DispatcherServlet 的定位与设计思想
9 大特殊 Bean DispatcherServlet 识别的核心扩展点
请求处理完整流程 doDispatch 源码级分析
HandlerMapping 详解 RequestMappingHandlerMapping vs RouterFunctionMapping
HandlerAdapter 详解 适配不同 Handler 类型的机制
视图解析流程 ViewResolver 链的工作原理
拦截器机制 HandlerInterceptor 三个回调的执行顺序
与 Spring Boot 的关系 DispatcherServletAutoConfiguration 原理
常见坑 字符编码、静态资源、异常处理等

整体架构:前端控制器模式

Front Controller Pattern(前端控制器模式)是 GoF 之外的企业应用模式,由 Martin Fowler 在《Patterns of Enterprise Application Architecture》中定义:

用一个统一的入口点处理所有请求,集中处理安全、路由、视图渲染等横切关注点,再将控制权委托给具体的处理器。

Spring MVC 的 DispatcherServlet 正是该模式的实现:

HTTP 请求
   │
   ▼
DispatcherServlet(前端控制器)
   │  统一入口,协调所有组件
   ├─► HandlerMapping     → 找到 Handler(Controller 方法)
   ├─► HandlerAdapter     → 调用 Handler,返回 ModelAndView
   ├─► ViewResolver       → 逻辑视图名 → 具体 View 对象
   └─► View.render()      → 生成响应

DispatcherServlet 继承链:

HttpServlet
  └── HttpServletBean          (Spring 托管的 Servlet 基类)
        └── FrameworkServlet   (持有 WebApplicationContext)
              └── DispatcherServlet  (核心前端控制器)

FrameworkServlet 覆写了 doGet / doPost 等方法,统一转发给 processRequest(),最终调用 DispatcherServlet.doService()doDispatch()


9 大特殊 Bean

DispatcherServlet 在初始化时(initStrategies())从 WebApplicationContext 中查找以下 9 种 Bean,若找不到则加载 DispatcherServlet.properties 中配置的默认实现:

Bean 类型 默认实现 职责
HandlerMapping RequestMappingHandlerMapping
BeanNameUrlHandlerMapping
将请求 URL 映射到 Handler(含拦截器链)
HandlerAdapter RequestMappingHandlerAdapter
HttpRequestHandlerAdapter
SimpleControllerHandlerAdapter
适配并调用具体 Handler
HandlerExceptionResolver ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
DefaultHandlerExceptionResolver
统一处理 Handler 执行期间抛出的异常
ViewResolver InternalResourceViewResolver 逻辑视图名 → View 对象
LocaleResolver AcceptHeaderLocaleResolver 解析客户端 Locale,支持国际化
ThemeResolver FixedThemeResolver 解析主题(现代项目基本不用)
MultipartResolver 无默认(需手动配置) 解析文件上传请求(multipart/form-data)
FlashMapManager SessionFlashMapManager 管理 PRG(Post/Redirect/Get)模式中的 Flash 属性
RequestToViewNameTranslator DefaultRequestToViewNameTranslator 当 Handler 未返回视图名时,从 URL 自动推导视图名

关键细节MultipartResolver 没有默认实现,若不配置则 DispatcherServlet 不会解析 multipart 请求,文件上传会失败。Spring Boot 通过 MultipartAutoConfiguration 自动注册 StandardServletMultipartResolver


请求处理完整流程

doDispatch 源码分析

DispatcherServlet#doDispatch 是请求处理的核心方法,简化后的源码逻辑如下:

// org.springframework.web.servlet.DispatcherServlet#doDispatch
protected void doDispatch(HttpServletRequest request, HttpServletResponse response)
        throws Exception {

    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            // 1. 检查是否是 multipart 请求,若是则封装为 MultipartHttpServletRequest
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // 2. 遍历所有 HandlerMapping,找到第一个匹配的 HandlerExecutionChain
            //    HandlerExecutionChain = Handler + 拦截器列表
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                // 找不到 Handler → 返回 404 或 noHandlerFound()
                noHandlerFound(processedRequest, response);
                return;
            }

            // 3. 找到能处理该 Handler 的 HandlerAdapter
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // 4. 处理 Last-Modified(GET/HEAD 请求的缓存优化)
            String method = request.getMethod();
            boolean isGet = HttpMethod.GET.matches(method);
            if (isGet || HttpMethod.HEAD.matches(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return; // 304 Not Modified,直接返回
                }
            }

            // 5. 依次执行所有拦截器的 preHandle()
            //    若某个 preHandle() 返回 false,逆序执行已通过拦截器的 afterCompletion(),终止流程
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // 6. 调用 HandlerAdapter 执行实际的 Handler(Controller 方法)
            //    返回 ModelAndView;若 Handler 直接写响应(@ResponseBody),mv 可能为 null
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            // 7. 异步处理判断
            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            // 8. 若 mv 不为 null 且没有视图名,使用 RequestToViewNameTranslator 推导视图名
            applyDefaultViewName(processedRequest, mv);

            // 9. 逆序执行所有拦截器的 postHandle()
            mappedHandler.applyPostHandle(processedRequest, response, mv);

        } catch (Exception ex) {
            dispatchException = ex;
        } catch (Throwable err) {
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }

        // 10. 处理结果:渲染视图 或 处理异常(HandlerExceptionResolver)
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

    } catch (Exception ex) {
        // 11. 无论如何,逆序执行 afterCompletion()
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    } finally {
        // 12. 若是 multipart 请求,清理临时文件
        if (multipartRequestParsed) {
            cleanupMultipart(processedRequest);
        }
    }
}

processDispatchResult 中的视图渲染

// 简化逻辑
private void processDispatchResult(...) throws Exception {
    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        } else {
            // 交给 HandlerExceptionResolver 链处理
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
        }
    }

    if (mv != null && !mv.wasCleared()) {
        // 渲染视图
        render(mv, request, response);
    }

    // 无论成功还是异常,最终都执行 afterCompletion
    if (mappedHandler != null) {
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

流程时序图

sequenceDiagram
    participant C as 客户端
    participant DS as DispatcherServlet
    participant HM as HandlerMapping
    participant I as Interceptor
    participant HA as HandlerAdapter
    participant H as Handler(Controller)
    participant VR as ViewResolver
    participant V as View

    C->>DS: HTTP 请求
    DS->>DS: checkMultipart()
    DS->>HM: getHandler(request)
    HM-->>DS: HandlerExecutionChain
    DS->>DS: getHandlerAdapter(handler)
    DS->>I: preHandle() × N(顺序)
    I-->>DS: true/false
    DS->>HA: handle(request, response, handler)
    HA->>H: 调用方法(参数解析、返回值处理)
    H-->>HA: 返回值
    HA-->>DS: ModelAndView(或 null)
    DS->>I: postHandle() × N(逆序)
    DS->>VR: resolveViewName(viewName, locale)
    VR-->>DS: View 对象
    DS->>V: render(model, request, response)
    V-->>C: HTTP 响应
    DS->>I: afterCompletion() × N(逆序)

HandlerMapping 详解

HandlerMapping 接口只有一个核心方法:

public interface HandlerMapping {
    @Nullable
    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

getHandler() 返回的 HandlerExecutionChain 包含:

RequestMappingHandlerMapping

最常用的实现,处理 @RequestMapping(及其派生注解 @GetMapping@PostMapping 等)。

初始化阶段afterPropertiesSet()):

  1. 扫描所有 Bean,找到标注 @Controller@RequestMapping 的类
  2. 提取每个方法上的 @RequestMapping 元信息,封装为 RequestMappingInfo
  3. RequestMappingInfo → HandlerMethod 的形式注册到内部 Map

匹配阶段

// 注册示例:@GetMapping("/user/{id}")
// 内部存储:RequestMappingInfo{"/user/{id}", GET} → HandlerMethod{UserController#getUser}

RouterFunctionMapping

函数式端点的 HandlerMapping,配合 Spring 5.2+ 引入的函数式 Web 框架使用:

@Bean
public RouterFunction<ServerResponse> routes(UserHandler handler) {
    return RouterFunctions.route()
        .GET("/user/{id}", handler::getUser)
        .POST("/user", handler::createUser)
        .build();
}

RouterFunctionMappingApplicationContext 收集所有 RouterFunction<?> Bean,合并为一条路由链,按顺序匹配。

对比

维度 RequestMappingHandlerMapping RouterFunctionMapping
编程风格 注解驱动(声明式) 函数式(命令式)
路由定义 @GetMapping 等注解 RouterFunctions.route()
Handler 类型 HandlerMethod HandlerFunction<ServerResponse>
适配器 RequestMappingHandlerAdapter HandlerFunctionAdapter
适合场景 传统 MVC、CRUD 轻量路由、函数式风格

HandlerAdapter 详解

HandlerAdapter 解决的核心问题:DispatcherServlet 不直接调用 Handler,因为 Handler 可以是任意类型(HandlerMethodHttpRequestHandlerController 接口实现等),需要一个适配层。

public interface HandlerAdapter {
    // 判断是否能处理该 Handler
    boolean supports(Object handler);

    // 调用 Handler,返回 ModelAndView(@ResponseBody 场景返回 null)
    @Nullable
    ModelAndView handle(HttpServletRequest request,
                        HttpServletResponse response,
                        Object handler) throws Exception;
}

三种主要实现

实现类 支持的 Handler 类型 典型场景
RequestMappingHandlerAdapter HandlerMethod@RequestMapping 方法) 现代 Spring MVC 控制器
HttpRequestHandlerAdapter HttpRequestHandler 接口实现 静态资源处理(ResourceHttpRequestHandler
SimpleControllerHandlerAdapter Controller 接口实现 遗留 Spring 2.x 风格控制器

RequestMappingHandlerAdapter 内部流程

// 简化的 handle() 逻辑
ModelAndView handle(...) {
    // 1. 参数解析:HandlerMethodArgumentResolver 链
    //    解析 @RequestParam、@PathVariable、@RequestBody、@ModelAttribute 等
    Object[] args = resolveHandlerArguments(request, response, handlerMethod);

    // 2. 调用目标方法(反射)
    Object returnValue = handlerMethod.invokeForRequest(request, mavContainer, args);

    // 3. 返回值处理:HandlerMethodReturnValueHandler 链
    //    @ResponseBody → 直接用 HttpMessageConverter 写响应,不产生视图
    //    String → 视图名
    //    ModelAndView → 直接使用
    handleReturnValue(returnValue, returnType, mavContainer, request, response);
}

视图解析流程

ViewResolver 链

DispatcherServlet 维护一个 List<ViewResolver>,按 Order 排序,依次调用每个 ViewResolver.resolveViewName(),返回第一个非 null 的 View 对象:

// DispatcherServlet#resolveViewName
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
        Locale locale, HttpServletRequest request) throws Exception {
    for (ViewResolver viewResolver : this.viewResolvers) {
        View view = viewResolver.resolveViewName(viewName, locale);
        if (view != null) {
            return view;  // 取第一个非 null 结果
        }
    }
    return null;
}

常用 ViewResolver 对比

ViewResolver 适用场景 关键配置
InternalResourceViewResolver JSP 模板 prefix=/WEB-INF/views/suffix=.jsp
ThymeleafViewResolver Thymeleaf 模板 配合 SpringTemplateEngine
FreeMarkerViewResolver FreeMarker 模板 配合 FreeMarkerConfigurer
ContentNegotiatingViewResolver 根据 Accept 头选择视图格式 包装其他 ViewResolver,优先级最高
BeanNameViewResolver 按 Bean 名称查找 View Bean 用于自定义 View 实现

ContentNegotiatingViewResolver 的特殊性

它本身不解析视图,而是委托给其他 ViewResolver,根据客户端的 Accept 请求头选择最合适的视图格式:

Accept: application/json  →  MappingJackson2JsonView
Accept: text/html         →  InternalResourceViewResolver → JSP
Accept: application/xml   →  MarshallingView

拦截器机制

HandlerInterceptor 接口

public interface HandlerInterceptor {

    // 在 HandlerAdapter 调用 Handler 之前执行
    // 返回 false 时,终止后续流程(不调用 Handler,不调用 postHandle)
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response,
            Object handler) throws Exception {
        return true;
    }

    // 在 HandlerAdapter 调用 Handler 之后、视图渲染之前执行
    // 可以修改 ModelAndView;若 Handler 抛出异常则不执行
    default void postHandle(HttpServletRequest request, HttpServletResponse response,
            Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    // 整个请求完成后执行(视图渲染完毕后)
    // 无论是否发生异常都会执行(前提:对应的 preHandle 返回过 true)
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response,
            Object handler, @Nullable Exception ex) throws Exception {
    }
}

执行顺序

假设注册了 3 个拦截器 I1、I2、I3(按注册顺序):

请求进来:
  I1.preHandle()   → true
  I2.preHandle()   → true
  I3.preHandle()   → true
  Handler 执行
  I3.postHandle()  ← 逆序
  I2.postHandle()
  I1.postHandle()
  视图渲染
  I3.afterCompletion()  ← 逆序
  I2.afterCompletion()
  I1.afterCompletion()

关键规则:若 I2.preHandle() 返回 false,则只执行 I1.afterCompletion()(已通过的拦截器逆序执行 afterCompletion),I2 和 I3 的 afterCompletion 不执行。

注册拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 全局拦截器
        registry.addInterceptor(new AuthInterceptor())
                .order(1);

        // 指定路径的拦截器
        registry.addInterceptor(new LogInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/public/**")
                .order(2);
    }
}

AsyncHandlerInterceptor

继承自 HandlerInterceptor,增加 afterConcurrentHandlingStarted() 回调,用于异步请求场景(Callable/DeferredResult)。异步请求中,postHandleafterCompletion 不在原始线程执行,需要用 afterConcurrentHandlingStarted 做资源清理。

Filter vs HandlerInterceptor vs AOP MethodInterceptor

Spring MVC 为何不用 Servlet Filter 或 Spring AOP 拦截器,而要自己实现一套 HandlerInterceptor

能力 Servlet Filter HandlerInterceptor AOP MethodInterceptor
工作层次 Servlet 容器(DispatcherServlet 外) DispatcherServlet 内部 Spring Bean 方法调用层
拿到 HttpRequest/Response ✅ 直接传参 ❌ 需绕路 RequestContextHolder
知道由哪个 Controller 方法处理 HandlerMethod
直接终止请求(返回 false) ✅(不调用 chain.doFilter) ✅(preHandle 返回 false) ❌ 需抛异常
修改 ModelAndView ✅ postHandle
视图渲染后执行(afterCompletion)
注入 Spring Bean ❌(需 DelegatingFilterProxy)
代理开销 无(DispatcherServlet 直调) CGLIB/JDK 动态代理
典型用途 跨域、编码、安全框架(Shiro/JWT) 认证鉴权、操作日志、耗时统计 事务、缓存、方法级审计

三者不是替代关系,是不同层次的切面:Filter 做通用 HTTP 处理,Interceptor 做 MVC 层控制,AOP 做业务逻辑横切。实际项目中通常混合使用。


与 Spring Boot 的关系

DispatcherServletAutoConfiguration

Spring Boot 通过 spring-boot-autoconfigure 模块自动完成 DispatcherServlet 的注册,核心类:

// org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfiguration(after = ServletWebServerFactoryAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
public class DispatcherServletAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @Conditional(DefaultDispatcherServletCondition.class)
    @ConditionalOnClass(ServletRegistration.class)
    @EnableConfigurationProperties(WebMvcProperties.class)
    protected static class DispatcherServletConfiguration {

        @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
            DispatcherServlet dispatcherServlet = new DispatcherServlet();
            // 从 spring.mvc.* 配置中读取参数
            dispatcherServlet.setDispatchOptionsRequest(
                    webMvcProperties.isDispatchOptionsRequest());
            dispatcherServlet.setDispatchTraceRequest(
                    webMvcProperties.isDispatchTraceRequest());
            dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
            dispatcherServlet.setEnableLoggingRequestDetails(
                    webMvcProperties.isLogRequestDetails());
            return dispatcherServlet;
        }
    }

    @Configuration(proxyBeanMethods = false)
    @Conditional(DispatcherServletRegistrationCondition.class)
    @ConditionalOnClass(ServletRegistration.class)
    @EnableConfigurationProperties(WebMvcProperties.class)
    @Import(DispatcherServletConfiguration.class)
    protected static class DispatcherServletRegistrationConfiguration {

        @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
        @ConditionalOnBean(value = DispatcherServlet.class,
                name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        public DispatcherServletRegistrationBean dispatcherServletRegistration(
                DispatcherServlet dispatcherServlet,
                WebMvcProperties webMvcProperties,
                ObjectProvider<MultipartConfigElement> multipartConfig) {
            // 注册到嵌入式 Servlet 容器,默认映射 "/"
            DispatcherServletRegistrationBean registration =
                    new DispatcherServletRegistrationBean(dispatcherServlet,
                            webMvcProperties.getServlet().getPath());
            registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
            registration.setLoadOnStartup(
                    webMvcProperties.getServlet().getLoadOnStartup());
            multipartConfig.ifAvailable(registration::setMultipartConfig);
            return registration;
        }
    }
}

Spring Boot 与传统 Spring MVC 对比

维度 传统 Spring MVC(war) Spring Boot
Servlet 容器 外置(Tomcat/Jetty/WildFly) 内嵌(默认 Tomcat)
DispatcherServlet 注册 web.xmlWebApplicationInitializer DispatcherServletAutoConfiguration 自动注册
配置方式 @EnableWebMvc + 手动配置 spring.mvc.* 属性 + WebMvcConfigurer
部署产物 .war 文件 可执行 .jar 文件
上下文 Root + Servlet 两级上下文 单一 ApplicationContext(默认)

注意:Spring Boot 中若使用 @EnableWebMvc,会完全接管 MVC 自动配置,WebMvcAutoConfiguration 的默认配置(静态资源处理、消息转换器等)将全部失效。扩展 MVC 配置应实现 WebMvcConfigurer 而非加 @EnableWebMvc


常见坑

坑 1:@RequestBody 字符编码问题

现象:POST 请求体中的中文乱码。

原因CharacterEncodingFilter 必须在所有其他 Filter 之前执行,且需要设置 forceRequestEncoding=true。若 request.getCharacterEncoding() 非空(某些容器默认设置了编码),则 CharacterEncodingFilter 默认不会覆盖。

// Spring Boot 中通过配置开启(默认已配置 UTF-8)
// application.properties
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.force=true
server.servlet.encoding.enabled=true
// 手动配置(传统 Spring MVC)
@Bean
public FilterRegistrationBean<CharacterEncodingFilter> characterEncodingFilter() {
    FilterRegistrationBean<CharacterEncodingFilter> bean = new FilterRegistrationBean<>();
    CharacterEncodingFilter filter = new CharacterEncodingFilter();
    filter.setEncoding("UTF-8");
    filter.setForceRequestEncoding(true);   // 关键:强制覆盖
    filter.setForceResponseEncoding(true);
    bean.setFilter(filter);
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 必须最先执行
    return bean;
}

@RequestBody 使用 HttpMessageConverter(如 MappingJackson2HttpMessageConverter)读取请求体,编码由 Content-Type: application/json;charset=UTF-8 头决定,通常不受 CharacterEncodingFilter 影响。真正受影响的是 @RequestParam / 表单参数。

坑 2:静态资源被 DispatcherServlet 拦截

现象/static/app.js 返回 404,或被 DispatcherServlet 处理后找不到 Handler。

原因DispatcherServlet 映射到 / 时,会拦截所有请求,包括静态资源。

解决方案

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 映射 /static/** 到 classpath:/static/
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/")
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
    }
}

Spring Boot 的 WebMvcAutoConfiguration 默认已配置:/webjars/**/**classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public/

坑 3:拦截器中使用 @Transactional 无效

原因HandlerInterceptorpreHandleRequestMappingHandlerAdapter 调用 Handler 之前执行,此时事务上下文尚未建立(事务由 AOP 在 Controller 方法调用时开启)。在拦截器中操作数据库应使用 TransactionTemplate 编程式事务。

坑 4:postHandle 在 @ResponseBody 场景不渲染视图

原因@ResponseBody 方法返回值由 HttpMessageConverter 直接写入响应流,ModelAndView 为 null,视图渲染步骤被跳过,postHandle 仍会执行但 modelAndView 参数为 null,无法修改视图。

坑 5:多个 ViewResolver 顺序问题

现象:配置了 Thymeleaf 和 JSP,但总是走 JSP 解析。

原因InternalResourceViewResolverOrder 值较小(优先级高),或未设置 Order(默认 Integer.MAX_VALUE,优先级最低,但 InternalResourceViewResolver 在没有找到模板时也会返回 View 对象而非 null,从而"吞掉"后续解析机会)。

解决:始终给 InternalResourceViewResolver 设置最低优先级(最大 Order 值):

@Bean
public InternalResourceViewResolver jspViewResolver() {
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    resolver.setOrder(Ordered.LOWEST_PRECEDENCE);  // 最后才检查 JSP
    return resolver;
}

坑 6:@ExceptionHandler 的作用域

@ExceptionHandler 方法在 @Controller 类中只对该类的 Handler 异常生效。要全局处理,需使用 @ControllerAdvice

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    @ResponseBody
    public ApiResponse<Void> handleBusinessException(BusinessException ex) {
        return ApiResponse.error(ex.getCode(), ex.getMessage());
    }
}

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论