spring揭秘25-springmvc03-其他组件(文件上传+拦截器+处理器适配器+异常统一处理)

文章目录

【README】

本文总结自《spring揭秘》,作者王福强,非常棒的一本书,墙裂推荐;

代码详情参见: springmvcDiscoverFirstDemo【github】

1)springmvc其他组件如下:

  • MultipartResolver(多部件解析器): 在 HandlerMapping之前执行, 处理文件上传请求;
  • HandlerInterceptor(处理器拦截器): 对处理流程进行拦截;
  • HandlerAdapter(处理器适配器): 帮助我们使用其他类型的Handler;(而不仅仅只使用Controller这一种Handler)
  • HandlerExceptionResolver(处理器异常解析器): 处理器异常解析器; 提供处理器异常处理的标准框架;

2)web.xml (web应用部署描述符,servlet容器加载时读取的xml文件)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app
        xmlns = "https://jakarta.ee/xml/ns/jakartaee"
        xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation = "https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
        version = "5.0"
        metadata-complete = "false"
>
  <display-name>springmvcDiscover</display-name>

  <!-- 指定ContextLoaderListener加载web容器时使用的多个xml配置文件(默认使用/WEB-INF/applicationContext.xml) -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml,/WEB-INF/applicationContext-module1.xml</param-value>
  </context-param>

  <filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- 注册过滤器代理 -->
  <filter>
    <filter-name>customFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>customFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- 配置监听器ContextLoaderListener,其加载顶层WebApplicationContext web容器-->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <!-- 注册一级控制器 DispatcherServlet,用于拦截所有请求(匹配url-pattern) -->
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- DispatcherServlet启动读取xml配置文件加载组件,构建web容器(子),通过contextConfigLocation为其配置多个xml文件-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/dispatcher-servlet.xml,/WEB-INF/dispatcher-servlet2.xml,/WEB-INF/dispatcher-servlet3-upload.xml</param-value>
    </init-param>
    <load-on-startup>2</load-on-startup>
    <!-- 新增multipart-config 子元素,该servlet才启用文件上传功能(必须)  -->
    <multipart-config>
      <!-- 当上传文件被处理或文件超过fileSizeThreshold,文件的保存路径;默认为空串 -->
      <location>D:\temp\springmvcUploadDir</location>
      <!-- 上传文件字节最大值,若超过则抛出异常;默认无限;我们这里设置为20M -->
      <max-file-size>20971520</max-file-size>
      <!-- 请求报文字节最大值,若超过则抛出异常;默认无限;我们这里设置为1000M -->
      <max-request-size>1048576000</max-request-size>
      <!-- 临时保存到磁盘的文件大小最小值(超过该值就保存);默认0 -->
      <file-size-threshold>-1</file-size-threshold>
    </multipart-config>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>

</web-app>

3)配置文件目录结构:

  • springmvc顶级web容器配置文件: applicationContext.xml , applicationContext-module1.xml
  • DispatcherServlet次顶级web容器配置文件:dispatcher-servlet.xml, dispatcher-servlet2.xml, dispatcher-servlet3-upload.xml

【1】文件上传与MultipartResolver

1)RFC1867: 为html表单新增了一种MIME类型(multipart/formdata),表示表单的文件上传 ;

2)**MIME(Multipurpose Internet Mail Extensions)定义:**多用途互联网邮件扩展类型。媒体类型(也称为多用途互联网邮件扩展或 MIME 类型)表示文档、文件或字节组合的性质和格式。简单理解:MIME定义了文档,文件或字节组合的格式;MIME 类型在 IETF 的 RFC 6838 中定义并标准化, 参见 https://datatracker.ietf.org/doc/html/rfc6838

  • 常见的MIME类型(通用型):
    • 超文本标记语言文本 .html text/html
    • xml文档 .xml text/xml
    • XHTML文档 .xhtml application/xhtml+xml
    • 普通文本 .txt text/plain
    • RTF文本 .rtf application/rtf
    • PDF文档 .pdf application/pdf
    • Microsoft Word文件 .word application/msword
    • PNG图像 .png image/png
    • GIF图形 .gif image/gif
    • JPEG图形 .jpeg,.jpg image/jpeg
    • au声音文件 .au audio/basic
    • MIDI音乐文件 mid,.midi audio/midi,audio/x-midi
    • RealAudio音乐文件 .ra, .ram audio/x-pn-realaudio
    • MPEG文件 .mpg,.mpeg video/mpeg
    • AVI文件 .avi video/x-msvideo
    • GZIP文件 .gz application/x-gzip
    • TAR文件 .tar application/x-tar
    • 任意的二进制数据 application/octet-stream
  • MIME作用: 显然,MIME是定义文档,文件或字节组合格式的一种标准;
    • 有了标准,客户端(如浏览器)根据MIME某种标准格式封装请求报文;
    • 有了标准, 服务器根据MIME格式解析请求报文(字节流),并做处理;

