zoukankan      html  css  js  c++  java
  • SpringMVC-嵌套对象传参及原理解析

    引子

    在涉及前后端交互的 Java 应用中,SpringMVC 可以说是很流行的一种框架。那么在 SpringMVC 中,如何将较复杂的嵌套对象从前端传给后端呢?可以使用注解 @RequestBody 。 @RequestBody 的实现原理是:根据指定的前端传参类型及 Media Type 来选择适当的 HttpMessageConverter 来进行参数转换。

    传参

    后端接收:

    @RequestMapping(value = "/save")
    @ResponseBody
    public BaseResult save(@RequestBody BookInfo bookInfo) {
        Assert.notNull(bookInfo, "商品对象不能为空");
        Assert.notNull(bookInfo.getGoods().getGoodsId(), "商品ID不能为空");
    
        complete(bookInfo);
        boolean isSaved = goodsSnapshotService.save(bookInfo);
        BookSaveResponse bookSaveResponse = new BookSaveResponse(bookInfo.getOrder().getOrderNo(), bookInfo.getGoods().getGoodsId());
        return isSaved ? BaseResult.succ(bookSaveResponse): BaseResult.failed(Errors.BookError);
    }
    
    public class BookInfo {
    
        /** 下单的商品信息 */
        private GoodsInfo goods;
    
        /** 下单的订单信息 */
        private Order order;
    }
    
    public class GoodsInfo {
    
        /** 商品ID */
        private Long goodsId;
    
        /** 店铺ID */
        private Long shopId;
    
        /** 商品标题 */
        private String title;
    
        /** 商品描述 */
        private String desc;
    
        /** 商品服务 keys */
        private String serviceKeys;
    
        /** 商品规格选择 */
        private String choice;
    
        /** 商品关联的订单号 */
        private String orderNo;
    }
    
    public class Order {
    
        /** 订单号 */
        private String orderNo;
    
        /** 店铺ID */
        private Long shopId;
    
        /** 下单时间 */
        private Long bookTime;
    
        /** 下单人ID */
        private Long userId;
    
        /** 是否货到付款 */
        private Boolean isCodPay;
    
        /** 是否担保交易 */
        private Boolean isSecuredOrder;
    
        /** 是否有线下门店 */
        private Boolean hasRetailShop;
    
        /** 配送方式 0 快递 1 自提 2 同城送 */
        private DeliveryType deliveryType;
    
        /** 订单金额, 分为单位 */
        private Long price;
    
        /** 快递配送金额 */
        private Long expressFee;
    
        /** 同城配送起送金额 */
        private Long localDeliveryBasePrice;
    
        /** 同城配送金额 */
        private Long localDeliveryPrice;
    
        /** 订单的服务 keys */
        private List<String> keys;
    }
    

    前端传参:

    
        var bookInfo = {
           'goods': {
                'shopId': shopId,
                'goodsId': goodsId,
                'price': priceNum,
                'title': title,
                'desc': desc,
                'serviceKeys' : serviceKeys,
                'choice': choice
            },
            'order': {
                'shopId': shopId,
                'userId': userId,
                'deliveryType': deliveryType,
                'price': priceNum,
                'isCodPay' : isCodPay,
            }
        };
    
        var jqXHR = jQuery.ajax({
            dataType: "json",
            contentType: "application/json; charset=utf-8",
            url: 'http://localhost:8080/api/goodsnapshot/save',
            data: JSON.stringify(bookInfo),
            timeout: 90000,
            type: 'POST'
        });
    
    

    原理

    @RequestBody 的作用就是做参数校验和参数转换,将前端的 JSON 字符串转换成后端给定的 Java 对象。那么,它的参数转换功能是如何实现的呢? 要回答这个问题,就要找到这个注解的实现类。

    找到入口方法

    可以在 IDEA 搜索类 RequestBody,得到与之相关的几个类如下图所示:

    在可能实现类的 public 方法入口打上断点,然后单步调试。可以找到入口方法是 RequestResponseBodyMethodProcessor.resolveArgument 。这个方法进一步调用了方法 AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters , 该方法会拿到若干个 HttpMessageConverter 的实现类(如下图所示),判断其中是否有可以处理请求对象 InputMessage 的类。只要任一能处理即可。

    AbstractMessageConverterMethodArgumentResolver.java

    
            Object inputMessage;
            try {
                inputMessage = new AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage(inputMessage);
                Iterator var10 = this.messageConverters.iterator();
    
                while(var10.hasNext()) {
                    HttpMessageConverter<?> converter = (HttpMessageConverter)var10.next();
                    Class<HttpMessageConverter<?>> converterType = converter.getClass();
                    if (converter instanceof GenericHttpMessageConverter) {
                        GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter)converter;
                        if (genericConverter.canRead(targetType, contextClass, contentType)) {
                            if (this.logger.isDebugEnabled()) {
                                this.logger.debug("Read [" + targetType + "] as "" + contentType + "" with [" + converter + "]");
                            }
    
                            if (((HttpInputMessage)inputMessage).getBody() != null) {     // HttpMessageConverter 进行参数转换时有切面
                                inputMessage = this.getAdvice().beforeBodyRead((HttpInputMessage)inputMessage, parameter, targetType, converterType);
                                body = genericConverter.read(targetType, contextClass, (HttpInputMessage)inputMessage);
                                body = this.getAdvice().afterBodyRead(body, (HttpInputMessage)inputMessage, parameter, targetType, converterType);
                            } else {
                                body = this.getAdvice().handleEmptyBody((Object)null, (HttpInputMessage)inputMessage, parameter, targetType, converterType);
                            }
                            break;
                        }
                    } else if (targetClass != null && converter.canRead(targetClass, contentType)) {
                        if (this.logger.isDebugEnabled()) {
                            this.logger.debug("Read [" + targetType + "] as "" + contentType + "" with [" + converter + "]");
                        }
    
                        if (((HttpInputMessage)inputMessage).getBody() != null) {   // HttpMessageConverter 进行参数转换时有切面
                            inputMessage = this.getAdvice().beforeBodyRead((HttpInputMessage)inputMessage, parameter, targetType, converterType);  
                            body = converter.read(targetClass, (HttpInputMessage)inputMessage);
                            body = this.getAdvice().afterBodyRead(body, (HttpInputMessage)inputMessage, parameter, targetType, converterType);
                        } else {
                            body = this.getAdvice().handleEmptyBody((Object)null, (HttpInputMessage)inputMessage, parameter, targetType, converterType);
                        }
                        break;
                    }
                }
    
    

    找到参数转换类

    如何判断 HttpMessageConverter 是否能够处理 InputMessage 呢?要满足两个基本条件:

    • 能够处理请求参数类,比如,这里是 BookInfo ;
    • 要能处理给定的 Content-type ,比如,这里是 JSON , UTF-8。

    AbstractHttpMessageConverter.java

    
        public boolean canRead(Class<?> clazz, MediaType mediaType) {
            return this.supports(clazz) && this.canRead(mediaType);
        }
    
        protected boolean canRead(MediaType mediaType) {
            if (mediaType == null) {
                return true;
            } else {
                Iterator var2 = this.getSupportedMediaTypes().iterator();
    
                MediaType supportedMediaType;
                do {
                    if (!var2.hasNext()) {
                        return false;
                    }
    
                    supportedMediaType = (MediaType)var2.next();
                } while(!supportedMediaType.includes(mediaType));
    
                return true;
            }
        }
    
    

    可以逐一查看上述截图中的 HttpMessageConverter 实现类(支持的 Media Type 如截图所示):

    • ByteArrayHttpMessageConverter: 支持字节数组的处理;
    • StringHttpMessageConverter: 支持字符串的处理;
    • ResourceHttpMessageConverter: 支持 Resource 类型的处理;
    • SourceHttpMessageConverter: 支持 DOMSource, SAXSource, StAXSource, StreamSource, Source 类型;
    • AllEncompassingFormHttpMessageConverter: 支持表单的处理;
    • MappingJackson2HttpMessageConverter: 支持 JSON 字符串的处理;
    • Jaxb2RootElementHttpMessageConverter:参数类带有 XmlRootElement 或 XmlType 。

    最终发现,能够处理 JSON 字符串及 JSON media 的是类 MappingJackson2HttpMessageConverter 。 注意到,在这个类处理 InputMessage 的时候,还有一个切面。这个切面的实现类是 RequestResponseBodyAdviceChain ,里面应用了责任链设计模式来处理请求 InputMessage。对应于 @RequestBody 的切面类是 JsonViewRequestBodyAdvice, 仅在参数上有 @JsonView 注解时才处理。

    向前追溯

    为什么入口类是 RequestResponseBodyMethodProcessor 呢?可以看看方法 RequestResponseBodyMethodProcessor.resolveArgument 的调用者。进一步追溯可知:

    • Spring 应用里的参数类(比如 BookInfo)对应于 Spring 框架里的 MethodParam ;

    • 在类 HandlerMethodArgumentResolverComposite (应用了组合设计模式)里面含有一个参数解析映射缓存 Map[MethodParam, HandlerMethodArgumentResolver] argumentResolverCache,可以根据指定的 MethodParam 拿到对应的 HandlerMethodArgumentResolver 实现类,即 RequestResponseBodyMethodProcessor。

    argumentResolverCache 的内容是获取参数解析类的时候动态添加进去的,它依赖于一个实例成员 List[HandlerMethodArgumentResolver] argumentResolvers ,而 argumentResolvers 是在方法 RequestMappingHandlerAdapter.getDefaultArgumentResolvers 里添加的,该方法在类 RequestMappingHandlerAdapter 初始化完成后调用。

    HandlerMethodArgumentResolverComposite.java

    
             private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    		if (result == null) {
    			for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
    				if (logger.isTraceEnabled()) {
    					logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +
    							parameter.getGenericParameterType() + "]");
    				}
    				if (methodArgumentResolver.supportsParameter(parameter)) {
    					result = methodArgumentResolver;
    					this.argumentResolverCache.put(parameter, result);
    					break;
    				}
    			}
    		}
    		return result;
           	}
    
    

    看来,参数类解析相关的初始化的奥秘在类 RequestMappingHandlerAdapter 里。

    RequestMappingHandlerAdapter.java

    
    	public void afterPropertiesSet() {
    		// Do this first, it may add ResponseBody advice beans
    		initControllerAdviceCache();
    
    		if (this.argumentResolvers == null) {
    			List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
    			this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    		}
    		if (this.initBinderArgumentResolvers == null) {
    			List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
    			this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    		}
    		if (this.returnValueHandlers == null) {
    			List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
    			this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
    		}
    	}
    
    

    请求的处理

    类 RequestMappingHandlerAdapter 既然负责初始化,那很可能也负责请求处理。可以在 RequestMappingHandlerAdapter 的 public 方法打断点,然后发请求。果然,经过 RequestMappingHandlerAdapter.invokeHandlerMethod 方法。再查看方法 invokeHandlerMethod 的调用,一直向前追溯调用链,最终,可以找到源头处,即: DispatcherServlet.doService 方法。 好了,可以在这里打个断点,一步步跟踪,看看一路经过了哪些类,数据是怎样的,这样,就可以理解 MVC 的整个请求处理流程。


    引申

    不同的参数注解,就对应不同的参数解析类。 比如:

    • @RequestParam => RequestParamMethodArgumentResolver & RequestParamMapMethodArgumentResolver
    • @PathVariable => PathVariableMethodArgumentResolver

    这些参数解析类,有两个主要方法:

    • supportsParameter : 支持解析怎样的参数形式;
    • resolveName : 具体地解析参数的实现。

    RequestParamMethodArgumentResolver

    RequestParamMethodArgumentResolver 适用的情况可以看它的 supportsParameter 方法:

    • @RequestPart MultipartFile
    • @RequestPart Part
    • @RequestParam + simple type

    后台代码如下:

    
    @RequestMapping(value = "/searchForSelect")
    @ResponseBody
    public Map<String, Object> searchForSelect(
        @RequestParam(value = "k", required = false) String title,
        @RequestParam(value = "page", defaultValue = "1") Integer page,
        @RequestParam(value = "rows", defaultValue = "10") Integer pageSize) {
        CreativeQuery query = buildCreativeQuery(title, page, pageSize);
        return searchForSelect2(query,
                                (q) -> creativeService.search(q),
                                (q) -> creativeService.count(q));
      }
    
    

    发送请求: http://localhost:8080/api/creatives//searchForSelect?page=2&rows=20 ,就会使用类 RequestParamMethodArgumentResolver 来解析参数,得到 page = 2, pageSize = 20。这个值是怎么拿到的呢 ? 是从 request.coyoteRequest.parameters.paramHashValues 里面取到的。


    RequestParamMapMethodArgumentResolver

    RequestParamMapMethodArgumentResolver 的实现方式与 RequestParamMethodArgumentResolver 相同,也是从 request.coyoteRequest.parameters.paramHashValues 里面取到的。所不同的是,它修饰的参数类型是 Map 。 如下代码所示:

    
    @RequestMapping(value = "/updateByMap")
    @ResponseBody
    public BaseResult updateByMap(@RequestParam  Map creative) {
        Assert.notNull(creative, "对象不能为空");
        Assert.notNull(creative.get("creativeId"), "创意ID不能为空");
        CreativeDO creativeObj = JSONObject.parseObject(JSONObject.toJSONString(creative), CreativeDO.class);
        creativeService.update(creativeObj);
        return BaseResult.succ("创意更新成功");
    }
    
    

    发送请求: http://localhost:8080/api/creatives//updateByMap?creativeId=2&content=newContent22&title=newtitle222 ,就会使用类 RequestParamMapMethodArgumentResolver 来解析参数,将 creative 设置为 Map["creativeId"=>2, "content"=>"newContent22", "title" => newtitle222] 。

    PathVariableMethodArgumentResolver

    适用于请求路径中含有可变参数的情形。比如 /get/{creativeId} 。 后台代码如下:

    
    @RequestMapping(value = "/get/{creativeId}")
    public String get(@PathVariable Long creativeId, ModelMap model) {
        Assert.notNull(creativeId, "请选择要查询的创意id");
        CreativeDO creativeObj = creativeService.get(creativeId);
        model.put("creative", creativeObj);
        return "/creative/detail";
    }
    
    

    发送请求:http://localhost:8080/api/creatives/get/1 ,使用类 PathVariableMethodArgumentResolver 来解析路径参数,得到请求参数 creativeId = 1 。这个 1 是怎么得到的呢 ? 是方法 PathVariableMethodArgumentResolver.resolveName 从 request.attributes 取出 key = “org.springframework.web.servlet.HandlerMapping.uriTemplateVariables” 的值拿到的。

    小结

    一切都在代码里。只要找到了入口,知道了套路,学会单步调试,类似问题解决起来就相对容易了。


  • 相关阅读:
    JavaScript中null、undefined有什么区别?
    div居中和内容居中的css属性设置
    jQuery中有哪些选择器?
    Javascript正则表达式使用方式有哪些?
    写一些常见的Javascript正则表达式?
    body中的onload事件和document.ready()有什么区别?
    MapReduce架构设计
    谁在用 Hadoop
    淘宝在数据处理领域的项目及开源产品介绍
    ★Java多线程编程总结 系列 转
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/14192871.html
Copyright © 2011-2022 走看看