SpringMVC 源码分析之 FrameworkServlet

2021-03-23  本文已影响0人  _江南一点雨

前面和小伙伴们聊了 SpringMVC 的初始化流程,相信大家对于 SpringMVC 的初始化过程都有一个基本认知了,今天我们就来看看当一个请求到达后,它的执行流程是什么样的?当然这个流程比较长,松哥这里可能会分两篇文章来和大家分享。

很多小伙伴都知道 SpringMVC 的核心是 DispatcherServlet,而 DispatcherServlet 的父类就是 FrameworkServlet,因此我们先来看看 FrameworkServlet,这有助于我们理解 DispatcherServlet。

1.FrameworkServlet

FrameworkServlet 继承自 HttpServletBean,而 HttpServletBean 继承自 HttpServlet,HttpServlet 就是 JavaEE 里边的东西了,这里我们不做讨论,从 HttpServletBean 开始就是框架的东西了,但是 HttpServletBean 比较特殊,它的特殊在于它没有进行任何的请求处理,只是参与了一些初始化的操作,这些比较简单,而且我们在上篇文章中也已经分析过了,所以这里我们对 HttpServletBean 不做分析,就直接从它的子类 FrameworkServlet 开始看起。

image

和所有的 Servlet 一样,FrameworkServlet 对请求的处理也是从 service 方法开始,我们先来看看该方法 FrameworkServlet#service:

@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
    if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
        processRequest(request, response);
    }
    else {
        super.service(request, response);
    }
}

可以看到,在该方法中,首先获取到当前请求方法,然后对 patch 请求额外关照了下,其他类型的请求统统都是 super.service 进行处理。

然而在 HttpServlet 中并未对 doGet、doPost 等请求进行实质性处理,所以 FrameworkServlet 中还重写了各种请求对应的方法,如 doDelete、doGet、doOptions、doPost、doPut、doTrace 等,其实就是除了 doHead 之外的其他方法都重写了。

我们先来看看 doDelete、doGet、doPost 以及 doPut 四个方法:

@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    processRequest(request, response);
}
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    processRequest(request, response);
}
@Override
protected final void doPut(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    processRequest(request, response);
}
@Override
protected final void doDelete(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    processRequest(request, response);
}

可以看到,这里又把请求交给 processRequest 去处理了,在 processRequest 方法中则会进一步调用到 doService,对不同类型的请求分类处理。

doOptions 和 doTrace 则稍微有些差异,如下:

@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
        processRequest(request, response);
        if (response.containsHeader("Allow")) {
            return;
        }
    }
    super.doOptions(request, new HttpServletResponseWrapper(response) {
        @Override
        public void setHeader(String name, String value) {
            if ("Allow".equals(name)) {
                value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();
            }
            super.setHeader(name, value);
        }
    });
}
@Override
protected void doTrace(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    if (this.dispatchTraceRequest) {
        processRequest(request, response);
        if ("message/http".equals(response.getContentType())) {
            return;
        }
    }
    super.doTrace(request, response);
}

可以看到这两个方法的处理多了一层逻辑,就是去选择是在当前方法中处理对应的请求还是交给父类去处理,由于 dispatchOptionsRequest 和 dispatchTraceRequest 变量默认都是 false,因此默认情况下,这两种类型的请求都是交给了父类去处理。

2.processRequest

我们再来看 processRequest,这算是 FrameworkServlet 的核心方法了:

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = buildLocaleContext(request);
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
    initContextHolders(request, localeContext, requestAttributes);
    try {
        doService(request, response);
    }
    catch (ServletException | IOException ex) {
        failureCause = ex;
        throw ex;
    }
    catch (Throwable ex) {
        failureCause = ex;
        throw new NestedServletException("Request processing failed", ex);
    }
    finally {
        resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }
        logResult(request, response, failureCause, asyncManager);
        publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}

这个方法虽然比较长,但是其实它的核心就是最中间的 doService 方法,以 doService 为界,我们可以将该方法的内容分为三部分:

  1. doService 之前主要是一些准备工作,准备工作主要干了两件事,第一件事就是从 LocaleContextHolder 和 RequestContextHolder 中分别获取它们原来保存的 LocaleContext 和 RequestAttributes 对象存起来,然后分别调用 buildLocaleContext 和 buildRequestAttributes 方法获取到当前请求的 LocaleContext 和 RequestAttributes 对象,再通过 initContextHolders 方法将当前请求的 LocaleContext 和 RequestAttributes 对象分别设置到 LocaleContextHolder 和 RequestContextHolder 对象中;第二件事则是获取到异步管理器并设置拦截器。
  2. 接下来就是 doService 方法,这是一个抽象方法,具体的实现在 DispatcherServlet 中,这个松哥放到 DispatcherServlet 中再和大家分析。
  3. 第三部分就是 finally 中,这个里边干了两件事:第一件事就是将 LocaleContextHolder 和 RequestContextHolder 中对应的对象恢复成原来的样子(参考第一步);第二件事就是通过 publishRequestHandledEvent 方法发布一个 ServletRequestHandledEvent 类型的消息。

经过上面的分析,大家发现,processRequest 其实主要做了两件事,第一件事就是对 LocaleContext 和 RequestAttributes 的处理,第二件事就是发布事件。我们对这两件事分别来研究。

2.1 LocaleContext 和 RequestAttributes

LocaleContext 和 RequestAttributes 都是接口,不同的是里边存放的对象不同。

2.1.1 LocaleContext

LocaleContext 里边存放着 Locale,也就是本地化信息,如果我们需要支持国际化,就会用到 Locale。

国际化的时候,如果我们需要用到 Locale 对象,第一反应就是从 HttpServletRequest 中获取,像下面这样:

Locale locale = req.getLocale();