2)声明文件上传的html表单元素:

html 复制代码
<!-- html文件上传表单元素 -->
<form method="post" action="busiFileUpload.do" enctype="multipart/form-data">
        <table>
            <tr>
                <td>选择上传文件: <input name="inputFile" type="file" /></td>
            </tr>
            <tr>
                <td><input type="submit" value="提交" /></td>
            </tr>
        </table>
    </form>

3)文件上传请求报文封装与解析:

  • 客户端浏览器根据RFC1867定义的格式或标准,对文件上传表单内容进行编码;而服务器根据RFC1867对请求报文解码,就可以获取表单提交的数据,包括上传的文件流;
  • 服务器端对multipart/form-data类型的报文解析,没必要自定义实现;可以复用已有的文件上传类库,如 CommonsFileUpload

4)springmvc提供了几种文件上传类库,通过MultipartResolver接口的抽象,我们可以自行选择使用哪种文件上传类库;


【1.1】使用MultipartResolver进行文件上传

1)web.xml 配置文件上传,参见 https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#a-basic-example (搜索multipart-config)

2)java的servlet规范能够处理multipart请求,并使得mime类型(多用途互联网邮件扩展)附件可用;但需要对servlet(springmvc中的DispatcherServlet)新增配置 ,使得该servlet启用处理Multipart请求功能,包括但不限于文件上传;


【1.2】springmvc处理multipart多部件请求流程

1)springmvc处理multipart多部件请求流程(multipart请求包括但不限于文件上传):

  • 浏览器提交Multipart请求到springmvc应用;
  • DispatcherServlet接收到请求后,从自身的spring web容器WebApplicationContext中找到名为multipartResolver的多组件解析器实例;(本文用的是StandardServletMultipartResolver)
  • 通过multipartResolver.isMultipart(request)判断该请求是否为 multipart 请求(请求报文的mime类型是否 multipart开头);
    • 若不是,则直接返回原始HttpServletRequest;
    • 若是,则通过multipartResolver.resolveMultipart(request) 把request封装为StandardMultipartHttpServletRequest , (HttpServletRequest子类) ;后续所有请求都使用 StandardMultipartHttpServletRequest 进行业务逻辑处理;
  • multipart请求处理完成后,DispatcherServlet会调用multipartResolver的cleanupMultipart()方法释放文件上传处理时的系统资源;

【1.3】使用springmvc上传文件代码实现(springmvc6.10版本):

【fileUpload.jsp】文件上传页面

jsp 复制代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"  import="java.util.List" import="java.util.ArrayList"  isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>文件上传列表</title>
</head>
<body>
    <form method="post" action="busiFileUpload.do" enctype="multipart/form-data">
        <table>
            <tr>
                <td>选择上传文件: <input name="inputFile" type="file" /></td>
            </tr>
            <tr>
                <td><input type="submit" value="提交" /></td>
            </tr>
        </table>
    </form>
</body>
</html>

【web.xml】

xml 复制代码
<!-- 注册一级控制器 DispatcherServlet,用于拦截所有请求(匹配url-pattern) -->
<servlet>
  <servlet-name>dispatcher</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <!-- DispatcherServlet启动读取xml配置文件加载组件,构建web容器(子),通过contextConfigLocation为其配置多个xml文件-->
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/dispatcher-servlet.xml,/WEB-INF/dispatcher-servlet2.xml,/WEB-INF/dispatcher-servlet3-upload.xml</param-value>
  </init-param>
  <load-on-startup>2</load-on-startup>
  <!-- 新增multipart-config 子元素,该servlet才启用处理Multipart请求,包括文件上传(必须)  -->
  <multipart-config>
    <!-- 当上传文件被处理或文件超过fileSizeThreshold,文件的保存路径;默认为空串 -->
    <location>D:\temp\springmvcUploadDir</location>
    <!-- 上传文件字节最大值,若超过则抛出异常;默认无限;我们这里设置为20M -->
    <max-file-size>20971520</max-file-size>
    <!-- 请求报文字节最大值,若超过则抛出异常;默认无限;我们这里设置为1000M -->
    <max-request-size>1048576000</max-request-size>
    <!-- 临时保存到磁盘的文件字节最小阈值;默认0 -->
    <file-size-threshold>0</file-size-threshold>
  </multipart-config>
</servlet>
<servlet-mapping>
  <servlet-name>dispatcher</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>

【文件临时保存】通过调试我们发现, 文件在处理过程中会被临时保存(因为保存的阈值为0,即所有文件都被临时暂存;当然可以调整为其他值); 如下;

【dispatcher-servlet3-upload.xml】DispatcherServlet的spring容器配置文件:注册Multipart解析器到spring容器(StandardServletMultipartResolver)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 注册多部件请求解析器 -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />

    <bean id="/busiFileUpload.do" class="com.tom.springmvc.controller.upload.BusiFileUploadController" />

    <bean id="/fileUploadPage.do" class="com.tom.springmvc.controller.upload.BusiFileUploadPageController"/>
