SpringMVC - @RequestJson之HandlerMethodArgumentResolver 从入门到青铜

JSON回顾,如果是新手,可以简单看看;如果老手勿喷,直接拉到下文从前言开始...

JSON简介

在企业当中,我们都知道为了前后端分离统一,使用开发原则中的“约定大于配置”的原则,甚至团队开发规范、开发编译环境等等也是要大家约定来执行的,提高各自的开发效率。而其中一个特别重要的约定就是前端后端中间彼此传输的数据一般情况都推荐使用JSON格式数据,原因有以下几点:

  1. JAVAScript Object Notation是一种轻量级的数据交换格式。
  2. 具有良好的可读和便于快速编写的特性。
  3. 业内主流技术为其提供了完整的解决方案(有点类似于正则表达式 ,获得了当今大部分语言的支持)。
  4. JSON采用兼容性很高的文本格式,同时也具备类似于C语言体系的行为。 – Json.org
  5. JSON作为数据是目前网络中主流的数据传输格式之一,应用十分广泛,说是使用率达到99%一点也不勉强。
  6. 数据格式比较简单, 易于读写, 格式都是压缩的, 占用带宽小。
  7. 易于解析这种语言, 客户端JavaScript可以简单的通过eval()进行JSON数据的读取。
  8. 因为JSON格式能够直接为服务器端代码使用, 大大简化了服务器端和客户端的代码开发量, 但是完成的任务不变, 且易于维护。

 

JSON支持的数据类型

JSON里面的数据是以一种键值对的方式存在,("key": "value")中值的类型可以是下面数据类型中的任意一种: 

  1. 数字(整数或浮点数) 
  2. 逻辑值(true 或 false) 
  3. 字符串(在双引号中) 
  4. 数组(在方括号中) 
  5. 函数 
  6. 对象(在大括号中) 
  7. null

 

JSON语法规则

1、基本语法(英文状态):

  • 大括号:{} 
  • 中括号:[] 
  • 逗号:,
  • 冒号:: 
  • 双引号:""

2、数据类型: 

  • 嵌套对象
  • 数组
  • 字符串
  • 数字
  • 布尔值
  • 空值

3、组合解析:

  • {} 解析大括号类型 
  • [] 解析中括号类型 
  • {"key": [...]} 解析大括号嵌套中括号类型
  • [{"key": "value"}] 解析中括号嵌套大括号类型

 

JSON数据解析

  • 如果看到是{ } => 使用 JSONObject 解析
  • 如果看到的[ ] => 使用 JSONArray 解析

前言

该项目是一个SpringMVC中自定义解析JSON参数的Project,我们都知道Spring传统的解决方案是@RequestBody来解析JSON格式参数,但这样一来,会出现一个很不方便的问题,失去了@RequestParam的特性——自定义分参解析,因为@RequestBody只能一次性全部解析完,这样又会带来一个可读性的问题,如果是多个简单的JSON数据混合在一起的话,只能用Map来接收,这样导致可读性严重下降。所以,@RequestJson的出现就是为了解决这两大问题,也可以这么理解@RequestJson的特性:@RequestJson == @RequestBody + @RequestParam

在项目中,如果后端要接收前端传来的 json 参数,原 spring 虽然提供了 @RequestBody 注解来封装 json 数据,但局限性也挺大的(因为@RequestBody + @RequestParm无法相辅相成),对参数要么适用 jsonObject 或者 javaBean 类,或者 string。

  • 若使用 jsonObject 接收,对于 json 里面的参数,还要进一步获取解析,很麻烦。
  • 若使用 javaBean 来接收,若接口参数不一样,那么每一个接口都得对应一个javaBean。
  • 若使用 string 来接收,那么也得需要自己解析json参数。
  • @RequestBody有单体限制的问题
  • Map解析器优先于自定义解析器的问题

所以就琢磨了一个和GET/POST - JSON提交方式一样,直接在Controller层接口写参数名即可接收对应JSON数据参数值。

 

运行环境

  • JDK 1.8
  • IDEA 2018.3

 

技术理解

  1. @RequestMapping

  2. @RequestParam

  3. @RequestBody

  4. @PathVariable

  5. HandlerMethodArgumentResolver

 

一、理解 @RequestMapping

SpringMVC - @RequestJson之HandlerMethodArgumentResolver 从入门到青铜_第1张图片

SpringMVC - @RequestJson之HandlerMethodArgumentResolver 从入门到青铜_第2张图片

SpringMVC - @RequestJson之HandlerMethodArgumentResolver 从入门到青铜_第3张图片

国际惯例先介绍什么是@RequestMapping,@RequestMapping 是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径;用于方法上,表示在类的父路径下追加方法上注解中的地址将会访问到该方法,此处需注意@RequestMapping用在类上可以没有,但是用在方法上必须有。

@Controller
@RequestMapping(value = "/controllers") // 设置想要跳转的父路径
public class StatisticUserCtrl {
  
  //如需注入,则写入需要注入的类
  //@Autowired
 
  // 设置方法下的子路经
  @RequestMapping(value = "/method")
  public String helloworld() {
 
      return "helloWorld";
 
  }
}

其原理也非常好了解,其对应的 action 就是“ (父路径) controller/(父路径下方法路经)method ”。因此,在本地服务器上访问方法 http://localhost:8080/controller/method 就会返回(跳转)到“ helloWorld.jsp ”页面。

