SpringMVC 启动流程源码分析

  • 三哥

内容来自【自学星球】

欢迎大家来了解我的星球,和星主(也就是我)一起学习 Java ,深入 Java 体系中的所有技术。我给自己定的时间是一年,无论结果如何,必定能给星球中的各位带来点东西。

想要了解更多,欢迎访问👉:自学星球

--------------SSM系列源码文章及视频导航--------------

创作不易,望三连支持!

SSM源码解析视频

👉点我

Spring

  1. Spring 中注入 Bean 的各种骚操作做
  2. Spring 中Bean的生命周期及后置处理器使用
  3. Spring 中容器启动分析之refresh方法执行之前
  4. Spring refresh 方法分析之一
  5. Spring refresh 方法之二 invokeBeanFactoryPostProcessors 方法解析
  6. Spring refresh 方法分析之三
  7. Spring refresh 方法之四 finishBeanFactoryInitialization 分析
  8. Spring AOP源码分析一
  9. Spring AOP源码分析二
  10. Spring 事务源码分析

SpringMVC

  1. SpringMVC 启动流程源码分析
  2. SpringMVC 请求流程源码分析

MyBatis

  1. MyBatis 源码分析之 SqlSessionFactory 创建
  2. MyBatis 源码分析之 SqlSession 创建
  3. MyBatis 源码分析之 Mapper 接口代理对象生成及方法执行
  4. MyBatis 源码分析之 Select 语句执行(上)
  5. MyBatis 源码分析之 Select 语句执行(下)
  6. MyBatis 源码分析一二级缓存

---------------------【End】--------------------

一、SpringMVC案例

遵循源码解读的惯例,先来搭建一个简单的使用案例。

SpringMVC的案例可以分为两种:

  1. XML版本
  2. 注解版本

对于这两种形式的使用都非常重要,所以后面我都会对其进行源码分析,那先来看看使用流程。

1.1 XML版本

1、创建一个 web 环境的项目

2、引入 pom 依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
</dependency>

3、编写SpringMVC配置文件

<?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:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
                           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <!-- 扫描被注解的包 -->
    <context:component-scan base-package="cn.j3code.studyspring.mvc.xml" use-default-filters="false">
        <!-- 告知springMVC只扫描Controller注解的包-->
        <context:include-filter type="annotation"
                                expression="org.springframework.stereotype.Controller"/>
        <context:include-filter type="annotation"
                                expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
    </context:component-scan>
    <!-- 开启springMVC框架支持   非常重要-->
    <mvc:annotation-driven></mvc:annotation-driven>
</beans>

4、编写 web.xml 文件

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>
    <!-- 前端控制器-->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springweb.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

5、编写测试 Controller

@RestController
@RequestMapping("/my")
public class MyController {

    @GetMapping("/test01")
    public String test() {
        return "Hello World J3";
    }
}

6、配置 Tomcat 之后,就可以启动测试了

1.2 注解版本

注解版本和 XML 版本的 1、2 步骤一样。

3、编写注解配置文件

@Configuration
@ComponentScan("cn.j3code.studyspring.mvc.annotation")
public class MySpringMvcConfig {
}

4、编写web初始化类

public class WebInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 启动 SpringMVC 容器
        AnnotationConfigWebApplicationContext app = new AnnotationConfigWebApplicationContext();

        // 注册 MVC 配置类
        app.register(MySpringMvcConfig.class);

        // 创建 DispatcherServlet
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(app));

        // 填写映射路径
        dispatcher.addMapping("/");

        // 填写优先级
        dispatcher.setLoadOnStartup(1);
    }
}

5、web.xml配置文件置空或者删除

6、编写测试 Controller

@RestController
@RequestMapping("/my/annotation")
public class MyAnnotationController {

    @GetMapping("/test01")
    public String test() {
        return "Hello World J3";
    }
}

7、配置 Tomcat 之后,就可以启动测试了

二、启动流程分析

使用案例搭建完毕之后,那就要来分析分析其是如何启动的了,依然是分两种情况分析:

  1. XML启动流程
  2. 注解启动流程

2.1 xml 方式启动流程

在 XML 方式启动 SpringMVC 框架时,我们在 web.xml 文件中配置了一个类 DispatcherServlet 。显然,这个类就是我们分析 SpringMVC 框架启动流程的入口。

先来看看 DispatcherServlet 类的继承结构图:

在这里插入图片描述

由图我们可知 DispatcherServlet 是一个 Servlet ,那么在 Tomcat 启动的时候会来调用其 init 方法进行 Servlet 的初始化,所以这就是我们分析的入口。