</beans>

【BusiFileUploadController】文件上传控制器

java 复制代码
public class BusiFileUploadController extends AbstractController {

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (!(request instanceof MultipartHttpServletRequest multipartRequest)) {
            return new ModelAndView("error");
        }
        // 类型转换
        MultipartFile multipartFile = multipartRequest.getFile("inputFile");
        if (Objects.isNull(multipartFile)) {
            return new ModelAndView("error");
        }
        // 保存文件流到本地
        String fileName = multipartFile.getOriginalFilename();
        String path = request.getServletContext().getRealPath("/") + fileName;
        BusiIOUtils.saveToDiskFile(multipartFile, path);
        // 返回视图
        ModelAndView uploadSuccMv = new ModelAndView("fileUploadSucc");
        uploadSuccMv.addObject("fileName", multipartFile.getOriginalFilename());
        uploadSuccMv.addObject("path", path);
        return uploadSuccMv;
    }
}

【BusiIOUtils.java】

java 复制代码
/**
 * @description 保存到本地磁盘文件
 * @author admin
 */
public static void saveToDiskFile(MultipartFile multipartFile, String path) throws IOException {
    BufferedOutputStream targetBufferedOutputStream = new BufferedOutputStream(new FileOutputStream(path));
    BufferedInputStream bufferedInputStream = new BufferedInputStream(multipartFile.getInputStream());
    byte[] bufferArr = new byte[1024];
    while (bufferedInputStream.read(bufferArr, 0, bufferArr.length) != -1) {
        targetBufferedOutputStream.write(bufferArr);
    }
    targetBufferedOutputStream.flush();
    targetBufferedOutputStream.close();
    bufferedInputStream.close();
}

【上传效果】


【2】Handler与HandlerAdaptor(处理器与处理器适配器)

【2.1】概述

1)springmvc中:任何用于web请求处理的处理对象统称为Handler处理器; Controller是处理器的一种;

2)对于DispatcherServlet来说,有个问题:DispatcherServlet应该使用什么样的Handler, 又如何调用Handler的哪个方法来处理请求?

  • DispatcherServlet把Handler调用职责转交给HandlerAdapter(为什么DispatcherServlet调用HandlerAdapter,再由HandlerAdapter调用具体Handler,而不是DispatcherServlet直接调用Handler;因为Handler可以有多种,如servlet,controller;他们要适配DispatcherServlet的调用,就需要拥有相同的方法名;而因为历史原因,servlet先于controller被发明,即Handler间没有相同的方法名;即对于没有相同方法的Handler需要适配DispatcherServlet的调用(即便通过实现新增接口,可能对存量Handler有侵入性,强耦合),就需要使用适配器模式;这是适配器模式的又一应用; );
    • DispatcherServlet从 HandlerMapping获取Handler后,通过HandlerAdapter#supports(handler)方法判断当前HandlerAdapter是否支持对该handler的调用;
      • 返回true,则表示支持,则把handler作为参数传给 HandlerAdapter#handle()方法进行请求处理;
      • 若遍历所有HandlerAdapter,所有HandlerAdapter都不支持该handler的调用,则抛出异常;
  • 想让DispatcherServlet支持新的Handler类型, 只需要提供对应的新的HandlerAdapter实现类 ;
    • 如 Controller这种处理器对应的HandlerAdapter是SimpleControllerHandlerAdapter;
java 复制代码
public interface HandlerAdapter {
    boolean supports(Object handler);

    @Nullable
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

    /** @deprecated */
    @Deprecated
    long getLastModified(HttpServletRequest request, Object handler);
}

【DispatcherServlet#doDispatch】 查找HandlerAdapter及调用handle()方法的过程

java 复制代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        try {
            try {
                ModelAndView mv = null;
                Exception dispatchException = null;
				// ... 
                try {
                    processedRequest = this.checkMultipart(request);
                    multipartRequestParsed = processedRequest != request;
					// 传入请求到HanderMapping获取二级控制器(或处理器)如Controller(DispatcherServlet是一级控制器) 
                    mappedHandler = this.getHandler(processedRequest);
                    if (mappedHandler == null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }
					// 传入处理器获取HandlerAdapter处理器适配器 (底层调用 adapter.supports()方法,若为true,则返回adapter )
                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                    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;
                        }
                    }

                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }
					// 调用处理器适配器的handle() 方法,并获取处理结果ModelAndView 
                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                    if (asyncManager.isConcurrentHandlingStarted()) {
                        return;
                    }
                    // ...... 
    }

【getHandlerAdapter()】

java 复制代码
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
        if (this.handlerAdapters != null) {
            Iterator var2 = this.handlerAdapters.iterator();

            while(var2.hasNext()) {
                HandlerAdapter adapter = (HandlerAdapter)var2.next();
                // 判断当前处理器适配器是否支持对该handler的调用 
                if (adapter.supports(handler)) {
                    return adapter;
                }
            }
        }
        throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
    }