/**
    params:可以限制请求参数
    param1: 表示请求必须包含名为 param1 的请求参数
    !param1: 表示请求不能包含名为 param1 的请求参数
    param1 != value1: 表示请求包含名为 param1 的请求参数,但其值不能为 value1
    {“param1=value1”, “param2”}: 请求必须包含名为 param1 和param2 的两个请求参数,且 param1 参   数的值必须为 value1
*/
 
@RequestMapping(value="/handle03",params="stu")
public String handle03(){
    return "success.jsp";
}

 

二、理解 @RequestParam

用来处理Content-Type: 为 application/x-www-form-urlencoded编码的内容。(Http协议中,如果不指定Content-Type,则默认传递的参数就是application/x-www-form-urlencoded类型)

RequestParam可以接受简单类型的属性,也可以接受对象类型。 

实质是将Request.getParameter() 中的Key-Value参数Map利用Spring的转化机制ConversionService配置,转化成参数接收对象或字段。

defaultValue:如果该参数为NULL,则采用此默认值。

public String fun(@Requestparam(value="name", defaultValue="zhangsan") String name)
{}

Ps:在Content-Type: application/x-www-form-urlencoded的请求中, get 方式中queryString的值,和post方式中 body data的值都会被Servlet接受到并转化到Request.getParameter()参数集中,所以@RequestParam可以获取的到。

 

三、理解 @RequestBody

@RequestBody 注解则是将 HTTP 请求正文插入方法中,使用适合的 HttpMessageConverter 将请求体写入某个对象。

作用:

1)该注解用于读取Request请求的body部分数据,使用系统默认配置的HttpMessageConverter进行解析,然后把相应的数据绑定到要返回的对象上。

2)再把HttpMessageConverter返回的对象数据绑定到 controller 中方法的参数上。

3)该注解对实体类里嵌套 List 也可以解析进去。

使用时机:

A) GET、POST方式提时, 根据request header Content-Type的值来判断:

application/x-www-form-urlencoded, 可选(即非必须,因为这种情况的数据@RequestParam, @ModelAttribute也可以处理,当然@RequestBody也能处理); 
multipart/form-data, 不能处理(即使用@RequestBody不能处理这种格式的数据); 
其他格式, 必须(其他格式包括application/json, application/xml等。这些格式的数据,必须使用@RequestBody来处理);

B) PUT方式提交时, 根据request header Content-Type的值来判断:

application/x-www-form-urlencoded, 必须;multipart/form-data, 不能处理;其他格式必须;

说明:

request的body部分的数据编码格式由header部分的Content-Type指定。

@RequestMapping(value = "user/login")
@ResponseBody
// 将ajax(datas)发出的请求写入 User 对象中
public User login(@RequestBody User user) {   
// 这样就不会再被解析为跳转路径,而是直接将user对象写入 HTTP 响应正文中
    return user;    
}

 

小总结

当前台界面使用GET或POST方式提交数据时,数据编码格式由请求头的ContentType指定。分为以下几种情况:

  1. application/x-www-form-urlencoded,这种情况的数据@RequestParam、@ModelAttribute可以处理,@RequestBody也可以处理。
  2. multipart/form-data,@RequestBody不能处理这种格式的数据。(form表单里面有文件上传时,必须要指定enctype属性值为multipart/form-data,意思是以二进制流的形式传输文件。)
  3. application/json、application/xml等格式的数据,必须使用@RequestBody来处理。

 

四、理解 @PathVariable

说到这了,顺便说一下 @PathVariable 注解,其用来获取请求路径(url )中的动态参数。如果@RequestMapping中表示为”item/{id}”,id和形参名称一致,@PathVariable不用指定名称。

页面发出请求:

function login() {
    var url = "${pageContext.request.contextPath}/person/login/";
    var query = $('#id').val() + '/' + $('#name').val() + '/' + $('#status').val();
    url += query;
    $.get(url, function(data) {
        alert("id: " + data.id + "name: " + data.name + "status: "
                + data.status);
    });
}
/**
* @RequestMapping(value = "user/login/{id}/{name}/{status}") 中的 {id}/{name}/{status}
* 与 @PathVariable int id、@PathVariable String name、@PathVariable boolean status
* 一一对应,按名匹配。
*/

@RequestMapping(value = "user/login/{id}/{name}/{status}")
@ResponseBody
//@PathVariable注解下的数据类型均可用
public User login(@PathVariable int id, @PathVariable String name, @PathVariable boolean status) {
//返回一个User对象响应ajax的请求
    return new User(id, name, status);
}

 

五、理解 HandlerMethodArgumentResolver

在初学springmvc框架时,我就一直有一个疑问,为什么controller方法上竟然可以放这么多的参数,而且都能得到想要的对象,比如HttpServletRequest或HttpServletResponse,各种注解@RequestParam、@RequestHeader、@RequestBody、@PathVariable、@ModelAttribute等。

org.springframework.web.method.support.HandlerMethodArgumentResolver接口

springmvc自带的一些实现:

  • ServletRequestMethodArgumentResolver和ServletResponseMethodArgumentResolver处理了自动绑定HttpServletRequest和HttpServletResponse
  • RequestParamMapMethodArgumentResolver处理了@RequestParam
  • RequestHeaderMapMethodArgumentResolver处理了@RequestHeader
  • PathVariableMapMethodArgumentResolver处理了@PathVariable
  • ModelAttributeMethodProcessor处理了@ModelAttribute
  • RequestResponseBodyMethodProcessor处理了@RequestBody
  • ……