但是大家知道,HttpServletRequest 只存在于 Controller 中,如果我们想要在 Service 层获取 HttpServletRequest,就得从 Controller 中传参数过来,这样就比较麻烦,特别是有的时候 Service 中相关方法都已经定义好了再去修改,就更头大了。

所以 SpringMVC 中还给我们提供了 LocaleContextHolder,这个工具就是用来保存当前请求的 LocaleContext 的。当大家看到 LocaleContextHolder 时不知道有没有觉得眼熟,松哥在之前的 Spring Security 系列教程中和大家聊过 SecurityContextHolder,这两个的原理基本一致,都是基于 ThreadLocal 来保存变量,进而确保不同线程之间互不干扰,对 ThreadLocal 不熟悉的小伙伴,可以看看松哥的 Spring Security 系列,之前有详细分析过(公号后台回复 ss)。

有了 LocaleContextHolder 之后,我们就可以在任何地方获取 Locale 了,例如在 Service 中我们可以通过如下方式获取 Locale:

Locale locale = LocaleContextHolder.getLocale();

上面这个 Locale 对象实际上就是从 LocaleContextHolder 中的 LocaleContext 里边取出来的。

需要注意的是,SpringMVC 中还有一个 LocaleResolver 解析器,所以前面 req.getLocale() 并不总是获取到 Locale 的值,这个松哥在以后的文章中再和小伙伴们细聊。

2.1.2 RequestAttributes

RequestAttributes 是一个接口,这个接口可以用来 get/set/remove 某一个属性。

RequestAttributes 有诸多实现类,默认使用的是 ServletRequestAttributes,通过 ServletRequestAttributes,我们可以 getRequest、getResponse 以及 getSession。

在 ServletRequestAttributes 的具体实现中,会通过 scope 参数判断操作 request 还是操作 session(如果小伙伴们不记得 Spring 中的作用域问题,可以公号后台回复 spring,看看松哥录制的免费的 Spring 入门教程,里边有讲),我们来看一下 ServletRequestAttributes#setAttribute 方法(get/remove 方法执行逻辑类似):

public void setAttribute(String name, Object value, int scope) {
    if (scope == 0) {
        if (!this.isRequestActive()) {
            throw new IllegalStateException("Cannot set request attribute - request is not active anymore!");
        }
        this.request.setAttribute(name, value);
    } else {
        HttpSession session = this.obtainSession();
        this.sessionAttributesToUpdate.remove(name);
        session.setAttribute(name, value);
    }
}

可以看到,这里会先判断 scope,scope 为 0 就操作 request,scope 为 1 就操作 session。如果操作的是 request,则需要首先通过 isRequestActive 方法判断当前 request 是否执行完毕,如果执行完毕,就不可以再对其进行其他操作了(当执行了 finally 代码块中的 requestAttributes.requestCompleted 方法后,isRequestActive 就会返回 false)。

和 LocaleContext 类似,RequestAttributes 被保存在 RequestContextHolder 中,RequestContextHolder 的原理也和 SecurityContextHolder 类似,这里不再赘述。

看了上面的讲解,大家应该发现了,在 SpringMVC 中,如果我们需要在 Controller 之外的其他地方使用 request、response 以及 session,其实不用每次都从 Controller 中传递 request、response 以及 session 等对象,我们完全可以直接通过 RequestContextHolder 来获取,像下面这样:

ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
HttpServletResponse response = servletRequestAttributes.getResponse();

是不是非常 easy!

2.2 事件发布

最后就是 processRequest 方法中的事件发布了。

在 finally 代码块中会调用 publishRequestHandledEvent 方法发送一个 ServletRequestHandledEvent 类型的事件,具体发送代码如下:

private void publishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response,
        long startTime, @Nullable Throwable failureCause) {
    if (this.publishEvents && this.webApplicationContext != null) {
        // Whether or not we succeeded, publish an event.
        long processingTime = System.currentTimeMillis() - startTime;
        this.webApplicationContext.publishEvent(
                new ServletRequestHandledEvent(this,
                        request.getRequestURI(), request.getRemoteAddr(),
                        request.getMethod(), getServletConfig().getServletName(),
                        WebUtils.getSessionId(request), getUsernameForRequest(request),
                        processingTime, failureCause, response.getStatus()));
    }
}

可以看到,事件的发送需要 publishEvents 为 true,而该变量默认就是 true。如果需要修改该变量的值,可以在 web.xml 中配置 DispatcherServlet 时,通过 init-param 节点顺便配置一下该变量的值。正常情况下,这个事件总是会被发送出去,如果项目有需要,我们可以监听该事件,如下:

@Component
public class ServletRequestHandleListener implements ApplicationListener<ServletRequestHandledEvent> {
    @Override
    public void onApplicationEvent(ServletRequestHandledEvent servletRequestHandledEvent) {
        System.out.println("请求执行完毕-"+servletRequestHandledEvent.getRequestUrl());
    }
}

当一个请求执行完毕时,该事件就会被触发。

3.小结

这篇文章主要和小伙伴们分享了 SpringMVC 中 DispatcherServlet 的父类 FrameworkServlet,FrameworkServlet 的功能其实比较简单,主要就是在 service 方法中增加了对 PATCH 的处理,然后其他类型的请求都被归类到 processRequest 方法中进行统一处理,processRequest 方法则又分了三部分,首先是对 LocaleContext 和 RequestAttributes 的处理,然后执行 doService,最后在 finally 代码块中对 LocaleContext 和 RequestAttributes 属性进行复原,同时发布一个请求结束的事件。

doService 是重头戏,松哥将在下篇文章中和大家分享。好啦,今天就先和小伙伴们聊这么多~

上一篇下一篇

猜你喜欢

热点阅读