【2.1.1】Handler及HandlerAdapter处理web请求过程

1)我们以Controller这种handler为例,对Handler及HandlerAdapter的工作细节进行说明;(注意: Controller是二级控制器或二级处理器,DispatcherServlet是一级处理器 )

2)Controller对应的HandlerAdapter是SimpleControllerHandlerAdapter; 定义如下;

【SimpleControllerHandlerAdapter】

我想这个HandlerAdapter的代码非常简单了,不再展开赘述;逻辑是:判断当前handler是否为二级控制器类型Controller,若是,则通过SimpleControllerHandlerAdapter本身的handle方法调用handler.handleRequest()方法处理请求;

java 复制代码
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
    public SimpleControllerHandlerAdapter() {
    }
   
    public boolean supports(Object handler) {
        return handler instanceof Controller;
    }

    @Nullable
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return ((Controller)handler).handleRequest(request, response); // 调用具体的处理器的handleRequest()方法 
    }

    public long getLastModified(HttpServletRequest request, Object handler) {
        if (handler instanceof LastModified lastModified) {
            return lastModified.getLastModified(request);
        } else {
            return -1L;
        }
    }
}

【3】web请求处理拦截与HandlerInterceptor拦截器

1)拦截器: 在web请求处理过程中,新增业务拦截逻辑,如拦截切面;应用场景如前置参数校验, 报文解析与参数类型转换, 收集日志等;

2)springmvc中使用HandlerInterceptor抽象拦截器,有3个方法(preHandle, postHandle, afterCompletion) ;

  • preHandle:在调用HandlerAdapter#handle()之前执行; (应用场景,如前置参数校验)
    • 返回true,则继续执行后续步骤;
    • 返回false, 不允许执行后续步骤; 包括HandlerInterceptor链中其他 HandlerInterceptor以及之后的Handler;
  • postHandle: 在调用HandlerAdapter#handle()之后,但在视图渲染之前执行; (应用场景,如统计处理耗时)
  • afterCompletion:无论是否抛出异常,该方法在请求被处理完成后都被执行;
java 复制代码
public interface HandlerInterceptor {
    // 前置处理 
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }
    // 后置处理 
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }
    // 无论是否抛出异常,该方法在请求被处理完成后都被执行   
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

【3.1】springmvc提供的HandlerInterceptor实现类


【3.2】自定义 HandlerInterceptor(统计执行耗时)

【TimeCostHandlerInterceptor】 执行耗时统计拦截器

java 复制代码
public class TimeCostHandlerInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        request.setAttribute("startTime" , System.currentTimeMillis());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        Long startTime = (Long) request.getAttribute("startTime");
        System.out.println("执行耗时统计(单位秒)=" + (System.currentTimeMillis() - startTime) / 1000);
    }
}

【dispatcher-servlet3-upload.xml】注册拦截器bean-timeCostHandlerInterceptor到spring容器

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 注册多部件请求解析器 -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />

    <bean id="/busiFileUpload.do" class="com.tom.springmvc.controller.upload.BusiFileUploadController" />

    <bean id="/fileUploadPage.do" class="com.tom.springmvc.controller.upload.BusiFileUploadPageController"/>

    <!-- 注册自定义处理器拦截器 -->
    <bean id="timeCostHandlerInterceptor"  class="com.tom.springmvc.handlerinterceptor.TimeCostHandlerInterceptor"/>

</beans>

【把拦截器装配到 HandlerMapping】 dispatcher-servlet.xml 中的HandlerMapping中装配拦截器timeCostHandlerInterceptor

xml 复制代码
<!-- 注册HandllerMapping bean到springweb容器, BeanNameUrlHandlerMapping使用URL与Controller的bean名称进行匹配 -->
<bean id="beanNameUrlHandlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="timeCostHandlerInterceptor" />
        </list>
    </property>
</bean>

【3.2.1】HandlerInterceptor装配

1)为什么HandlerInterceptor要在HandlerMapping装配,而不是其他组件?

【DispatcherServlet#doDispatch】