springmvc具有一个参数解析器容器RequestMappingHandlerAdapter.argumentResolvers,该参数的初始化在RequestMappingHandlerAdapter#afterPropertiesSet()。

public void afterPropertiesSet() {
    ......
    if (this.argumentResolvers == null) {
        List resolvers = getDefaultArgumentResolvers();
        this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    ......
}

/**
 * Return the list of argument resolvers to use including built-in resolvers
 * and custom resolvers provided via {@link #setCustomArgumentResolvers}.
 */
private List getDefaultArgumentResolvers() {
    List resolvers = new ArrayList();

    // Annotation-based argument resolution
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
    resolvers.add(new RequestParamMapMethodArgumentResolver());
    resolvers.add(new PathVariableMethodArgumentResolver());
    resolvers.add(new PathVariableMapMethodArgumentResolver());
    resolvers.add(new MatrixVariableMethodArgumentResolver());
    resolvers.add(new MatrixVariableMapMethodArgumentResolver());
    resolvers.add(new ServletModelAttributeMethodProcessor(false));
    resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new RequestHeaderMapMethodArgumentResolver());
    resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new SessionAttributeMethodArgumentResolver());
    resolvers.add(new RequestAttributeMethodArgumentResolver());

    // Type-based argument resolution
    resolvers.add(new ServletRequestMethodArgumentResolver());
    resolvers.add(new ServletResponseMethodArgumentResolver());
    resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RedirectAttributesMethodArgumentResolver());
    resolvers.add(new ModelMethodProcessor());
    resolvers.add(new MapMethodProcessor());
    resolvers.add(new ErrorsMethodArgumentResolver());
    resolvers.add(new SessionStatusMethodArgumentResolver());
    resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

    // Custom arguments
    if (getCustomArgumentResolvers() != null) {
        resolvers.addAll(getCustomArgumentResolvers());
    }

    // Catch-all
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
    resolvers.add(new ServletModelAttributeMethodProcessor(true));

    return resolvers;
}

可以看出springmvc的参数解析器容器中存放着内置的参数解析器 + 自定义解析器,这里边就包括@RequestBody的解析器RequestResponseBodyMethodProcessor,来看一下这个解析器的主要方法:

@Override
public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(RequestBody.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
             // 这里使用MappingJackson2HttpMessageConverter将输入流body体中的转化为Book对象
}

相关API:

SpringMVC - @RequestJson之HandlerMethodArgumentResolver 从入门到青铜_第4张图片
SpringMVC - @RequestJson之HandlerMethodArgumentResolver 从入门到青铜_第5张图片

作用:

SpringMVC解析器用于解析request请求参数并绑定数据到Controller的入参上。

自定义一个参数解析器需要实现HandlerMethodArgumentResolver接口,重写supportsParameter和resolveArgument方法,配置文件中加入resolver配置。

如果需要多个解析器同时生效需要在一个解析器中对其他解析器做兼容。

由来:

如果前端对某个不要入参做了加密操作,后端接收到该参数后,都需要进行解密操作。

针对这种需求,首先想到的是filter或者interceptor实现,但是由于HttpServletRequest对象本身是不提供setParameter()方法的,因此想要修改request中的参数值为decode后的值是不易达到的。

SpringMVC的HandlerMethodArgumentResolver,解析器;其功能就是解析request请求参数并绑定数据到Controller的入参上。

注意:

1、一个参数解析器最重要的方法有两个:
(1)supportsParameter 指定哪些参数使用该解析器进行解析,用于判定是否需要处理该参数分解,返回true为需要,并会去调用下面的方法resolveArgument。
(2)resolveArgument 对参数进行真正的解析操作,真正用于处理参数分解的方法,返回的Object就是controller方法上的形参对象。

这也是自定义参数解析器需要去实现的两个方法。

2、在解析器容器中,自定义解析器是位于内置解析器之后,这个顺序也是解析器的优先级,也就是说假设有一个参数同时满足两个解析器,只有第一个解析器会生效,那么怎么去调整这个解析器的顺序呢?

好,现在,我们已经大致了解了springmvc的参数解析器,以及@RequestBody的解析过程。那么来看一下这个例子:

@RequestMapping(value = "/two-body", method = RequestMethod.POST)
public Book testCommon(@RequestBody Book book1, @RequestBody Book book2) {
    Book book = new Book();
    book.setId(Optional.ofNullable(book1).orElse(book2).getId());
    book.setName(Optional.ofNullable(book1).orElse(book2).getName());
    return book;
}

有两个@RequestBody,一执行,结果抛错:

{
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.http.converter.HttpMessageNotReadableException",
  "message": "I/O error while reading input message; nested exception is 
             java.io.IOException: Stream closed"
}

400通常是输入参数错误,错误原因:从上文对@RequestBody的解析过程的分析来看,这个参数实际上是将输入流的body体作为一个整体进行转换,而body整体只有一份,解析完成之后会关闭输入流,所以第二个参数book2的解析就会抛错。

当前,解决此类的方案有两种:

1、@RequestBody List books

2、@RequestBody MultiObject books

