Java架构技术进阶

当一个http请求来临时,SpringMVC究竟偷偷帮你做了什么

2020-06-19  本文已影响0人  代码搬运者Java

前文

Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。

Springmvc的优点:

  1. 可以支持各种视图技术,而不仅仅局限于JSP;
  2. 与Spring框架集成(如IoC容器、AOP等);
  3. 清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。
  4. 支持各种请求资源的映射策略。

请求映射器源码解析

这些优秀的特性使得它在企业级开发中使用率超过98%,如此优秀的框架,你是否疑惑过,在一个请求到达后,是如何被SpringMvc拦截到并处理的?

image

相信大家对上面的流程图都很熟悉,或多或少无论是在准备面试的时候,还是自己学习的时候,都会接触到这个流程图,我见过很多的人,对着这个图死记硬背!我也面试过一些技术人员,问到这块知识,仰着头闭着眼(夸张一下)把这块知识说出来,再往深了问一点就懵逼,归根到底就是对框架理解不够深刻。

SpringMVC是如何感知到每个方法对应的url路径的?

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping 实现 org.springframework.beans.factory.InitializingBean 覆盖 afterPropertiesSet方法,这个方法会在Spring容器初始化的时候回调该方法

image

该方法类定义为

<pre style="box-sizing: border-box; outline: none; margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: couriernew, courier, monospace; vertical-align: baseline; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

@Override
public void afterPropertiesSet() {
    initHandlerMethods();
}
复制代码

</pre>

调用initHandlerMethods方法,那么initHandlerMethods里面干了什么事情呢?对该方法逐步分析!

<pre style="box-sizing: border-box; outline: none; margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: couriernew, courier, monospace; vertical-align: baseline; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

/**
 * Scan beans in the ApplicationContext, detect and register handler methods.
 * @see #getCandidateBeanNames()
 * @see #processCandidateBean
 * @see #handlerMethodsInitialized
 */
protected void initHandlerMethods() {
    for (String beanName : getCandidateBeanNames()) {
        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
            processCandidateBean(beanName);
        }
    }
    handlerMethodsInitialized(getHandlerMethods());
}

</pre>

首先 getCandidateBeanNames() 方法,我们看它的定义

<pre style="box-sizing: border-box; outline: none; margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: couriernew, courier, monospace; vertical-align: baseline; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

/**
 * Determine the names of candidate beans in the application context.
 * @since 5.1
 * @see #setDetectHandlerMethodsInAncestorContexts
 * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors
 */
protected String[] getCandidateBeanNames() {
    return (this.detectHandlerMethodsInAncestorContexts ?
            BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
            obtainApplicationContext().getBeanNamesForType(Object.class));
}

</pre>

initHandlerMethods() 获取到所有的bean之后然后循环遍历,我们将目光聚集在循环体内部的processCandidateBean方法

<pre style="box-sizing: border-box; outline: none; margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: couriernew, courier, monospace; vertical-align: baseline; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

protected void processCandidateBean(String beanName) {
    Class<?> beanType = null;
    try {
        beanType = obtainApplicationContext().getType(beanName);
    }
    catch (Throwable ex) {
        // An unresolvable bean type, probably from a lazy bean - let's ignore it.
        if (logger.isTraceEnabled()) {
            logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
        }
    }
    if (beanType != null && isHandler(beanType)) {
        detectHandlerMethods(beanName);
    }
}

</pre>

<pre style="box-sizing: border-box; outline: none; margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: couriernew, courier, monospace; vertical-align: baseline; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

@Override
protected boolean isHandler(Class<?> beanType) {
    return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
    AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

</pre>

<pre style="box-sizing: border-box; outline: none; margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: couriernew, courier, monospace; vertical-align: baseline; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

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

</pre>

内部该段逻辑可以遍历某个类下所有的方法

image

<pre style="box-sizing: border-box; outline: none; margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: couriernew, courier, monospace; vertical-align: baseline; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

 设置了该方法 的映射路径,方法对象,方法参数,设置的方法请求头,消费类型,可接受类型,映射名称等信息封装成RequestMappingInfo对象返回!

</pre>

image

会发现,我们方法上标注的 url会和前面读取的该方法的定义绑定在一个叫做 urlLookup的方法里面,请大家记住这个方法,这个方法对我们理解SpringMvc的处理逻辑有大用处!

3.请求获取逻辑源码解析

现在,整个工程所有对应的@requestMapping的方法已经被缓存,以该方法为例子!

<pre style="box-sizing: border-box; outline: none; margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: couriernew, courier, monospace; vertical-align: baseline; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

@RestController
public class TestController {

    @RequestMapping("test")
    public String test(){
        return "success";
    }
}

</pre>

现在在urlLookup属性里面就有一个 key为test,value为test()方法详细定义的 k:v键值对:v:

我们看下下面这个类图,DispatcherServlet这个关键的中央类,实际上是Servlet的子类,熟悉Servlet的同学都知道,之前在做Servlet开发的时候,所有的请求经过配置后都会被内部的doget和dopost方法拦截,至此SpringMvc为什么能够拦截URL也就不难分析了,拦截到url后,进入如下的流程调用链!

image

请求经由 org.springframework.web.servlet.FrameworkServlet#doGet捕获,委托给org.springframework.web.servlet.FrameworkServlet#processRequest方法,最后在调用org.springframework.web.servlet.DispatcherServlet#doService来处理真正的逻辑!

我们看一下这个方法里面的一些主要逻辑吧!

org.springframework.web.servlet.DispatcherServlet#doDispatch调用org.springframework.web.servlet.DispatcherServlet#getHandler方法,再次调用org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler经由org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal方法的org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod的org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#getMappingsByUrl

讲过这么长的调用链是不是懵了,此时我们终于看到了正主!

<pre style="box-sizing: border-box; outline: none; margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: couriernew, courier, monospace; vertical-align: baseline; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

/**
 * Return matches for the given URL path. Not thread-safe.
 * @see #acquireReadLock()
 */
@Nullable
public List<T> getMappingsByUrl(String urlPath) {
    return this.urlLookup.get(urlPath);
}

</pre>

这段代码是不是熟悉?这就是我们Spring容器在初始化的时候将url和方法定义放置的那个属性,现在Spring容器经由DispatcherServlet拦截请求后又重新找到该方法,并且返回!此时就完成了MVC流程图里面的HandlerMapping处理映射器的部分!

本章关于请求映射器的源码分析到这也就结束了,后续作者会将处理适配器,处理器,视图解析器一一讲明白,其实后续的逻辑也就很简单了,简单来说,拿到方法后反射执行该方法(不一定,一般场景是这样),然后拿到返回值,判断是否有@responseBody注解,判断是否需要转换成json,再通过write写回到页面!大致流程就是这样,详细过程作者后续会写!

经过今天的流程分析,你能否基于Servlet写一个属于自己的SpringMvc呢?

上一篇 下一篇

猜你喜欢

热点阅读