java 复制代码
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 {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);
				// Determine handler for the current request.
                // 根据请求processedRequest, 获取映射后的处理器mappedHandler,类型为HandlerExecutionChain,HandlerExecutionChain是一个Handler包装类, 包装了具体处理器(如Controller), HandlerInterceptor列表 </font>
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				} 
				// 根据Handler处理器获取 HandlerAdapter处理器适配器 
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				// ...... 
				// 调用拦截器前置处理方法(拦截器)
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
                // 调用handle处理业务逻辑,处理完后返回ModelAndView对象
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
				// ... 
				applyDefaultViewName(processedRequest, mv);
                // 调用拦截器后置处理方法(拦截器)
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				dispatchException = new ServletException("Handler dispatch failed: " + err, err);
			}
			// 视图渲染,且渲染完成后调用拦截器执行完成后的处理方法 (拦截器) -- 不抛异常也会调用  
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); 
		}
		catch (Exception ex) {
		    // 调用拦截器执行完成后的处理方法(拦截器) -- 异常时调用
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new ServletException("Handler processing failed: " + err, err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

// 根据请求processedRequest, 获取映射后的处理器,类型为HandlerExecutionChain
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

【HandlerExecutionChain】处理器执行链属性定义

显然, HandlerExecutionChain是一个Handler包装类, 包装了具体处理器(如Controller), HandlerInterceptor列表 ;

java 复制代码
public class HandlerExecutionChain {
    private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);
    private final Object handler;
    private final List<HandlerInterceptor> interceptorList = new ArrayList<>();
    private int interceptorIndex = -1;
    // ......
}

【DispatcherServlet#getHandler()调试时的内存信息】

1)getHandler():遍历所有HandlerMapping列表,通过request找出处理器(二级控制器, 如Controller);

  • 返回类型是HandlerExecutionChain,而HandlerExecutionChain包装了具体处理器和拦截器列表;

【补充】

  • AbstractHandlerMapping#getHandler()方法, 调用getHandlerExecutionChain()获取HandlerExecutionChain;
  • 而 getHandlerExecutionChain()方法新建HandlerExecutionChain对象,并把AbstractHandlerMapping中adaptedInterceptors收集到HandlerExecutionChain中;
  • 而adaptedInterceptor是由initInterceptors()方法遍历this.interceptors 并执行适配方法收集得到的;
  • 这就是为什么要在BeanNameUrlHandlerMapping的bean注册配置信息中,装配interceptors属性,并引用timeCostHandlerInterceptor的原因 ;

【dispatcher-servlet.xml】装配拦截器到BeanNameUrlHandlerMapping

xml 复制代码
<!-- 注册HandllerMapping bean到springweb容器, BeanNameUrlHandlerMapping使用URL与Controller的bean名称进行匹配 -->
<bean id="beanNameUrlHandlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="timeCostHandlerInterceptor" />
        </list>
    </property>
</bean>

【3.2.2】拦截器作用范围(HandlerMapping)

1)由配置可知,拦截器的作用范围是HandlerMapping ; 如本文配置了2个HandlerMapping,包括 BeanNameUrlHandlerMapping, SimpleUrlHandlerMapping ;而只有BeanNameUrlHandlerMapping装配了timeCostHandlerInterceptor拦截器,而SimpleUrlHandlerMapping 没有;

  • 所以:通过BeanNameUrlHandlerMapping找到的二级处理器,并调用该处理器时,才会有timeCostHandlerInterceptor拦截功能;而SimpleUrlHandlerMapping 没有;

【3.3】过滤器Filter

1)对web请求进行拦截,除了使用 HandlerInterceptor之外,还可以使用 Filter;

2)HandlerInterceptor与Filter过滤器区别:

  • Filter是Servlet规范的标准组件, Filter在DispatcherServlet之前对servlet进行拦截(过滤);是servlet级别的拦截(过滤);
  • HandlerInterceptor是在DispatcherServlet内部对handler做拦截(细粒度),包括请求处理前,请求处理后及完成后拦截;

3)Filter过滤器是一个接口,如下:

java 复制代码
package jakarta.servlet;

import java.io.IOException;

public interface Filter {
    default public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;

    default public void destroy() {
    }
} 

过滤器执行逻辑:

  • 若请求通过拦截条件,则在 doFilter()方法中执行 chain.doFilter(request, response); 把请求透传给下一个处理步骤;
  • 若不通过,则不调用 chain.doFilter,即请求处理流程终止(当然,终止请求处理时,需要封装响应报文,以提示错误信息);

【3.3.1】springmvc配置过滤器代码实践

【web.xml】注册DelegatingFilterProxy到servlet容器 【servlet容器】