不管是哪一种,其实都是将众多的对象组成一个,因为在springmvc的一个方法中只能有一个@RequestBody,这被称为单体限制。其实在有些场景下,我就是想实现多个@RequestBody这样的功能,该怎么办?(我在实现kspringfox框架的时候,就遇到了这样的诉求:kspringfox是一个扩展了springfox的框架,主要实现了对dubbo接口的文档化,以及将dubbo接口透明的转为rest接口供我们调用的功能)。

 

开始划重点…前方高能!

 

自定义spring参数注解 - 打破@RequestBody单体限制实现方案


支持数据

  • 实体
  • 集合
  • 数组
  • MapWapper(Map的一个包装器,通过getInnerMap获取真实Map)

 

零、依赖JAR包


  com.jayway.jsonpath
  json-path
  2.4.0



  com.alibaba
  fastjson
  1.2.47

 

一、自定义spring的参数注解

首先自定义一个类似于@RequestBody的注解:@RequestJson

自定义注解很简单:@Target指明注解应用于参数上;@Retention指明注解应用于运行时。

package com.luxsuen.requestjson.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestJson {

    String value();

    boolean required() default true;

    String defaultValue() default "";
}

 

二、编写spring的参数注解解析器

package com.luxsuen.requestjson.resolver;

import com.alibaba.fastjson.JSON;
import com.jayway.jsonpath.JsonPath;
import com.luxsuen.requestjson.common.Const;
import com.luxsuen.requestjson.util.IOUtil;
import com.luxsuen.requestjson.util.MapWrapper;
import net.minidev.json.JSONArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class RequestJsonHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * @Author Lux Sun
     * @Description: 判断是否支持,若支持,则调用 resolveArgument
     * @Param: [methodParameter]
     * @Return: boolean
     */
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.hasParameterAnnotation(Const.ANNOTATION_CLASS);
    }

    /**
     * @Author Lux Sun
     * @Description: 重写 resolveArgument,核心函数:分解参数功能
     * @Param: [methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory]
     * @Return: java.lang.Object
     */
    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
                                  NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {

        String body = getRequestBody(nativeWebRequest);
        String annotationKey = null;
        Object matchObj = null;

        try {
            // 获取注解 value
            annotationKey = methodParameter.getParameterAnnotation(Const.ANNOTATION_CLASS).value();

            int midBracket = body.indexOf('[');
            int bigBracket = body.indexOf('{');
            // 匹配[...{}...]、[...]、{...}类型
            if(bigBracket > midBracket && midBracket != -1 || midBracket != -1 && bigBracket == -1)
            {
                // 此时的annotationKey取什么都可以,因为下面一定会解析出属于JSONArray类型,那时才是真正的key
                body = "{\"" + annotationKey + "\":" + body + "}";
            }

            // 默认只读第一层{...}中key(至此一定是{...}结构)
            matchObj = JsonPath.read(body, annotationKey);
            if (methodParameter.getParameterAnnotation(Const.ANNOTATION_CLASS).required() && matchObj == null) {
                throw new Exception(annotationKey + "不能为空");
            }
            else if(matchObj instanceof LinkedHashMap && ((Map)matchObj).size()==0) // { key: {} }
            {
                throw new Exception("JSON对象为空");
            }
            else if(matchObj instanceof JSONArray && ((List)matchObj).size()==0) // { key: [] }
            {
                throw new Exception("JSONArray为空");
            }
        }
        catch (Exception e) {
            // 情况1:required == false && 找不到 key 对应的 json(无论Body是否为空)
            if (!methodParameter.getParameterAnnotation(Const.ANNOTATION_CLASS).required()) {
                return null;
            }

            // 情况2:required == true && 找不到 key 对应的 json(无论Body是否为空)
            e.printStackTrace();
            return null;
        }

        // Map2JsonString
        String matchJsonStr = JSON.toJSONString(matchObj);

        // methodParameter.getGenericParameterType() 返回参数的完整类型(带泛型)(web层形参的类型)
        final Type type = methodParameter.getGenericParameterType();

        // 判断转换类型
        if(MapWrapper.class.isAssignableFrom(methodParameter.getParameterType())){ // Map类型(使用自己新封装的JSONObject充当Map)
            if(matchObj instanceof JSONArray) // 匹配JSONArray类型,前面需要加一个key,组装成JSONObject类型格式
            {
                matchJsonStr = "{\"" + annotationKey + "\":" + matchJsonStr + "}";
            }
            MapWrapper mapWrapper = new MapWrapper(JSON.parseObject(matchJsonStr));
            return mapWrapper;
        }
        else { // Number、BigDecimal/BigInteger、Boolean、String、Entity/List
            Object rsObj = null;
            try {
                rsObj = JSON.parseObject(matchJsonStr, type);
            }
            catch (Exception e) {
                // 情况:形参类型与JSON解析后的类型不匹配
                e.printStackTrace();
            }
            return rsObj;
        }
    }

    /**
     * @Author Lux Sun
     * @Description: 获取请求包的Body内容
     * @Param: [nativeWebRequest]
     * @Return: java.lang.String
     */
    private String getRequestBody(NativeWebRequest nativeWebRequest) {
        HttpServletRequest servletRequest = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        String jsonBody = (String) servletRequest.getAttribute(Const.JSON_REQUEST_BODY);
        if (jsonBody == null) {
            try {
//                jsonBody = IOUtils.toString(in); // closed stream 问题
                jsonBody = IOUtil.inputStream2Str(servletRequest.getInputStream(),"UTF-8");
                servletRequest.setAttribute(Const.JSON_REQUEST_BODY, jsonBody);
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }
        return jsonBody;
    }
}

