Spring web请求controller puzzle

遇到一个非常奇怪的问题, 在浏览器中和PostMan中向后端发送请求, 得到的请求结果是不一样的

后端代码如下:

@Controller
@RequestMapping("/test")
@Slf4j
public class TestController {

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    @ResponseBody
    public Map<String, String> testMethod(@RequestParam("id") long id) {
        Map<String, String> rst = new HashMap<String, String>();
        rst.put("a", String.valueOf(id));
        rst.put("b", String.valueOf(RandomUtils.nextInt(20)));

        return rst;
    }

目的是希望通过jackson把返回数据格式化,

  • 在浏览器中请求得到的响应是:
<Map xmlns="">
    <a>1000</a>
    <b>1</b>
</Map>
  • 在postman中请求得到的响应是:
{
    "a": "1000",
    "b": "5"
}

看到两者请求返回数据的格式是不一样的, 需要对此进行探究一下

一. 首先从SpringMVC处理请求的流程说起

  1. Web服务启动的时候, 需要在sprinf-mvc context配置文中配置mvc:annotation-driven
<mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <!-- 将StringHttpMessageConverter的默认编码设为UTF-8 -->
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <constructor-arg value="UTF-8" />
            </bean>
            <!-- 将Jackson2HttpMessageConverter的默认格式化输出设为true -->
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="prettyPrint" value="false"/>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

SpringMVC中自带了自定义命名空间解析器

public class MvcNamespaceHandler extends NamespaceHandlerSupport {

    @Override
    public void init() {
        registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
        ........
    }
}

进入到AnnotationDrivenBeanDefinitionParser中会发现默认向bean容器注册了一个RequestMappingHandlerAdapter的beanDefinition

@Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        ...........

        RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
        handlerAdapterDef.setSource(source);
        handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
        handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
        handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters);
        addRequestBodyAdvice(handlerAdapterDef);
        addResponseBodyAdvice(handlerAdapterDef);

        if (element.hasAttribute("ignore-default-model-on-redirect")) {
            Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignore-default-model-on-redirect"));
            handlerAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel);
        }
        else if (element.hasAttribute("ignoreDefaultModelOnRedirect")) {
            // "ignoreDefaultModelOnRedirect" spelling is deprecated
            Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignoreDefaultModelOnRedirect"));
            handlerAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel);
        }

        if (argumentResolvers != null) {
            handlerAdapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers);
        }
        if (returnValueHandlers != null) {
            handlerAdapterDef.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers);
        }
        if (asyncTimeout != null) {
            handlerAdapterDef.getPropertyValues().add("asyncRequestTimeout", asyncTimeout);
        }
        if (asyncExecutor != null) {
            handlerAdapterDef.getPropertyValues().add("taskExecutor", asyncExecutor);
        }

        handlerAdapterDef.getPropertyValues().add("callableInterceptors", callableInterceptors);
        handlerAdapterDef.getPropertyValues().add("deferredResultInterceptors", deferredResultInterceptors);
        readerContext.getRegistry().registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME , handlerAdapterDef);

        ........
    }

springMVC的context加载的时候会调用preInitializationSingletons, 会初始化singleton的bean对象, 这时候会初始化RequestMappingHandlerAdapter, RequestMappingHandlerAdapter的初始化中会加载一些MessageConverter.

public RequestMappingHandlerAdapter() {
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

        this.messageConverters = new ArrayList<HttpMessageConverter<?>>(4);
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(stringHttpMessageConverter);
        this.messageConverters.add(new SourceHttpMessageConverter<Source>());
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    }

在AllEncompassingFormHttpMessageConverter中我们可以看到这样的逻辑

private static final boolean jaxb2Present =
            ClassUtils.isPresent("javax.xml.bind.Binder", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
private static final boolean jackson2Present =
            ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader()) &&
                    ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
private static final boolean jackson2XmlPresent =
            ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
private static final boolean gsonPresent =
            ClassUtils.isPresent("com.google.gson.Gson", AllEncompassingFormHttpMessageConverter.class.getClassLoader());