xml 复制代码
<!-- 注册过滤器代理 -->
<filter>
  <filter-name>customFilter</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
  <filter-name>customFilter</filter-name>
  <url-pattern>/*</url-pattern>  <!-- 对路径以/开头的所有servlet都进行过滤 -->
</filter-mapping>

【applicationContext.xml】注册名为customFilter的过滤器到spring容器【 spring 容器】, filter名称(customFilter)需要与DelegatingFilterProxy的filterName保持一致;

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="userAppService" class="com.tom.springmvc.model.UserAppService" />

    <bean id="bankCardAppService" class="com.tom.springmvc.model.bankcard.BankCardAppService" />

    <!-- 注册自定义过滤器 -->
    <bean id="customFilter"  class="com.tom.springmvc.filter.CustomFilter"/>

</beans>

【CustomFilter】过滤器定义

java 复制代码
public class CustomFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println(request.getServletContext().getContextPath() + "CustomFilter 过滤器执行");
        chain.doFilter(request, response);
    }
}

【执行效果】

c++ 复制代码
/springmvcDiscoverFirstDemo CustomFilter 过滤器执行

【3.3.2】底层原理

1)问题: 为什么要在web.xml中注册DelegatingFilterProxy ;

  • DelegatingFilterProxy的作用:作为Filter的代理对象;当对请求拦截时,把拦截逻辑委派给具体的Filter(如本文中的CustomFilter);
    • 物理结构上DelegatingFilterProxy在web.xml中注册,在servlet容器中;
    • 而 CustomFilter 在 applicationContext.xml 中注册,在springmvc顶级WebApplicationContext容器中;
  • 当然,我们讲,在web.xml中肯定可以注册CustomFilter来执行拦截逻辑;但无法装配spring容器的bean;
  • 简单理解: 要把spring容器的bean装配到CustomFilter,则CustomerFilter必须注册到spring容器; 所以CustomerFilter在applicationContext.xml中注册(applicationContext.xml是springmvc顶级web容器加载的配置文件)
  • 又引入新问题:把CustomFilter注册到spring的顶级web容器中,servlet容器是无法识别的;由上文可知,filter是servlet级别的拦截,又servlet容器无法识别spring容器中的CustomFilter,所以如果没有中介,servlet容器是无法调用spring容器中的CustomFilter执行过滤逻辑 ;
    • 解决方法: DelegatingFilterProxy 就是连接servlet容器与spring容器的中介;DelegatingFilterProxy在web.xml中配置,注册到servlet容器,servlet容器执行DelegatingFilterProxy的doFilter()方法,doFilter方法内部根据filter名称从spring容器中取出目标filter并执行目标filter的过滤逻辑;

【注意】上述过程,在DelegatingFilterProxy#initFilterBean()方法中设置断点并调试,即可明了 ;


【4】springmvc异常处理与HandlerExceptionResolver(处理器异常解析器)

【4.1】HandlerExceptionResolver-处理器异常解析器

1)HandlerExceptionResolver定义: Handler处理器接口能够设计得如此灵活(如Handler的实现可以是servlet,也可以是Controller),除了HandlerAdapter适配器之外,还因为HandlerExceptionResolver提供的框架内统一的异常处理方式 ;

  • 若handler处理请求没有异常,则handler返回ModelAndView,封装了后续处理流程要用的视图和模型数据信息;
  • 若handler处理有异常,则由HandlerExceptionResolver接手处理异常 ,封装异常视图与异常提示信息到ModelAndView并返回;
java 复制代码
public interface HandlerExceptionResolver {   
    // 处理异常,并把处理结果封装到ModelAndView并返回 
    ModelAndView resolveException(
          HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}

2)HandlerExceptionResolver子类:

本文使用SimpleMappingExceptionResolver为例介绍HandlerExceptionResolver;


【4.2】SimpleMappingExceptionResolver

1)SimpleMappingExceptionResolver使用 Properties管理具体异常类型与所要转向的错误页面之间的映射关系;

  • SimpleMappingExceptionResolver内部遍历exceptionMappings的所有元素,找出与当前抛出异常类型最接近的映射值,并将其映射值作为错误信息页面的逻辑视图名,然后封装到ModelAndView返回以供后续处理流程使用;

2)SimpleMappingExceptionResolver属性定义:

java 复制代码
public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionResolver {

    /** The default name of the exception attribute: "exception". */
    public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception";


    @Nullable
    private Properties exceptionMappings;

    @Nullable
    private Class<?>[] excludedExceptions;

    @Nullable
    private String defaultErrorView;

    @Nullable
    private Integer defaultStatusCode;

    private final Map<String, Integer> statusCodes = new HashMap<>();

    @Nullable
    private String exceptionAttribute = DEFAULT_EXCEPTION_ATTRIBUTE;
    //...
}

3)SimpleMappingExceptionResolver属性:

  • exceptionMappings: 异常类型与异常信息视图属性映射;
  • defaultErrorView: 默认异常信息视图逻辑名;
  • defaultStatusCode:默认状态码;
  • exceptionAttribute:异常属性(前端可以通过该属性获取异常信息);

【4.2.1】SimpleMappingExceptionResolver处理异常代码实践

【applicationContext.xml】配置SimpleMappingExceptionResolver-异常处理器

xml 复制代码
<!-- 注册SimpleMappingExceptionResolver-处理器异常解析器 -->
<bean name="simpleMappingExceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="defaultErrorView" value="/error/defaultErrorPage" />
    <property name="exceptionAttribute" value="exceptionInfo" />
    <property name="exceptionMappings">
        <props>
            <prop key="com.tom.springmvc.exception.TomWebException">/error/tomWebErrorPage</prop>
            <prop key="java.lang.Exception">/error/exceptionBaseErrorPage</prop>
        </props>
    </property>
</bean>

【tomWebErrorPage.jsp】异常信息展示视图页面

jsp 复制代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"  import="java.util.List" import="java.util.ArrayList"  isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>tomWebErrorPage</title>
</head>
<body>
    tomWebErrorPage
    <p>异常信息: ${exceptionInfo}</p>