注意:

1、supportsParameter方法指明RequestJsonHandlerMethodArgumentResolver只处理带有@RequestJson注解的参数。

2、resolveArgument方法对入参进行解析:首先通过JsonPath获取对应参数的参数值(json串),然后获取参数的完整类型(带泛型),最后使用fastjson解析器将json格式的参数值转化为具体类型的对象。

 

三、将自定义参数解析器设置到spring的参数解析器集合中

XML配置1


    
        
    

或XML配置2


    
          
    

或SpringBoot配置

import java.util.List;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@SpringBootApplication
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

	@Override
	protected void addArgumentResolvers(List argumentResolvers) {

		// 注册自定义的参数分解器
		argumentResolvers.add(new RequestJsonHandlerMethodArgumentResolver());
	}
}

通过上述这种方式,我们就将自定义的RequestJsonHandlerMethodArgumentResolver解析器添加到了spring的自定义参数解析器集合中。

此时,一个自定义的参数注解就可以基本使用在我们的项目中了。

 

四、指定参数解析器的“优先级”

通过前边的步骤,一个自定义的参数注解就“基本”可以使用了,但是还有一个问题。看这个例子。

@RequestMapping(value = "/map", method = RequestMethod.POST)
public Map testMap(@RequestJson(value = "title2Book") Map title2Book) {
	return title2Book;
}

在RequestJsonHandlerMethodArgumentResolver#supportsParameter方法中打断点来debug一下,发现上边这个例子根本不会走进去,也就是说此时我们自定义的RequestJsonHandlerMethodArgumentResolver不再起作用了。

原因:在springmvc的解析器容器中,自定义解析器是放在内置解析器之后的,这个顺序也是解析器的优先级,也就是说假设有一个参数同时满足两个解析器,只有第一个解析器会生效。而springmvc对Map是专门有一个内置解析器的,这个解析器位于我们的RequestJsonHandlerMethodArgumentResolver之前,所以springmvc会使用Map解析器进行解析,而不再使用RequestJsonHandlerMethodArgumentResolver。

具体源码我们再翻回头看一下“一”中的getDefaultArgumentResolvers:

/**
 * Return the list of argument resolvers to use including built-in resolvers
 * and custom resolvers provided via {@link #setCustomArgumentResolvers}.
 */
private List getDefaultArgumentResolvers() {

    List resolvers = new ArrayList();
    
    ...
   	//Map解析器
    resolvers.add(new MapMethodProcessor());
    ...

    // 自定义解析器
    if (getCustomArgumentResolvers() != null) {
        resolvers.addAll(getCustomArgumentResolvers());
    }

    return resolvers;
}

再看一下MapMethodProcessor#supportsParameter。

@Override
public boolean supportsParameter(MethodParameter parameter) {
	return Map.class.isAssignableFrom(parameter.getParameterType());
}

原因明了了以后,就要去想解决方案。(如果spring可以提供为参数解析器设置order的能力,那么就好了,但是spring没有提供,这也就是为什么在这“优先级”这里打了个引号的原因)。

解决方案

1、第一种方案

在服务启动时,动态替换掉MapMethodProcessor#supportsParameter的字节码。

@Override
public boolean supportsParameter(MethodParameter parameter) {
	if(parameter.hasParameterAnnotation(RequestJson.class))
	{
		return false;
	}
	return Map.class.isAssignableFrom(parameter.getParameterType());
}

使用javassist可以实现这一点,但是这样去做,代码复杂性较高。“任何一个功能的实现,都要想办法降低代码复杂性”。

 

第二种方案

首先删除WebConfig,让spring不再自动的将自定义解析器加到RequestMappingHandlerAdapter的解析器容器中;然后我们通过下面的方式手动的将RequestJsonHandlerMethodArgumentResolver加载到RequestMappingHandlerAdapter的解析容器中。(通过这样的方式,我们可以任意的指定解析器的顺序)。

@Configuration
public class MethodArgumentResolver {
    @Autowired
    private RequestMappingHandlerAdapter adapter;

    @PostConstruct
    public void injectSelfMethodArgumentResolver() {
        List argumentResolvers = new ArrayList<>();
        argumentResolvers.add(new RequestJsonHandlerMethodArgumentResolver());
        argumentResolvers.addAll(adapter.getArgumentResolvers());
        adapter.setArgumentResolvers(argumentResolvers);
    }
}

Ps:XML配置的话,思路类似,但是会出现一个问题,极有可能会影响其它参数解析原本并不冲突的解析器,导致这个功能OK了,另一个功能又崩了的局面,所以还有最后一种方案(推荐)。

 

第三种方案

此处我们封装一个类似JSONObject的实体类MapWrapper来替代Map原型解决,可以通过MapWrapper.getMapWrapper()拿到我们需要的Map。

package com.luxsuen.requestjson.util;

import com.alibaba.fastjson.JSONObject;

public class MapWrapper {
    private JSONObject jsonObject;

    public MapWrapper(JSONObject jsonObject) {
        this.jsonObject = jsonObject;
    }

    public JSONObject getMapWrapper() {
        return jsonObject;
    }
}

 

五、测试项目

package com.luxsuen.requestjson.entity;

import java.util.Date;

public class User {
    private Integer userId;

    private String userAccount;

    private String userPassword;

    private String userRemark;