org.springframework.web.servlet.HttpServletBean#init

public final void init() throws ServletException {
    if (logger.isDebugEnabled()) {
        logger.debug("Initializing servlet '" + getServletName() + "'");
    }

    // Set bean properties from init parameters.
    // 获取在web.xml配置的初始化参数<init-param>,并将其设置到DispatcherServlet中
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            // bw 就是 DispatcherServlet
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
            initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        }
        catch (BeansException ex) {
            if (logger.isErrorEnabled()) {
                logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
            }
            throw ex;
        }
    }

    // Let subclasses do whatever initialization they like.
    // 调用子类FrameworkServlet进行初始化
    // 模板方法,此方法在HttpServletBean本身是空的,但是因为调用方法的对象是DispatcherServlet
    // 所以优先在DispatcherServlet找,找不到再去父类找,最后在FrameworkServlet找
    initServletBean();

    if (logger.isDebugEnabled()) {
        logger.debug("Servlet '" + getServletName() + "' configured successfully");
    }
}

该方法做了两件事情,配置初始化参数和初始化 Servlet ,下面我们来看看是如何初始化的。

org.springframework.web.servlet.FrameworkServlet#initServletBean

protected final void initServletBean() throws ServletException {
    getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
    if (this.logger.isInfoEnabled()) {
        this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
    }
    long startTime = System.currentTimeMillis();

    try {
        // 初始化WebApplicationContext,并调用子类(DispatcherServlet)的onRefresh(wac)方法
        this.webApplicationContext = initWebApplicationContext();
        // 空方法
        initFrameworkServlet();
    }
    catch (ServletException ex) {
        this.logger.error("Context initialization failed", ex);
        throw ex;
    }
    catch (RuntimeException ex) {
        this.logger.error("Context initialization failed", ex);
        throw ex;
    }

    if (this.logger.isInfoEnabled()) {
        long elapsedTime = System.currentTimeMillis() - startTime;
        this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
                         elapsedTime + " ms");
    }
}

看 initWebApplicationContext 方法名字也知道这是初始化 web 应用上下文的,所以我们进去看看。

org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext

protected WebApplicationContext initWebApplicationContext() {
    // 获取root WebApplicationContext,即web.xml中配置的listener(ContextLoaderListener)
    WebApplicationContext rootContext =
        WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;

    // 判断容器是否由编程式传入(即是否已经存在了容器实例),存在的话直接赋值给wac,给springMVC容器设置父容器
    // 最后调用刷新函数configureAndRefreshWebApplicationContext(wac),作用是把Spring MVC配置文件的配置信息加载到容器中去
    if (this.webApplicationContext != null) {
        // A context instance was injected at construction time -> use it
        // context上下文在构造是注入
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                // context没有被refreshed,提供一些诸如设置父context、设置应用context id等服务
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent -> set
                    // the root application context (if any; may be null) as the parent
                    cwac.setParent(rootContext);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    // 在ServletContext中寻找是否有Spring MVC容器,初次运行是没有的,Spring MVC初始化完毕ServletContext就有了Spring MVC容器
    if (wac == null) {
        // No context instance was injected at construction time -> see if one
        // has been registered in the servlet context. If one exists, it is assumed
        // that the parent context (if any) has already been set and that the
        // user has performed any initialization such as setting the context id
        wac = findWebApplicationContext();
    }
    // 当wac既没有没被编程式注册到容器中的,也没在ServletContext找得到,此时就要新建一个Spring MVC容器
    if (wac == null) {
        // No context instance is defined for this servlet -> create a local one
        // 如果没有WebApplicationContext则创建
        wac = createWebApplicationContext(rootContext);
    }

    // 到这里Spring MVC容器已经创建完毕,接着真正调用DispatcherServlet的初始化方法onRefresh(wac)
    // 此处仍是模板模式的应用
    if (!this.refreshEventReceived) {
        // Either the context is not a ConfigurableApplicationContext with refresh
        // support or the context injected at construction time had already been
        // refreshed -> trigger initial onRefresh manually here.
        onRefresh(wac);
    }

    // 将Spring MVC容器存放到ServletContext中去,方便下次取出来
    if (this.publishContext) {
        // Publish the context as a servlet context attribute.
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
                              "' as ServletContext attribute with name [" + attrName + "]");
        }
    }

    return wac;
}

