源码跟踪-springmvc(一):DispatcherSevlet

背景

日常coding中,要解决项目当中遇到的问题,难免会需要写一个Converter,aspect,或者扩展一个MediaType等等。这时候需要写一个侵入性小的扩展,就需要了解源码。我找了很多博客文章,甚至看了《看透Spring MVC:源代码分析与实践》,写的很好,但是视角都是从整体框架出发,大而全,而我仅仅只是想解决当前的问题,所以我以代码跟踪的视角记录下这篇文章,免得下次忘了还要重新跟踪源码,直接过来看就好了。

目的

从源码中提取可能用到的工具,特别是标注好注意事项,下次扩展可以查阅。

准备

  1. springboot 2.1.1.RELEASE的demo
  2. pom依赖
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
  1. 示例类Employee,省略getter/setter
public class Employee {

    private Long id;
    private String name;
    private Integer age;
    private Date birthday;
    private Date createTime;
}
  1. Controller示例类
@RestController
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @PostMapping("/employee")
    public ResponseEntity<Employee> insert(Employee employee){
        employee.setId(1L);
        employee.setCreateTime(new Date());
        return ResponseEntity.ok(employee);
    }

}

断点

如图上断点



请求参数如图



ALT+左键点击employee检查参数

FrameworkServlet

  1. 我们在Debugger视图的Frames里从底部往上,找到第一个属于spring-webmvc包的类,FrameworkServlet
  2. 通过查看源码,我们GET到第一个工具类,HttpMethod,这个枚举类包含了所有的http方法。
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);
        }
    }
  1. 在Frames视图继续向上推,可以发现从 HttpServlet 转了一遭到了 FrameworkServlet.doPost ,然后来到了 FrameworkServlet.processRequest (删减版)
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // (1)
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        LocaleContext localeContext = buildLocaleContext(request);

        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

        initContextHolders(request, localeContext, requestAttributes);

        try {
            // (2)
            doService(request, response);
        }
        catch (ServletException | IOException ex) {
        }
        catch (Throwable ex) {
        }

        finally {
            resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                requestAttributes.requestCompleted();
            }
            // (3)
            publishRequestHandledEvent(request, response, startTime, failureCause);
        }
    }

看得出来,做了三件事

  1. ContextHolder的设置和重置
    具体见:源码跟踪-springmvc(二):LocaleContextHolder和RequestContextHolder
  2. 执行doService方法,也是真正执行handler的方法。
  3. 执行了publishRequestHandledEvent,代码如下
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()));
        }
    }

我们知道webApplicationContext继承了ApplicationEventPublisher,拥有了发布事件的能力,我把发布的事件打印出来看一下

@EventListener
    public void printServletRequestHandledEvent(ServletRequestHandledEvent event){
        System.out.println(event);
    }

打印结果如下

ServletRequestHandledEvent: url=[/employee]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[52ms]; status=[OK]

如果发现打印的内容满足需要,我们就不需要再写个aop用来记录日志啦。

DispatcherSevlet

  1. 现在进入了DispatcherSevlet.doService方法(删减版)
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        Map<String, Object> attributesSnapshot = null;
        if (WebUtils.isIncludeRequest(request)) {
            attributesSnapshot = new HashMap<>();
            Enumeration<?> attrNames = request.getAttributeNames();
            while (attrNames.hasMoreElements()) {
                String attrName = (String) attrNames.nextElement();
                if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
                    attributesSnapshot.put(attrName, request.getAttribute(attrName));
                }
            }
        }

        request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());

        try {
            doDispatch(request, response);
        }
        finally {
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
                // Restore the original attribute snapshot, in case of an include.
                if (attributesSnapshot != null) {
                    restoreAttributesAfterInclude(request, attributesSnapshot);
                }
            }
        }
    }

这里有两件事

  1. 如果满足一定的条件(WebUtils.isIncludeRequest(request)),会把request的attributes做一份快照备份(attributesSnapshot),执行完handler后还原备份(restoreAttributesAfterInclude(request, attributesSnapshot))。但是这里如果没有成立,也就没有执行,就先不管。但是很明显,这个手法和上面的ContextHolder如出一辙。这时候其实能够象出来,这样做的目的是为了安全。
  2. 在request中设置了一堆的attributes,有一个特别显眼,request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());,这个时候我们又get到一个获取webApplicationContext的办法。
WebApplicationContext wac = (WebApplicationContext) request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
  1. 进入了DispatcherSevlet.doDispatch方法(删减版)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ....
        try {
//...
            try {
                // (1)
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null || mappedHandler.getHandler() == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // (2)
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // (3)
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                // (4)
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // (5)
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                // (6)
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            catch (Throwable err) {
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }
            // (7)
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
            // (7)
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Throwable err) {
            // (7)
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                    new NestedServletException("Handler processing failed", err));
        }
    }

这里非常重要了,对应代码中的注释,解释如下

  1. 获取到mappedHandler,其实也就HandlerExecutionChain
    具体见源码跟踪-springmvc(三):RequestMappingHandlerMapping
  2. 获取处理器适配器,就是RequestMappingHandlerAdapter
  3. http协议中的缓存实现,可以看Spring mvc HTTP协议之缓存机制
  4. 分别调用mappedHandler中的三个拦截器的preHandle方法
  5. 执行真正的handler
    具体见:源码跟踪-springmvc(四):RequestMappingHandlerAdapter
  6. 执行拦截器的postHandle方法
  7. 执行拦截器的afterCompletion方法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容