    private Byte userDel;

    private Date userCreatetime;

    private Date userUpdatetime;

    public User(Integer userId, String userAccount, String userPassword, String userRemark, Byte userDel, Date userCreatetime, Date userUpdatetime) {
        this.userId = userId;
        this.userAccount = userAccount;
        this.userPassword = userPassword;
        this.userRemark = userRemark;
        this.userDel = userDel;
        this.userCreatetime = userCreatetime;
        this.userUpdatetime = userUpdatetime;
    }

    public User() {
        super();
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getUserAccount() {
        return userAccount;
    }

    public void setUserAccount(String userAccount) {
        this.userAccount = userAccount == null ? null : userAccount.trim();
    }

    public String getUserPassword() {
        return userPassword;
    }

    public void setUserPassword(String userPassword) {
        this.userPassword = userPassword == null ? null : userPassword.trim();
    }

    public String getUserRemark() {
        return userRemark;
    }

    public void setUserRemark(String userRemark) {
        this.userRemark = userRemark == null ? null : userRemark.trim();
    }

    public Byte getUserDel() {
        return userDel;
    }

    public void setUserDel(Byte userDel) {
        this.userDel = userDel;
    }

    public Date getUserCreatetime() {
        return userCreatetime;
    }

    public void setUserCreatetime(Date userCreatetime) {
        this.userCreatetime = userCreatetime;
    }

    public Date getUserUpdatetime() {
        return userUpdatetime;
    }

    public void setUserUpdatetime(Date userUpdatetime) {
        this.userUpdatetime = userUpdatetime;
    }
}
package com.luxsuen.requestjson.web;

import com.luxsuen.jsonutil.util.JsonUtil;
import com.luxsuen.requestjson.entity.User;
import com.luxsuen.requestjson.annotation.RequestJson;
import com.luxsuen.requestjson.util.MapWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

@Controller
@RequestMapping("/user/")
public class UserController {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * @Author Lux Sun
     * @Description: 测试(以大括号开始) @PathVariable & @RequestJson + 一步获取分参后的value
     * @Param: [userId, userDel, userAccount, scoreArray, money, user1, user2, user3, userList, mapWrapper1, mapWrapper2]
     * @Return: java.lang.Object
     */
    @RequestMapping(value = "info/{userId}", method = RequestMethod.POST)
    @ResponseBody
    public Object checkRequestJsonByBeginBigBrace(
                                                @PathVariable(value = "userId") Integer userId,
                                                @RequestJson(value = "userDel") Boolean userDel,
                                                @RequestJson(value = "userAccount") String userAccount,
                                                @RequestJson(value = "scoreArray") Double[] scoreArray,
                                                @RequestJson(value = "money") BigDecimal money,
                                                @RequestJson(value = "user1") User user1,
                                                @RequestJson(value = "user2") User user2,
                                                @RequestJson(value = "user3") User user3,
                                                @RequestJson(value = "userList") List userList,
                                                @RequestJson(value = "mapWrapper1") MapWrapper mapWrapper1,
                                                @RequestJson(value = "mapWrapper2") MapWrapper mapWrapper2)
    {
        // 获取实体类 & 实体类数组
        Object user = JsonUtil.getJsonEntityByKeyArray(mapWrapper1.getMapWrapper(), User.class, "k8");
        Object userlist = JsonUtil.getJsonEntityListByKeyArray(mapWrapper1.getMapWrapper(), User.class, "k9");

        // 获取普通类型 & 集合
        Object integer = JsonUtil.getJsonValueByKeyArray(mapWrapper1.getMapWrapper(), Integer.class, "k9", "0", "userId");
        Object string = JsonUtil.getJsonValueByKeyArray(mapWrapper1.getMapWrapper(), String.class, "k2");
        Object bool = JsonUtil.getJsonValueByKeyArray(mapWrapper1.getMapWrapper(), Boolean.class, "k3");
        Object array1 = JsonUtil.getJsonValueByKeyArray(mapWrapper1.getMapWrapper(), List.class, "k4");
        Object map1 = JsonUtil.getJsonValueByKeyArray(mapWrapper1.getMapWrapper(), Map.class, "k5");
        Object map2 = JsonUtil.getJsonValueByKeyArray(mapWrapper1.getMapWrapper(), Map.class, "k6");
        Object array2 = JsonUtil.getJsonValueByKeyArray(mapWrapper1.getMapWrapper(), List.class, "k7");
        return null;
    }

    /**
     * @Author Lux Sun
     * @Description: 测试(以中括号开始)
     * @Param: [mapWrapper3]
     * @Return: java.lang.Object
     */
    @RequestMapping(value = "info", method = RequestMethod.POST)
    @ResponseBody
    public Object checkRequestJsonByBeginMidBrace(@RequestJson(value = "mapWrapper3") MapWrapper mapWrapper3)
    {
        return null;
    }

    /**
     * @Author Lux Sun
     * @Description: 测试(只有大括号)
     * @Param: [arr]
     * @Return: java.lang.Object
     */
    @RequestMapping(value = "onlyBigBrace", method = RequestMethod.POST)
    @ResponseBody
    public Object checkRequestJsonByOnlyBigBrace(@RequestJson(value = "name") String name)
    {
        return null;
    }