</body>
</html>

【TomWebException】自定义web异常

java 复制代码
public class TomWebException extends RuntimeException {

    public TomWebException() {
        super();
    }

    public TomWebException(String message) {
        super("TomWebException-" + message);
    }
}

【TomWebThrowExceptionController】抛出异常控制器

java 复制代码
public class TomWebThrowExceptionController extends AbstractController {
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (Objects.isNull(request.getParameter("testParamKey"))) {
            throw new TomWebException("testParamKey查无记录");
        }
        return new ModelAndView("index");
    }
}

【异常处理效果】


【4.3】HandlerExceptionResolver异常处理代码调试

1)对于HandlerExceptionResolver处理器异常解析器提供的统一处理异常细节,还是需要从DispatcherServlet#doDispatch(HttpServletRequest request, HttpServletResponse response)说起;

【DispatcherServlet#doDispatch()】web请求处理入口

java 复制代码
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 {
           // 校验是否multipart请求(包括但不限于文件上传请求)
          processedRequest = checkMultipart(request);
          
          // 获取异常处理器,类型为HandlerExecutionChain,它是一个包装器,封装了实际的二级处理器(如Controller)与拦截器列表 
          mappedHandler = getHandler(processedRequest);
          if (mappedHandler == null) {
             noHandlerFound(processedRequest, response);
             return;
          }

          // Determine handler adapter for the current request.
           // 根据实际的二级处理器获取处理器适配器
          HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
          // ... 
          //  拦截器前置处理
          if (!mappedHandler.applyPreHandle(processedRequest, response)) {
             return;
          }

          // 实际调用二级处理器的处理方法,二级处理器也就是本文定义的TomWebThrowExceptionController
          mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

          if (asyncManager.isConcurrentHandlingStarted()) {
             return;
          }

          applyDefaultViewName(processedRequest, mv);
           // 拦击器后置处理
          mappedHandler.applyPostHandle(processedRequest, response, mv);
       }
       catch (Exception ex) {
           // 若二级处理器在处理过程中抛出异常,则在这里被捕获,赋值给dispatchException
          dispatchException = ex; 
       }
       catch (Throwable err) {
          // As of 4.3, we're processing Errors thrown from handler methods as well,
          // making them available for @ExceptionHandler methods and other scenarios.
          dispatchException = new ServletException("Handler dispatch failed: " + err, err);
       }
        // 无论是否抛出异常,都执行processDispatchResult()进行后续处理
        // 若处理逻辑成功,则dispatchException=null;若抛出异常,则dispatchException就是实际的业务异常 
       processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        // 请求处理完成后触发拦截器afterCompletion方法
       triggerAfterCompletion(processedRequest, response, mappedHandler, ex); 
    }
    catch (Throwable err) {
         // 请求处理完成后触发拦截器afterCompletion方法
       triggerAfterCompletion(processedRequest, response, mappedHandler,
             new ServletException("Handler processing failed: " + err, err));
    }
    finally {
       if (asyncManager.isConcurrentHandlingStarted()) {
          // Instead of postHandle and afterCompletion
          if (mappedHandler != null) {
             mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
          }
       }
       else {
          // Clean up any resources used by a multipart request.
          if (multipartRequestParsed) {
             cleanupMultipart(processedRequest);
          }
       }
    }
}

由上文可知, 二级控制器(或二级处理器,Controller)抛出异常,被捕获后,把异常对象作为入参,调用processDispatchResult方法;

【DispatcherServlet#processDispatchResult()】加工二级控制器的请求处理结果(主要包括处理异常,视图渲染,再执行处理结束的拦截器方法)

java 复制代码
// 若二级控制器抛出异常,则exception不为空;若处理流程成功,则exception为null 
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
       @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
       @Nullable Exception exception) throws Exception {

    boolean errorView = false;

    if (exception != null) { // 异常则进入这个分支
       if (exception instanceof ModelAndViewDefiningException mavDefiningException) {
          logger.debug("ModelAndViewDefiningException encountered", exception);
          mv = mavDefiningException.getModelAndView();
       }
       else { // 有异常,类型为TomWebException,非ModelAndViewDefiningException类型,进入这个分支  
          Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
          mv = processHandlerException(request, response, handler, exception);
          errorView = (mv != null);
       }
    }

    // Did the handler return a view to render?
    // 视图渲染(本文不展开)
    if (mv != null && !mv.wasCleared()) {
       render(mv, request, response);
       if (errorView) {
          WebUtils.clearErrorRequestAttributes(request);
       }
    }
   // ...
    if (mappedHandler != null) {
       // Exception (if any) is already handled..
       mappedHandler.triggerAfterCompletion(request, response, null); // 触发执行拦截器的处理结束方法
    }
}

由上文可知,本文抛出类型为TomWebException的异常,非ModelAndViewDefiningException类型,执行processHandlerException(request, response, handler, exception);

