03-SpringMVC初始化HandlerAdapters过程分析

上篇文章中我们看到了Spring官方对DispatcherServlet的几个特殊Bean的介绍,其中第二个就是HandlerAdapter,它是DispatcherServlet调用handler的关键,因为它会对DispatcherServlet屏蔽细节实现,让DispatcherServlet只关心调用结果而不需要去关心调用细节(例如参数类型转换及封装等)。下面我们来分析HandlerAdapter的初始化过程
我们已经知道DispatcherServlet有一个默认配置文件,里面配置了各种策略。对于HandlerAdapter来说存在3个默认配置,他们分别是

  • HttpRequestHandlerAdapter 这个处理器是用来处理静态资源的
  • SimpleControllerHandlerAdapter 这个处理器是用来处理在实现了Controller接口或者AbstractController等Spring的默认实现类的子类
  • RequestMappingHandlerAdapter 这个处理器用来处理@RequestMapping注解的处理器,也是最常用,用的最多的一个,我们今天会着重分析这个
    DispatcherServlet在初始化几个策略时遇到使用默认策略均会调用createDefaultStrategy方法,最终通过IOC容器创建对象并执行回调。通过RequestMappingHandlerAdapter的类关系图我们可以看出RequestMappingHandlerAdapter同样实现了InitializingBean接口,因此我们关注的重点一个是在实例化阶段,一个是在回调InitializingBean接口方法阶段。
    首先我们来看实例化阶段
// 祖先类中会设置allowHeader为除了Trace外所有的HttpMethod但并未设置supportedMethods
public WebContentGenerator(boolean restrictDefaultSupportedMethods) {
        if (restrictDefaultSupportedMethods) {
            this.supportedMethods = new LinkedHashSet<>(4);
            this.supportedMethods.add(METHOD_GET);
            this.supportedMethods.add(METHOD_HEAD);
            this.supportedMethods.add(METHOD_POST);
        }
        initAllowHeader();
}

private void initAllowHeader() {
        Collection<String> allowedMethods;
        // 这里配置allowedMethods
        if (this.supportedMethods == null) {
            allowedMethods = new ArrayList<>(HttpMethod.values().length - 1);
            for (HttpMethod method : HttpMethod.values()) {
                if (method != HttpMethod.TRACE) {
                    allowedMethods.add(method.name());
                }
            }
        }
        else if (this.supportedMethods.contains(HttpMethod.OPTIONS.name())) {
            allowedMethods = this.supportedMethods;
        }
        else {
            allowedMethods = new ArrayList<>(this.supportedMethods);
            allowedMethods.add(HttpMethod.OPTIONS.name());

        }
        this.allowHeader = StringUtils.collectionToCommaDelimitedString(allowedMethods);
    }

// 在自身的构造器中添加了StringHttpMessageConverter、ByteArrayHttpMessageConverter、
// SourceHttpMessageConverter、AllEncompassingFormHttpMessageConverter
public RequestMappingHandlerAdapter() {
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

        this.messageConverters = new ArrayList<>(4);
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(stringHttpMessageConverter);
        try {
            this.messageConverters.add(new SourceHttpMessageConverter<>());
        }
        catch (Error err) {
            // Ignore when no TransformerFactory implementation is available
        }
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    }

上述代码中初始化了4种消息转换器,针对不同的content-type,设置不同的消息转换器

  • StringHttpMessageConverter 支持text/plain或所有/的请求类型
  • ByteArrayHttpMessageConverter 支持application/octet-stream或所有/的请求类型
  • SourceHttpMessageConverter 支持text/xml、application/xml、application/*-xml这三种类型的请求
  • AllEncompassingFormHttpMessageConverter 支持application/x-www-form-urlencoded的读写(file upload)和multipart/form-data格式的写操作(RestTemplate或WebClient中发送数据)。同时会根据classpath是否包含class文件来决定是否添加对xml和json的处理。
    /**
     * jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
     * jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
                        ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
     * jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
     * jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
     * gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
     * jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
     */
    public AllEncompassingFormHttpMessageConverter() {
        try {
            addPartConverter(new SourceHttpMessageConverter<>());
        }
        catch (Error err) {
            // Ignore when no TransformerFactory implementation is available
        }

        if (jaxb2Present && !jackson2XmlPresent) {
            addPartConverter(new Jaxb2RootElementHttpMessageConverter());
        }

        if (jackson2Present) {
            addPartConverter(new MappingJackson2HttpMessageConverter());
        }
        else if (gsonPresent) {
            addPartConverter(new GsonHttpMessageConverter());
        }
        else if (jsonbPresent) {
            addPartConverter(new JsonbHttpMessageConverter());
        }

        if (jackson2XmlPresent) {
            addPartConverter(new MappingJackson2XmlHttpMessageConverter());
        }

        if (jackson2SmilePresent) {
            addPartConverter(new MappingJackson2SmileHttpMessageConverter());
        }
    }

实例化阶段主要就做了这些操作,后面在分析DispatcherServlet处理过程时详细分析各转换器的作用。
下面我们再来关注初始化后的回调,开始之前我们先来看两张图


RequestMethod.png
ReturnValue.png

以上两张图来自Spring官方文档,介绍了SpringMVC的Controller中支持的请求参数类型和返回值类型。其中我们眼熟的@RequestParam、@PathVariable、@RequestBody、Map、Model、ServletRequest、ServletResponse、@ResponseBody、ModelAndView等等。下面就会针对上述的类型注册相关解析器或处理器。

    @Override
    public void afterPropertiesSet() {
        // Do this first, it may add ResponseBody advice beans
        // 在这个方法内会获取当前IOC容器中所有注有@ControllerAdvice的Bean,将他们包装成ControllerAdviceBean。
        // 并将他们分条件缓存。条件包括注有@InitBinder的方法,注有@ModelAttribute但没有@RequestMapping的方法。
        // 还会判断ControllerAdviceBean上是否注有RequestBodyAdvice或ResponseBodyAdvice,若存在也会缓存
        initControllerAdviceCache();
        
        // 这里会初始化参数解析器(包括内建解析器和通过setCustomArgumentResolvers方法自定义的解析器)
        if (this.argumentResolvers == null) {
            List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
            this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
        }
        // 这里会初始化一系列解析@InitBinder的解析器
        if (this.initBinderArgumentResolvers == null) {
            List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
            this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
        }
        // 这里会初始化返回值处理器()包括内建处理器和通过setReturnValueHandlers自定义的处理器)
        if (this.returnValueHandlers == null) {
            List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
            this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
        }
    }

至此初始化过程基本结束,后面的异常处理、视图解析、本地化、样式、文件上传和请求转发处理我们以后在分析,下一篇我们会开始分析DispatcherServlet的处理过程。
在查看官方文档的时候发现Spring现在推荐使用MVC Config作为最佳入口,因为它申明了必须的Special Bean同时提供要给高等级的自定义配置回调API。


WebMVCConfig.png

只有无法找到MVC Config时才会使用DispatcherServlet.properties进行默认配置。后面有机会我们会重新分析MVC Config方式下的操作流程。

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

推荐阅读更多精彩内容