    /**
     * @Author Lux Sun
     * @Description: 测试(只有中括号)
     * @Param: [arr]
     * @Return: java.lang.Object
     */
    @RequestMapping(value = "onlyMidBrace", method = RequestMethod.POST)
    @ResponseBody
    public Object checkRequestJsonByOnlyMidBrace(@RequestJson(value = "arr") Integer[] arr)
    {
        return null;
    }
}

测试案例 

// 以大括号开始嵌套

{
  "userId": 1,
  "userDel": false,
  "userAccount": "Admin",
  "scoreArray": [
    98.5,
    89,
    120
  ],
  "money": 123.45678987654321,
  "user1": {
    "userId": 1,
    "userAccount": "Bob1",
    "userPassword": "abc123456",
    "userRemark": "",
    "userDel": 0,
    "userCreatetime": "2019-03-07 16:52:46",
    "userUpdatetime": "2019-03-07 16:52:46"
  },
  "user2": {
    "userId": 2,
    "userAccount": "Bob2",
    "userPassword": "abc123456",
    "userRemark": "",
    "userDel": 0,
    "userCreatetime": "2019-03-07 16:52:46",
    "userUpdatetime": "2019-03-07 16:52:46",
    "more": "!@#$%^&*"
  },
  "user3": {
    "userId": 3,
    "userAccount": "Bob3",
    "userPassword": "abc123456",
    "userRemark": "用户",
    "userCreatetime": "2019-03-07 16:52:46",
    "userUpdatetime": "2019-03-07 16:52:46"
  },
  "userList": [
    {
      "userId": 2,
      "userAccount": "Alice",
      "userPassword": "abc789",
      "userRemark": "大学生",
      "userDel": 1,
      "userCreatetime": "2019-03-08 16:52:46",
      "userUpdatetime": "2019-03-08 16:52:46"
    },
    {
      "userId": 3,
      "userAccount": "Tom",
      "userPassword": "[{}]147@#",
      "userRemark": "学生",
      "userDel": 0,
      "userCreatetime": "2007-03-08 16:52:46",
      "userUpdatetime": "2007-03-08 16:52:46"
    }
  ],
  "mapWrapper1": {
    "k1": 123,
    "k2": "abc",
    "k3": true,
    "k4": [
      1,
      2,
      3
    ],
    "k5": {
      "k5-1": "qwer",
      "k5-2": 90.6
    },
    "k6": {"k6-1": [
      "qaz",
      "wsx"
    ]},
    "k7": [
      {"k7-1": 456},
      {"k7-2": "asdf"}
    ],
    "k8": {
      "userId": 3,
      "userAccount": "Bob3",
      "userPassword": "abc123456",
      "userRemark": "用户",
      "userCreatetime": "2019-03-07 16:52:46",
      "userUpdatetime": "2019-03-07 16:52:46"
    },
    "k9": [
      {
        "userId": 2,
        "userAccount": "Alice",
        "userPassword": "abc789",
        "userRemark": "大学生",
        "userDel": 1,
        "userCreatetime": "2019-03-08 16:52:46",
        "userUpdatetime": "2019-03-08 16:52:46"
      },
      {
        "userId": 3,
        "userAccount": "Tom",
        "userPassword": "[{}]147@#",
        "userRemark": "学生",
        "userDel": 0,
        "userCreatetime": "2007-03-08 16:52:46",
        "userUpdatetime": "2007-03-08 16:52:46"
      }
    ]
  },
  "mapWrapper2": [
    1,
    "2",
    {
      "k1": 123,
      "k2": "abc",
      "k3": true,
      "k4": [
        1,
        2,
        3
      ],
      "k5": {
        "k5-1": "qwer",
        "k5-2": 90.6
      },
      "k6": {"k6-1": [
        "qaz",
        "wsx"
      ]},
      "k7": [
        {"k7-1": 456},
        {"k7-2": "asdf"}
      ]
    },
    {
      "k8": 456,
      "k9": "def",
      "k10": false,
      "k11": [
        4,
        5,
        6
      ],
      "k12": {
        "k12-1": "qwer",
        "k12-2": 90.6
      },
      "k13": {"k13-1": [
        "qaz",
        "wsx"
      ]},
      "k14": [
        {"k14-1": 456},
        {"k14-2": "asdf"}
      ]
    }
  ]
}
// 以中括号开始嵌套

[
  1,
  "2",
  {
    "k1": 123,
    "k2": "abc",
    "k3": true,
    "k4": [
      1,
      2,
      3
    ],
    "k5": {
      "k5-1": "qwer",
      "k5-2": 90.6
    },
    "k6": {"k6-1": [
      "qaz",
      "wsx"
    ]},
    "k7": [
      {"k7-1": 456},
      {"k7-2": "asdf"}
    ]
  },
  {
    "k8": 456,
    "k9": "def",
    "k10": false,
    "k11": [
      4,
      5,
      6
    ],
    "k12": {
      "k12-1": "qwer",
      "k12-2": 90.6
    },
    "k13": {"k13-1": [
      "qaz",
      "wsx"
    ]},
    "k14": [
      {"k14-1": 456},
      {"k14-2": "asdf"}
    ]
  }
]
// 只有大括号

{
  "name": "Admin"
}
// 只有中括号

[
  1, 2, 3
]
// required

{
  
}

Ps:如果在JsonPath执行报错时,当找不到“key”引起的话,这是正常现象,并不是程序的BUG噢~

 

附:

辅助代码

package com.luxsuen.requestjson.common;

import com.luxsuen.requestjson.annotation.RequestJson;

public class Const {