这个方法主要干了些啥:

  1. 获取root WebApplicationContext。
  2. 如果 root WebApplicationContext 不为空,则判断是否为 ConfigurableWebApplicationContext 类型,是的话则进行相关配置并执行 refresh 方法。
  3. 如果 wac 为空则调用 findWebApplicationContext 方法进行查找。
  4. 如果 wac 为空则调用 createWebApplicationContext 方法创建 web 应用上下文。
  5. 默认执行 onRefresh 方法,该方法为模板方法,被 DispatcherServlet 类所重写。
  6. 将 wac 存放到 ServletContext 中。

上述方法的 1 到 4 都是围绕 WebApplicationContext 所展开,如果有 WebApplicationContext 则进行配置和刷新如果没有则进行创建然后再配置刷新,所以我们就只分析一个比较全的方法 createWebApplicationContext 就可以了。

源码如下:

org.springframework.web.servlet.FrameworkServlet#createWebApplicationContext(org.springframework.web.context.WebApplicationContext)

protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) {
    return createWebApplicationContext((ApplicationContext) parent);
}

在进入:

org.springframework.web.servlet.FrameworkServlet#createWebApplicationContext(org.springframework.context.ApplicationContext)

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    Class<?> contextClass = getContextClass();
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Servlet with name '" + getServletName() +
                          "' will try to create custom WebApplicationContext context of class '" +
                          contextClass.getName() + "'" + ", using parent context [" + parent + "]");
    }
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException(
            "Fatal initialization error in servlet with name '" + getServletName() +
            "': custom WebApplicationContext class [" + contextClass.getName() +
            "] is not of type ConfigurableWebApplicationContext");
    }
    // 实例化容器
    ConfigurableWebApplicationContext wac =
        (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
    // 设置容器环境
    wac.setEnvironment(getEnvironment());
    // 设置父容器
    wac.setParent(parent);
    // 加载Spring MVC的配置信息,如:bean注入、注解、扫描等等
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }
    // 刷新容器,根据Spring MVC配置文件完成初始化操作
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}

该方法创建了 web 容器紧接着最关键的一步就是通过 configureAndRefreshWebApplicationContext 方法来刷新容器,下面来看看该方法源码。

org.springframework.web.servlet.FrameworkServlet#configureAndRefreshWebApplicationContext

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    // 用可以获取到的信息,获取一个更有意义的上下文
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }
        else {
            // Generate default id...
            // 生成默认 id
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                      ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
        }
    }
    // 设置 servletContext 属性
    wac.setServletContext(getServletContext());
    wac.setServletConfig(getServletConfig());
    wac.setNamespace(getNamespace());
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    // 初始化属性源, 确保 servlet属性源到位并能够在任何 refresh 之前的后期处理和初始化中使用
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
    }
    // 空方法
    postProcessWebApplicationContext(wac);
    // 应用 ApplicationContextInitializer 接口中的 initialize 方法
    applyInitializers(wac);
    //刷新容器
    wac.refresh();
}

这个方法为 web 容器设置了很多必要属性,并且在刷新容器之前还应用了 ApplicationContextInitializer 接口中的 initialize 方法,最后才开始进行容器的刷新。

现在我们回归到 initWebApplicationContext 方法,只剩下了 onRefresh(wac) 没有分析,那来看看其源码吧!

org.springframework.web.servlet.DispatcherServlet#onRefresh

protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

org.springframework.web.servlet.DispatcherServlet#initStrategies

protected void initStrategies(ApplicationContext context) {
    // 1、初始化文件上传处理器
    initMultipartResolver(context);

    // 2、初始化国际化配置
    initLocaleResolver(context);

    // 3、初始化主题处理器
    initThemeResolver(context);

    // 4、初始化HanlderMapping
    initHandlerMappings(context);

    // 5、初始化HandlerAdapter
    // HandlerAdapter用来调用具体的方法对用户发来的请求来进行处理
    initHandlerAdapters(context);

    // 6、初始化异常处理器,
    // HandlerExceptionResolver是用来对请求处理过程中产生的异常进行处理
    initHandlerExceptionResolvers(context);

    // 7、RequestToViewNameTranslator用于在视图路径为空的时候,自动解析请求
    // 去获取ViewName
    initRequestToViewNameTranslator(context);

    // 8、初始化视图处理器
    // ViewResolvers将逻辑视图转成view对象
    initViewResolvers(context);

    // 9、FlashMapManager用于存储、获取以及管理FlashMap实例
    initFlashMapManager(context);
}

该方法是初始化 SpringMVC 中的九大组件,这些组件初始化完毕时也就是意味着 SpringMVC 启动成功。

2.1 注解方式启动流程