可以看到, spring会判断有没有相关jar包依赖决定要不要加载对应的messageConverter

  1. web服务收到外部请求, 会将该请求转发给DispatchServlet, DispatchServlet是一个实现Servlet接口的类, 是SpringMVC的前端分发servlet.

    外部请求被引导到processRequest -> doService -> doDispatch, doDispatch中的逻辑相对复杂

    • step1: 判断是不是multiPart类型的请求,
    • step2: 根据request信息寻找对应的handler, 如果找不到handler, response返回错误信息
    • step3: 寻找handlerAdapter
    • step4: 判断对Last-Modified的支持
    • step5: 调用拦截器的preHandle方法
    • step6: 调用handler.handle方法
    • step7: 执行拦截器的postHandle方法
    • step8: 处理页面跳转
  2. 具体分下一下2所述的流程

    • 外部请求被引导到processRequest函数中执行
      • 有个threadLocal用于配置localContext, 虽然我暂时也不知道这个玩意是干啥的
      • 调用doService
      • reset local context, 恢复为之前的localContext
    • doService -> doDispatch
    • doDispatch
      • 检查是不是multiPart的请求
      • getHandler, 获得HandlerExecutionChain
      • 调用getHandlerAdapter, 获得HandlerAdapter ha
      • 调用handler拦截器的preProcess
      • 调用ha.handler执行实际操作
      • 调用handler拦截器的postProcess
      • 处理dispatch结果
    • 获得HandlerExecutionChain流程
      • 根据系统获得的handlerMappings(一般有两个: RequestMappingHandlerMapping和BeanNameUrlHandlerMapping), 调用每个handler的getHandler方法
      • 调用HandlerMapping的getHandler方法获得HandlerExecutionChain
        • 获得HandlerMethod, 调用 lookUpHandlerMethod
          • 先根据请求路径(绝对路径)查询直接匹配的元素, 如果每一个RequestMappingInfo的各个条件(例如method条件, header条件等)都满足, 则添加到临时容器中
          • 若没有命中的, 则尝试查询所有的RequestMappingInfo最终尝试获取满足条件的RequestMappingInfo
          • 如果有多个满足条件的, 则会进行排序比较找到最满足条件的
      • 获得HandlerExecutionChain, 遍历adaptedInterceptors找到匹配请求路径的拦截器(拦截器可以指定excludePath和includePath, 如果请求路径match excludePath则不匹配, 若请求路径匹配任一includePath, 则匹配)
    • 获得HandlerAdapter的过程: 遍历handlerAdapter, 找到第一个能support handler的handlerAdapter
    • 调用HandlerAdapter的handle方法
      • 首先进行一些基础校验check, 以及是否需要session等一些校验
      • 调用invokeHandlerMethod
        • 调用invokeAndHandle: 首先反射调用函数, 获得请求结果
        • 调用returnValueHandlers对计算结果进行处理
          • selectHandler, 这个实际上是遍历系统的returnvalueHandlers, 然后判断returnValueHandler是不是支持返回结果的returnType, 最终会匹配RequestResponseBodyMethodProcessor(判断逻辑是外围类是不是有ResponseBody注解或者返回值所在的method有没有ResponseBody注解)
          • 调用上述返回的HandlerMethodReturnValueHandler的handleReturnValue函数, 该函数中会调用writeWithMessageConverters函数, 在这个函数中, 会根据请求的header获得mediaType, 然后不同的mediaType会匹配到不同的messageConverter上, 写的格式也不一样
        • 还记得上述的RequestMappingHandlerAdapter初始化中加载的一堆messageConverter?
HandlerAdapter是什么?
在HandlerMapping返回处理请求的Controller实例后,需要一个帮助定位具体请求方法的处理类,这个类就是HandlerAdapter.

HandlerAdapter是处理器适配器,Spring MVC通过HandlerAdapter来实际调用处理函数。例如Spring MVC自动注册的AnnotationMethodHandlerAdpater.

HandlerAdapter定义了如何处理请求的策略,通过请求url、请求Method和处理器的requestMapping定义,最终确定使用处理类的哪个方法来处理请求,并检查处理类相应处理方法的参数以及相关的Annotation配置,确定如何转换需要的参数传入调用方法,并最终调用返回ModelAndView。

DispatcherServlet中根据HandlerMapping找到对应的handler method后,首先检查当前工程中注册的所有可用的handlerAdapter,根据handlerAdapter中的supports方法找到可以使用的handlerAdapter。
通过调用handlerAdapter中的handler方法来处理及准备handler method的参数及annotation(这就是spring mvc如何将request中的参数变成handle method中的输入参数的地方),最终调用实际的handler method。

最终问题明确了, 浏览器请求的时候会默认添加一个content-type的header, 而postman并不会带上, 这样会导致最终根据header匹配到不同的messageConverter, 从而返回不同的格式

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

推荐阅读更多精彩内容