    // request or session key mark
    public static final String JSON_REQUEST_BODY = "JSON_REQUEST_BODY ";

    // 获取注解Class
    public static final Class ANNOTATION_CLASS = RequestJson.class;
}
package com.luxsuen.requestjson.util;

import java.io.*;

public class IOUtil {

    /**
     * @Author Lux Sun
     * @Description: InputStream 转 String
     * @Param: [in, encode]
     * @Return: java.lang.String
     */
    public static String inputStream2Str(InputStream in, String encode)
    {
        String str = "";
        BufferedReader reader = null;
        try {
            if (encode == null || encode.equals(""))
            {
                // 默认以utf-8形式
                encode = "utf-8";
            }
            reader = new BufferedReader(new InputStreamReader(in, encode));
            StringBuffer sb = new StringBuffer();

            while ((str = reader.readLine()) != null)
            {
                sb.append(str).append("\n");
            }

            return sb.toString();
        }
        catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            try {
                if(reader != null)
                {
                    reader.close();
                }
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }

        return str;
    }
}



    
    
    
        
            
        
    

    
    
    

    
    
        
        
        
    

    
    

    
        
            
                text/html;charset=UTF-8
                application/json;charset=UTF-8
            
        
        
            
                
                WriteMapNullValue
                QuoteFieldNames
            
        
    



  
  
    encoding
    org.springframework.web.filter.CharacterEncodingFilter
    
      encoding
      UTF-8
    
  
  
    encoding
    /*
  

  
  
    dispather
    org.springframework.web.servlet.DispatcherServlet

    
    
    
    
      contextConfigLocation
      classpath:spring/spring-*.xml
    

  

  
    dispather
    
    /
  



  4.0.0

  com.luxsun
  requestjson
  1.0.0
  war

  
    
    
    
      
        maven-clean-plugin
        3.1.0
      
      
        maven-resources-plugin
        3.0.2
      
      
        maven-compiler-plugin
        3.8.0
      
      
        maven-surefire-plugin
        2.22.1
      
      
        maven-war-plugin
        3.2.2
      
      
        maven-install-plugin
        2.5.2
      
      
        maven-deploy-plugin
        2.8.2
      

      
        org.mybatis.generator 
        mybatis-generator-maven-plugin
        1.3.5
        
          true 
          true 
          src/main/resources/xml/generatorConfig.xml
           
        
      

      
        org.apache.maven.plugins 
        maven-compiler-plugin
        2.3.2
        
          1.8 
          1.8 
          UTF-8
        
      

      
      
        org.apache.maven.plugins
        maven-jar-plugin
        3.0.2
        
          
            default-jar
            package
            
              jar
            
          
        
        
          
            
            
            
            **/spring/
            **/com/luxsuen/requestjson/entity/
            **/com/luxsuen/requestjson/web/
            **/META-INF/*.kotlin_module
          
        
      
    
  

  
    4.3.2.RELEASE

    
    UTF-8
    UTF-8

    
    UTF-8
  

  

    
      com.luxsuen
      jsonutil
      1.0.0
    

    
    
      junit
      junit
      4.12
      test
    

    
    
      org.slf4j
      slf4j-api
      1.7.12
    

    
    
      ch.qos.logback
      logback-core
      1.1.1
    

    
    
      ch.qos.logback
      logback-classic
      1.1.1
    

    
    
      mysql
      mysql-connector-java
      5.1.35
      runtime
    

    
    
      c3p0
      c3p0
      0.9.1.2
    

    

    
    
      org.mybatis
      mybatis
      3.3.0
    

    
      org.mybatis
      mybatis-spring
      1.2.3
    

    
    
      taglibs
      standard
      1.1.2
    

    
      jstl
      jstl
      1.2
    

    
      com.fasterxml.jackson.core
      jackson-databind
      2.5.4
    

    
      javax.servlet
      javax.servlet-api
      3.1.0
    

    

    
    
      org.springframework
      spring-core
      ${spring.version}
    

    
      org.springframework
      spring-beans
      ${spring.version}
    

    
      org.springframework
      spring-context
      ${spring.version}
    

    
    
      org.springframework
      spring-aop
      ${spring.version}
    

    
      org.springframework
      spring-aspects
      ${spring.version}
    

    
      org.aspectj
      aspectjweaver
      1.8.6
    

    
      aopalliance
      aopalliance
      1.0
    

    
    
      org.springframework
      spring-jdbc
      ${spring.version}
    

    
      org.springframework
      spring-tx
      ${spring.version}
    

    
    
      org.springframework
      spring-web
      ${spring.version}
    

    
      org.springframework
      spring-webmvc
      ${spring.version}
    

    
    
      org.springframework
      spring-test
      ${spring.version}
    

    
    

    
    
      com.dyuproject.protostuff
      protostuff-core
      1.0.10
    

    
      com.dyuproject.protostuff
      protostuff-runtime
      1.0.10
    

    
      commons-collections
      commons-collections
      3.2.1
    

    
      org.apache.commons
      commons-io
      1.3.2
    

    
      org.codehaus.jackson
      jackson-mapper-asl
      1.9.12
    

    
      com.jayway.jsonpath
      json-path
      2.4.0
    

    
      com.alibaba
      fastjson
      1.2.47
    

    
      com.google.guava
      guava
      20.0
    
  

你可能感兴趣的:(#,SpringMVC,教程)