注解启动方式我们主要看案例中的 WebInitializer 类。

public class WebInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 启动 SpringMVC 容器
        AnnotationConfigWebApplicationContext app = new AnnotationConfigWebApplicationContext();

        // 注册 MVC 配置类
        app.register(MySpringMvcConfig.class);

        // 创建 DispatcherServlet
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(app));

        // 填写映射路径
        dispatcher.addMapping("/");

        // 填写优先级
        dispatcher.setLoadOnStartup(1);
    }
}

该类实现了 WebApplicationInitializer 接口中的 onStartup 方法,并且直接创建出了 web 容器和 DispatcherServlet 然后添加到 ServletContext 中。那有 DispatcherServlet 就好办了,肯定会走它的 init 方法,这流程就和 xml 的一致了,就不重复分析了。

最后一个问题来了,谁会过来调用 WebInitializer 类呢!

要明白这点,就不得不来看下面这个类了:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
        throws ServletException {

        List<WebApplicationInitializer> initializers = new LinkedList<>();

        // webAppInitializerClasses 就是servlet3.0规范中为我们收集的 WebApplicationInitializer 接口的实现类的class
        // 从webAppInitializerClasses中筛选并实例化出合格的相应的类
        if (webAppInitializerClasses != null) {
            for (Class<?> waiClass : webAppInitializerClasses) {
                // Be defensive: Some servlet containers provide us with invalid classes,
                // no matter what @HandlesTypes says...
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                    WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)
                                         ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }

        servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
        // 这行代码说明我们在实现WebApplicationInitializer可以通过继承Ordered, PriorityOrdered来自定义执行顺序
        AnnotationAwareOrderComparator.sort(initializers);
        for (WebApplicationInitializer initializer : initializers) {
            // 迭代每个initializer实现的方法
            initializer.onStartup(servletContext);
        }
    }

}

这个类是干啥的?

HandlesTypes 注解用于声明 onStartup 方法参数中感兴趣的 class ,例如 SpringServletContainerInitializer 类,HandlesTypes 注解中的值为 WebApplicationInitializer.class ,则 onStartup 参数中 webAppInitializerClasses 集合的值都是 WebApplicationInitializer.class 类型。

ServletContainerInitializer 是 Servlet 3.0 引入的接口,用于在 web 应用启动时动态添加 servlet 、 filter 和 listener 。

在 Tomcat 启动时,会读取 META-INF/services/javax.servlet.ServletContainerInitializer 文件中存放实现该接口的类,并调用 onStartup 方法,SpringServletContainerInitializer 的配置如图:

在这里插入图片描述

该机制也称为 SPI。

现在知道我们的 WebInitializer 类为啥会生效了吧!

三、九大组件初始化

这九大组件的初始化流程都非常类似,大致流程如下:

  1. 判断是否检查所有的 XXX 实现类并载入,默认为true
  2. 是,寻找IOC容器中对应类型的Bean并对结果集进行排序
  3. 否,从容器里获取beanName为对应类型的Bean
  4. 如果获取的结果为空,则调用 getDefaultStrategies(context, 对应组件 class) 获取默认值

我们来看看它们共同调用的方法 getDefaultStrategies 。

public class DispatcherServlet extends FrameworkServlet {
    private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
    static {
        // Load default strategy implementations from properties file.
        // This is currently strictly internal and not meant to be customized
        // by application developers.
        try {
            // 创建文件资源对象,文件路径为 DispatcherServlet.properties
            ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
            // 加载资源文件
            defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
        }
        catch (IOException ex) {
            throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
        }
    }
    protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
        // strategyInterface的class全限定名称
        String key = strategyInterface.getName();
        // 从默认properties中根据托底组件的class名称获取对应的托底组件实现类名称.
        // 可以参考DispatcherServlet.properties的具体格式
        String value = defaultStrategies.getProperty(key);
        if (value != null) {
            // 一个策略接口可能对应多个实现类.
            // 策略接口和实现类的格式为:com.wb.spring.interface.Strategy=com.wb.spring.impl.A,com.wb.spring.impl.B
            String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
            // 初始化策略实现类组价的list集合
            List<T> strategies = new ArrayList<>(classNames.length);
            // 循环初始化
            for (String className : classNames) {
                try {
                    Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
                    // 创建策略接口实现类对应的Bean组件
                    // 通过AutowireCapableBeanFactory.createBean(clazz)实现
                    Object strategy = createDefaultStrategy(context, clazz);
                    strategies.add((T) strategy);
                }
                catch (ClassNotFoundException ex) {
                    throw new BeanInitializationException(
                        "Could not find DispatcherServlet's default strategy class [" + className +
                        "] for interface [" + key + "]", ex);
                }
                catch (LinkageError err) {
                    throw new BeanInitializationException(
                        "Error loading DispatcherServlet's default strategy class [" + className +
                        "] for interface [" + key + "]: problem with class file or dependent class", err);
                }
            }
            return strategies;
        }
        else {
            return new LinkedList<>();
        }
    }
}