【DispatcherServlet#processHandlerException()】加工处理器异常(调用处理器异常解析器)

java 复制代码
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
       @Nullable Object handler, Exception ex) throws Exception {

    // ...
    // Check registered HandlerExceptionResolvers...
    // 遍历注册的HandlerExceptionResolver (本文在applicationContext.xml注册的SimpleMappingExceptionResolver)
    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
       for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
           // 遍历处理器异常解析器列表,并调用其resolveException方法,获取处理器异常解析器根据异常封装的ModelAndView;
           // 异常解析器按照Ordered语义排序(值越小,优先级越高) 
          exMv = resolver.resolveException(request, response, handler, ex);
          if (exMv != null) {
             break;
          }
       }
    }
    if (exMv != null) {
       if (exMv.isEmpty()) {
          request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
          return null;
       }
       // We might still need view name translation for a plain error model...
        // 若没有视图,则使用默认视图 
       if (!exMv.hasView()) {
          String defaultViewName = getDefaultViewName(request);
          if (defaultViewName != null) {
             exMv.setViewName(defaultViewName);
          }
       }
        // ... 
       WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
       return exMv; 
    }

    throw ex;
}

由上文可知;异常处理的传播链如下:

  • DispatcherServlet#doDispatch():web请求处理入口 ;
    • handlerAdapter.handle(processedRequest, response, mappedHandler.getHandler()): DispatcherServlet调用处理器适配器的handle方法执行实际处理器的业务处理逻辑(业务处理逻辑抛出异常);
  • DispatcherServlet#processDispatchResult():加工二级控制器的请求处理结果(主要包括处理异常,视图渲染,再执行处理结束的拦截器方法);
  • DispatcherServlet#processHandlerException():加工处理器异常(调用处理器异常解析器);
    • HandlerExceptionResolver#resolveException(request, response, handler, ex): 处理器异常解析器解析异常,返回解析后的封装了异常信息的ModelAndView对象 ; (因SimpleMappingExceptionResolver继承自AbstractHandlerExceptionResolver,实际调用的是AbstractHandlerExceptionResolver#resolveException)

【AbstractHandlerExceptionResolver#resolveException】处理器异常解析器解析异常方法

java 复制代码
public ModelAndView resolveException(
       HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

    if (shouldApplyTo(request, handler)) {
       prepareResponse(ex, response);
        // 调用doResolveException() 方法解析异常; 调用子类的调用SimpleMappingExceptionResolver#doResolveException()
       ModelAndView result = doResolveException(request, response, handler, ex);
       if (result != null) {
          // ... 
       }
       return result;
    }
    else {
       return null;
    }
}

【SimpleMappingExceptionResolver#doResolveException()】处理器异常解析器解析异常方法

java 复制代码
protected ModelAndView doResolveException(
       HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

    // Expose ModelAndView for chosen error view.
    String viewName = determineViewName(ex, request);
    if (viewName != null) {
       // Apply HTTP status code for error views, if specified.
       // Only apply it if we're processing a top-level request.
       Integer statusCode = determineStatusCode(request, viewName); // 获取响应码 
       if (statusCode != null) {
          applyStatusCodeIfPossible(request, response, statusCode);
       }
        // 获取ModelAndView对象 
       return getModelAndView(viewName, ex, request);
    }
    else {
       return null;
    }
}

protected ModelAndView getModelAndView(String viewName, Exception ex, HttpServletRequest request) {
		return getModelAndView(viewName, ex);
	}
	// 封装视图名与异常对象到ModelAndView,并返回 
	protected ModelAndView getModelAndView(String viewName, Exception ex) {
		ModelAndView mv = new ModelAndView(viewName);
		if (this.exceptionAttribute != null) {
			mv.addObject(this.exceptionAttribute, ex);
		}
		return mv;
	}

【SimpleMappingExceptionResolver#getModelAndView()】

相关推荐
小江的记录本4 分钟前
【JVM虚拟机】JVM调优:常用JVM参数、调优核心指标、OOM排查、GC日志分析、Arthas工具使用(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
程序员cxuan15 分钟前
我花了两天时间,终于把 Codex 额度掉太快的问题整明白了!!
人工智能·后端·程序员
IT_陈寒17 分钟前
Vue这个动态响应坑把我整不会了
前端·人工智能·后端
金銀銅鐵17 分钟前
[Java] 用图形化界面演示 iadd, isub, iconst_<i> 指令的效果
java·后端·python
AskHarries29 分钟前
做国内还是出海
后端
J2虾虾34 分钟前
Spring AI Alibaba文档
java·人工智能·spring
YikNjy40 分钟前
break和continue
java·开发语言·算法
SomeOtherTime42 分钟前
Geojson相关(AI回答)
java·前端·python
日月云棠1 小时前
10 Integer —— 最常用的整数包装类深度解析
java·后端
大鸡腿同学1 小时前
大模型为何总 “胡说八道”?做完 RAG 知识库,我看懂了它的底层逻辑
后端