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

【README】

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

1)springmvc其他组件如下:

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

2)web.xml (web应用部署描述符,servlet容器加载时读取的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文件上传表单元素 -->
<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】文件上传页面

<%@ 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】

<!-- 注册一级控制器 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 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】文件上传控制器

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】

/**
 * @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;
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()方法的过程

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()】

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()方法处理请求;

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:无论是否抛出异常,该方法在请求被处理完成后都被执行;
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】 执行耗时统计拦截器

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 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

<!-- 注册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】

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列表

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

<!-- 注册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过滤器是一个接口,如下:

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容器

<!-- 注册过滤器代理 -->
<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 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】过滤器定义

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);
    }
}

【执行效果】

/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并返回;
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属性定义:

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-异常处理器

<!-- 注册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】异常信息展示视图页面

<%@ 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异常

public class TomWebException extends RuntimeException {

    public TomWebException() {
        super();
    }

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

【TomWebThrowExceptionController】抛出异常控制器

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请求处理入口

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()】加工二级控制器的请求处理结果(主要包括处理异常,视图渲染,再执行处理结束的拦截器方法)

// 若二级控制器抛出异常,则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()】加工处理器异常(调用处理器异常解析器)

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】处理器异常解析器解析异常方法

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()】处理器异常解析器解析异常方法

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()】

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值