代码中的资源配置文件如下:

DispatcherServlet.properties

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

下面以 HandlerMapping 为例,说说 getDefaultStrategies 与 DispatcherServlet.properties 的加载关系:

默认情况下,SpringMVC 将加载当前系统中所有实现了 HandlerMapping 接口的bean。如果只期望 SpringMVC 加载指定的 HandlerMapping 时,可以修改 web.xml 中的 DispatcherServlet 的初始参数,将 detectAllHandlerMappings 的值设置为 false:

<init-param>
 <param-name>detectAllHandlerMappings</param-name>
 <param-value>false</param-value>
</init-param>

此时 Spring MVC 将查找名为 “handlerMapping” 的 bean ,并作为当前系统中唯一的 handlermapping 。如果没有定义 handlerMapping 的话,则 SpringMVC 将按照 org.Springframework.web.servlet.DispatcherServlet 所在目录下的 DispatcherServlet.properties 中所定义的 org.Springframework.web.servlet.HandlerMapping 的内容来加载默认的 handlerMapping 。

四、HandlerMapping

4.1 HandlerMapping 之 初始化

由第三节我们知道 SpringMVC 默认的 HandlerMapping 有两个:

  • org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping:基于配置文件的方式生成映射信息,所有处理器需要在 XML 文件中,以Bean的形式配置。
  • org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping:基于@RequestMapping 注解的方式生成映射信息。

在这里,我会分析 RequestMappingHandlerMapping ,因为平常我们使用最多的也就是通过 @RequestMapping 注解来标注映射 URL 。

先来看看 RequestMappingHandlerMapping 的类结构图:

在这里插入图片描述

该类我们关注两个点:

  • 实现类 Aware 接口体系中的相关接口
  • 实现了 InitializingBean 生命周期接口

通过 Aware 接口可以获取到容器及容器中的 Bean ,通过 InitializingBean 接口,可以在该类调用构造器方法之后做些小动作(afterPropertiesSet 方法)。

那,我们来看看该类功能开始的入口把!

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#afterPropertiesSet

public void afterPropertiesSet() {
    // 各种配置设置
    this.config = new RequestMappingInfo.BuilderConfiguration();
    this.config.setUrlPathHelper(getUrlPathHelper());
    this.config.setPathMatcher(getPathMatcher());
    this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
    this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
    this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
    this.config.setContentNegotiationManager(getContentNegotiationManager());
    // 调用AbstractHandlerMethodMapping afterPropertiesSet 方法
    super.afterPropertiesSet();
}

this.config 表示生成请求映射对象的配置类,该类先对其进行了相关赋值,然后调用父类 afterPropertiesSet 方法。

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#afterPropertiesSet

public void afterPropertiesSet() {
    initHandlerMethods();
}

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#initHandlerMethods

protected void initHandlerMethods() {
    if (logger.isDebugEnabled()) {
        logger.debug("Looking for request mappings in application context: " + getApplicationContext());
    }
    // 从root容器以及子容器里,或者仅从子容器里获取所有的Bean
    String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
                          BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
                          obtainApplicationContext().getBeanNamesForType(Object.class));
    // 遍历容器里所有的Bean
    for (String beanName : beanNames) {
        // 忽略掉scopedTarget.打头的bean(session application request之类的作用域内的代理类)
        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
            Class<?> beanType = null;
            try {
                // 获取Bean的Class类型
                beanType = obtainApplicationContext().getType(beanName);
            }
            catch (Throwable ex) {
                // An unresolvable bean type, probably from a lazy bean - let's ignore it.
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
                }
            }
            // 判断Class上是否有Controller注解或是RequestMapping注解
            if (beanType != null && isHandler(beanType)) {
                // 提取其url与controller映射关系
                detectHandlerMethods(beanName);
            }
        }
    }
    // 空方法
    handlerMethodsInitialized(getHandlerMethods());
}

先来看看判断方法 isHandler :

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler

protected boolean isHandler(Class<?> beanType) {
    //判断类上是否存在Controller注解或是RequestMapping注解
    return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
            AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

接着来看看 detectHandlerMethods 方法:

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#detectHandlerMethods

protected void detectHandlerMethods(final Object handler) {
    	// 如果handler是字符串,证明是一个beanName,则从IOC容器中获取其Class对象;否则直接获取Class对象
		Class<?> handlerType = (handler instanceof String ?
				obtainApplicationContext().getType((String) handler) : handler.getClass());

		if (handlerType != null) {
            // 为了确保获取到的类是被代理的类
			final Class<?> userType = ClassUtils.getUserClass(handlerType);
            // 寻找方法上有@RequestMapping注解的Method实例
			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
					(MethodIntrospector.MetadataLookup<T>) method -> {
						try {
							return getMappingForMethod(method, userType);
						}
						catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" +
									userType.getName() + "]: " + method, ex);
						}
					});
			if (logger.isDebugEnabled()) {
				logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
			}
            // 将获取到的Method对象依次注册到HandlerMapping中去
			for (Map.Entry<Method, T> entry : methods.entrySet()) {
                // 获取被AOP代理包装后的方法实例
				Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
				T mapping = entry.getValue();
				registerHandlerMethod(handler, invocableMethod, mapping);
			}
		}
	}

这个方法有两个重点:

  • 获取 @RequestMapping 注解标注的方法
  • 注册 @RequestMapping 注解标注的方法

先来看看获取

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#getMappingForMethod

protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
    // 创建方法上面的RequestMapping信息
    RequestMappingInfo info = createRequestMappingInfo(method);
    if (info != null) {
        // 创建类上面的RequestMapping信息
        RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
        if (typeInfo != null) {
            // 将两个信息合并
            info = typeInfo.combine(info);
        }
    }
    // 返回
    return info;
}

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#createRequestMappingInfo(java.lang.reflect.AnnotatedElement)

private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
    // 如果该函数含有@RequestMapping注解,则根据其注解信息生成RequestMapping实例,
    // 否则返回空
    RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
    RequestCondition<?> condition = (element instanceof Class ?
                                     getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
    return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#createRequestMappingInfo(org.springframework.web.bind.annotation.RequestMapping, org.springframework.web.servlet.mvc.condition.RequestCondition<?>)

protected RequestMappingInfo createRequestMappingInfo(
    RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
    // 这里用到了一个典型的建造者模式
    RequestMappingInfo.Builder builder = RequestMappingInfo
        // 这里对路径进行解析,在path中是支持SpEL表达式的,
        // RequestMappingHandlerMapping实现了EmbeddedValueResolverAware这个接口
        .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
        .methods(requestMapping.method())
        .params(requestMapping.params())
        .headers(requestMapping.headers())
        .consumes(requestMapping.consumes())
        .produces(requestMapping.produces())
        .mappingName(requestMapping.name());
    if (customCondition != null) {
        builder.customCondition(customCondition);
    }
    return builder.options(this.config).build();
}

让我们再回到 detectHandlerMethods 方法,当 selectMethods 方法执行完之后,Controller 方法实例和 RequestMappingInfo 的映射关系就建立起来了,并保存在 methods 这个 Map<Method, T> 集合中,key为方法实例,value 为 RequestMappingInfo 实例。

接下来再来分析注册方法:

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#registerHandlerMethod

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
    this.mappingRegistry.register(mapping, handler, method);
}

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#register

//储存 MappingRegistration 所有的注册信息
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
//储存RequestMappingInfo 与 HandlerMethod
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
//储存路径与RequestMappingInfo
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
//储存@RequestMapping 注解的请求路径 与 HandlerMethod列表
private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
//跨域配置
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
//读写锁
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

/**
 * 注册数据:  mapping => RequestMappingInfo ||  handler => beanName  ||  method => Method
 *      1、根据 handle 和 method,创建 HandlerMethod,
 *      2、效验 HandlerMethod 是否存在
 *      3、储存 HandlerMethod
 *      4、储存 RequestMappingInfo 跟 url
 *      5、储存 @RequestMapping 注解 的路径跟所有的方法
 *      6、存储 CorsConfiguration 信息(跨域)
 *      7、储存 MappingRegistration 对象
 */
public void register(T mapping, Object handler, Method method) {
    this.readWriteLock.writeLock().lock();
    try {
        // 根据 handle 和 method,创建 HandlerMethod,
        HandlerMethod handlerMethod = createHandlerMethod(handler, method);
        //验证方法的唯一性,即先前是否已经注册过同样的映射
        assertUniqueMethodMapping(handlerMethod, mapping);

        if (logger.isInfoEnabled()) {
            logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod);
        }
        // 注册RequestMappingInfo 和 HandlerMethod
        this.mappingLookup.put(mapping, handlerMethod);
        // 注册请求路径与对应的RequestMappingInfo
        List<String> directUrls = getDirectUrls(mapping);
        for (String url : directUrls) {
            this.urlLookup.add(url, mapping);
        }

        String name = null;
        if (getNamingStrategy() != null) {
            // 注册请求路径与HandlerMethod
            name = getNamingStrategy().getName(handlerMethod, mapping);
            addMappingName(name, handlerMethod);
        }

        CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
        if (corsConfig != null) {
            // 注册HandlerMethod与跨域信息
            this.corsLookup.put(handlerMethod, corsConfig);
        }
        // 创建及注册 MappingRegistation 信息
        this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
    }
    finally {
        this.readWriteLock.writeLock().unlock();
    }
}

该方法主要干了写啥:

  • 根据 handle 和 method,创建 HandlerMethod
  • 效验 HandlerMethod 是否存在,验证方法的唯一性,即先前是否已经注册过同样的映射
  • 注册 RequestMappingInfo 和 HandlerMethod
  • 注册请求路径与对应的 RequestMappingInfo
  • 注册请求路径与 HandlerMethod
  • 注册 HandlerMethod 与跨域信息
  • 创建及注册 MappingRegistation 信息

最终会将前面获取到的所有信息给包装起来,保存到 Map<T, MappingRegistration<T>> registry 成员变量中,后续就可以解析请求并选择合适的 Controller 方法来对请求进行处理。

到此就建立了 URL 请求和 Controller 方法的映射了。

4.1 HandlerMapping 之 getHandler 方法

getHandler 方法会返回一个 HandlerExecutionChain 对象,该对象包含用户请求的处理程序(handler)和拦截器(interceptors)。

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

org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    // 获取handler的具体逻辑,留给子类实现
    Object handler = getHandlerInternal(request);
    if (handler == null) {
        // 如果获取到的handler为null,则采用默认的handler,即属性defaultHandler
        handler = getDefaultHandler();
    }
    if (handler == null) {
        //如果连DefaultHandler也为空,则直接返回空
        return null;
    }
    // Bean name or resolved handler?
    // 如果handler是beanName,从容器里获取对应的bean
    if (handler instanceof String) {
        String handlerName = (String) handler;
        handler = obtainApplicationContext().getBean(handlerName);
    }
    // 根据handler和request获取处理器链HandlerExecutionChain
    HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
    // CORS请求的处理
    if (CorsUtils.isCorsRequest(request)) {
        // 全局配置
        CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
        // handler的单独配置
        CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
        // handler的所有配置,全局配置+单独配置
        CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
        // 根据cors配置更新HandlerExecutionChain
        executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
    }
    return executionChain;
}

该类逻辑很简单,先获取具体的 handler ,然后通过 handler 和 request 获取我们需要的 HandlerExecutionChain 对象。

那先来看看获取 handler 相关方法。

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    // 获取 request 中的 url,用来匹配 handler
    String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
    if (logger.isDebugEnabled()) {
        logger.debug("Looking up handler method for path " + lookupPath);
    }
    this.mappingRegistry.acquireReadLock();
    try {
        // 根据路径寻找 Handler,并封装成 HandlerMethod
        HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
        if (logger.isDebugEnabled()) {
            if (handlerMethod != null) {
                logger.debug("Returning handler method [" + handlerMethod + "]");
            }
            else {
                logger.debug("Did not find handler method for [" + lookupPath + "]");
            }
        }
        // 根据 handlerMethod 中的 bean 来实例化 Handler,并添加进 HandlerMethod
        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    }
    finally {
        this.mappingRegistry.releaseReadLock();
    }
}

该方法逻辑简单,先从 request 中获取请求 url ,然后通过 url 寻找对应的 HandlerMethod ,最后将找到的 handlerMethod 再次封装并返回出去。

那我们来看看,SpringMVC 是如何通过 url 寻找 handlerMethod 的。

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod

protected HandlerMethod lookupHandlerMethod(String lookupPath, 
                                            HttpServletRequest request) throws Exception {
    List<Match> matches = new ArrayList<>();
    // 通过uri直接在注册的RequestMapping中获取对应的RequestMappingInfo列表,需要注意的是,
    // 这里进行查找的方式只是通过url进行查找,但是具体哪些RequestMappingInfo是匹配的,还需要进一步过滤
    List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
    if (directPathMatches != null) {
        // 对获取到的RequestMappingInfo进行进一步过滤,并且将过滤结果封装为一个Match列表
        addMatchingMappings(directPathMatches, matches, request);
    }
    if (matches.isEmpty()) {
        // 如果无法通过uri进行直接匹配,则对所有的注册的RequestMapping进行匹配,这里无法通过uri
        // 匹配的情况主要有三种:
        // ①在RequestMapping中定义的是PathVariable,如/user/detail/{id};
        // ②在RequestMapping中定义了问号表达式,如/user/?etail;
        // ③在RequestMapping中定义了*或**匹配,如/user/detail/**
        addMatchingMappings(this.mappingRegistry.getMappings().keySet(), 
                            matches, request);
    }

    if (!matches.isEmpty()) {
        // 对匹配的结果进行排序,获取相似度最高的一个作为结果返回,这里对相似度的判断时,
        // 会判断前两个是否相似度是一样的,如果是一样的,则直接抛出异常,如果不相同,
        // 则直接返回最高的一个
        Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
        matches.sort(comparator);
        if (logger.isTraceEnabled()) {
            logger.trace("Found " + matches.size() 
                         + " matching mapping(s) for [" + lookupPath + "] : " + matches);
        }
        // 获取匹配程度最高的一个匹配结果
        Match bestMatch = matches.get(0);
        if (matches.size() > 1) {
            // 如果匹配结果不止一个,首先会判断是否是跨域请求,如果是,
            // 则返回PREFLIGHT_AMBIGUOUS_MATCH,如果不是,则会判断前两个匹配程度是否相同,
            // 如果相同则抛出异常
            if (CorsUtils.isPreFlightRequest(request)) {
                return PREFLIGHT_AMBIGUOUS_MATCH;
            }
            Match secondBestMatch = matches.get(1);
            if (comparator.compare(bestMatch, secondBestMatch) == 0) {
                Method m1 = bestMatch.handlerMethod.getMethod();
                Method m2 = secondBestMatch.handlerMethod.getMethod();
                throw new IllegalStateException("Ambiguous handler methods mapped for" 
                                                + " HTTP path '" + request.getRequestURL() + "': {" + m1 
                                                + ", " + m2 + "}");
            }
        }
        // 这里主要是对匹配结果的一个处理,主要包含对传入参数和返回的MediaType的处理
        handleMatch(bestMatch.mapping, lookupPath, request);
        return bestMatch.handlerMethod;
    } else {
        // 如果匹配结果是空的,则对所有注册的Mapping进行遍历,判断当前request具体是哪种情况导致
        // 的无法匹配:①RequestMethod无法匹配;②Consumes无法匹配;③Produces无法匹配;
        // ④Params无法匹配
        return handleNoMatch(this.mappingRegistry.getMappings().keySet(), 
                             lookupPath, request);
    }
}

该方法该类写啥:

1、首先根据url在对应的匹配集directPathMatches内进行查找,如果查找不到则进行全局查找。

2、要求 bestMatch 的优先级必须大于 matches 剩余的所有的匹配条件,如果存在相同的,则抛出错误。

3、调用handleMatch进行匹配成功后的处理

4、调用handleNoMatch进行匹配失败后的处理

再来看看通过 handler 和 request 获取我们需要的 HandlerExecutionChain 对象。

org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandlerExecutionChain

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
    // 判断handler是不是执行器链,如果不是则创建一个执行器链
    HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
                                   (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));

    // 获取请求url
    String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
    //包装拦截器
    /**
     *      <mvc:interceptor>
     *          <mvc:mapping path="/shopadmin/**" />
     *          <bean id="ShopInterceptor"
     *              class="com.imooc.o2o.interceptor.shopadmin.ShopLoginInterceptor" />
     *      </mvc:interceptor>
     */
    for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
        // MappedInterceptor根据url判断是否匹配
        if (interceptor instanceof MappedInterceptor) {
            MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
            if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
                chain.addInterceptor(mappedInterceptor.getInterceptor());
            }
        }
        else {
            // 其它类型的Interceptor对所有请求都有效
            chain.addInterceptor(interceptor);
        }
    }
    return chain;
}

该方法将 handler 封装成一个 HandlerExecutionChain 对象,然后遍历所有的拦截器,判断 url 是否符合拦截器的规则,符合则添加到 HandlerExecutionChain 对象中,最后返回该对象。

好了,今天的内容到这里就结束了,我是 【J3】关注我,我们下期见


  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。

  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。

  • 感谢您的阅读,十分欢迎并感谢您的关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

J3code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值