SpringBoot Controller 中使用多个@RequestBody的正确姿势

最近遇到Controller中需要多个@RequestBody的情况,但是发现并不支持这种写法,

这样导致

1、单个字符串等包装类型都要写一个对象才可以用@RequestBody接收;

2、多个对象需要封装到一个对象里才可以用@RequestBody接收。

查阅StackOverFlow,受到一个解决方案的启发,本人改进为以下版本,并给出了详尽的注释,希望对大家有帮助。

改进后的方案支持:

1、支持通过注解的value指定JSON的key来解析对象。

2、支持通过注解无value,直接根据参数名来解析对象

3、支持GET方式和其他请求方式

4、支持基本类型属性注入

5、支持通过注解无value且参数名不匹配JSON串key时,根据属性解析对象。

6、支持多余属性(不解析、不报错)、支持参数“共用”(不指定value时,参数名不为JSON串的key)

7、支持当value和属性名找不到匹配的key时,对象是否匹配所有属性。

重要更新记录:

2019年02月25日 新增xml方式参考配置

2019年02月07日 fix 当list参数为空时,parameterType.newInstance会导致异常。

2018年12月28日 新增测试用例,完善解析部分代码

2018年10月23日 完善项目格式

2018年08月28日 创建第一版

项目仅供参考,如因使用不当造成任何问题,请自行负责,有问题欢迎探讨改进。

项目地址(建议去拉最新代码):

https://github.com/chujianyun/Spring-MultiRequestBody

另外代码应该会尽量持续更新完善,欢迎大家贡献代码。

 

步骤如下:

0、除spring的Jar包外涉及的主要Maven依赖

  1.  
    <dependency>
  2.  
    <groupId>commons-lang</groupId>
  3.  
    <artifactId>commons-lang</artifactId>
  4.  
    <version>2.4</version>
  5.  
    </dependency>
  6.  
     
  7.  
    <dependency>
  8.  
    <groupId>com.alibaba</groupId>
  9.  
    <artifactId>fastjson</artifactId>
  10.  
    <version>1.2.35</version>
  11.  
    </dependency>
  12.  
     
  13.  
    <dependency>
  14.  
    <groupId>commons-io</groupId>
  15.  
    <artifactId>commons-io</artifactId>
  16.  
    <version>2.6</version>
  17.  
    </dependency>

其中fastjson用来解析json对象,commons-lang用来字符串判空(也可以自己手写),commons-io用来读取请求封装为字符串类型(也可以自己封装)。

 

1、重写方法参数解析器

  1.  
    package com.chujianyun.web.bean;
  2.  
     
  3.  
    import com.alibaba.fastjson.JSON;
  4.  
    import com.alibaba.fastjson.JSONObject;
  5.  
    import io.github.chujianyun.annotation.MultiRequestBody;
  6.  
    import org.apache.commons.io.IOUtils;
  7.  
    import org.apache.commons.lang3.StringUtils;
  8.  
    import org.springframework.core.MethodParameter;
  9.  
    import org.springframework.web.bind.support.WebDataBinderFactory;
  10.  
    import org.springframework.web.context.request.NativeWebRequest;
  11.  
    import org.springframework.web.method.support.HandlerMethodArgumentResolver;
  12.  
    import org.springframework.web.method.support.ModelAndViewContainer;
  13.  
     
  14.  
    import javax.servlet.http.HttpServletRequest;
  15.  
    import java.io.IOException;
  16.  
    import java.lang.reflect.Field;
  17.  
    import java.util.HashSet;
  18.  
    import java.util.Set;
  19.  
     
  20.  
    /**
  21.  
    * 多RequestBody解析器
  22.  
    *
  23.  
    * @author 明明如月
  24.  
    * @date 2018/08/27
  25.  
    */
  26.  
    public class MultiRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
  27.  
     
  28.  
    private static final String JSONBODY_ATTRIBUTE = "JSON_REQUEST_BODY";
  29.  
     
  30.  
    /**
  31.  
    * 设置支持的方法参数类型
  32.  
    *
  33.  
    * @param parameter 方法参数
  34.  
    * @return 支持的类型
  35.  
    */
  36.  
    @Override
  37.  
    public boolean supportsParameter(MethodParameter parameter) {
  38.  
    // 支持带@MultiRequestBody注解的参数
  39.  
    return parameter.hasParameterAnnotation(MultiRequestBody.class);
  40.  
    }
  41.  
     
  42.  
    /**
  43.  
    * 参数解析,利用fastjson
  44.  
    * 注意:非基本类型返回null会报空指针异常,要通过反射或者JSON工具类创建一个空对象
  45.  
    */
  46.  
    @Override
  47.  
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
  48.  
     
  49.  
    String jsonBody = getRequestBody(webRequest);
  50.  
     
  51.  
    JSONObject jsonObject = JSON.parseObject(jsonBody);
  52.  
    // 根据@MultiRequestBody注解value作为json解析的key
  53.  
    MultiRequestBody parameterAnnotation = parameter.getParameterAnnotation(MultiRequestBody.class);
  54.  
    //注解的value是JSON的key
  55.  
    String key = parameterAnnotation.value();
  56.  
    Object value;
  57.  
    // 如果@MultiRequestBody注解没有设置value,则取参数名FrameworkServlet作为json解析的key
  58.  
    if (StringUtils.isNotEmpty(key)) {
  59.  
    value = jsonObject.get(key);
  60.  
    // 如果设置了value但是解析不到,报错
  61.  
    if (value == null && parameterAnnotation.required()) {
  62.  
    throw new IllegalArgumentException(String.format("required param %s is not present", key));
  63.  
    }
  64.  
    } else {
  65.  
    // 注解为设置value则用参数名当做json的key
  66.  
    key = parameter.getParameterName();
  67.  
    value = jsonObject.get(key);
  68.  
    }
  69.  
     
  70.  
    // 获取的注解后的类型 Long
  71.  
    Class<?> parameterType = parameter.getParameterType();
  72.  
    // 通过注解的value或者参数名解析,能拿到value进行解析
  73.  
    if (value != null) {
  74.  
    //基本类型
  75.  
    if (parameterType.isPrimitive()) {
  76.  
    return parsePrimitive(parameterType.getName(), value);
  77.  
    }
  78.  
    // 基本类型包装类
  79.  
    if (isBasicDataTypes(parameterType)) {
  80.  
    return parseBasicTypeWrapper(parameterType, value);
  81.  
    // 字符串类型
  82.  
    } else if (parameterType == String.class) {
  83.  
    return value.toString();
  84.  
    }
  85.  
    // 其他复杂对象
  86.  
    return JSON.parseObject(value.toString(), parameterType);
  87.  
    }
  88.  
     
  89.  
    // 解析不到则将整个json串解析为当前参数类型
  90.  
    if (isBasicDataTypes(parameterType)) {
  91.  
    if (parameterAnnotation.required()) {
  92.  
    throw new IllegalArgumentException(String.format("required param %s is not present", key));
  93.  
    } else {
  94.  
    return null;
  95.  
    }
  96.  
    }
  97.  
     
  98.  
    // 非基本类型,不允许解析所有字段,必备参数则报错,非必备参数则返回null
  99.  
    if (!parameterAnnotation.parseAllFields()) {
  100.  
    // 如果是必传参数抛异常
  101.  
    if (parameterAnnotation.required()) {
  102.  
    throw new IllegalArgumentException(String.format("required param %s is not present", key));
  103.  
    }
  104.  
    // 否则返回null
  105.  
    return null;
  106.  
    }
  107.  
    // 非基本类型,允许解析,将外层属性解析
  108.  
    Object result;
  109.  
    try {
  110.  
    result = JSON.parseObject(jsonObject.toString(), parameterType);
  111.  
    } catch (JSONException jsonException) {
  112.  
    // TODO:: 异常处理返回null是否合理?
  113.  
    result = null;
  114.  
    }
  115.  
     
  116.  
    // 如果非必要参数直接返回,否则如果没有一个属性有值则报错
  117.  
    if (!parameterAnnotation.required()) {
  118.  
    return result;
  119.  
    } else {
  120.  
    boolean haveValue = false;
  121.  
    Field[] declaredFields = parameterType.getDeclaredFields();
  122.  
    for (Field field : declaredFields) {
  123.  
    field.setAccessible(true);
  124.  
    if (field.get(result) != null) {
  125.  
    haveValue = true;
  126.  
    break;
  127.  
    }
  128.  
    }
  129.  
    if (!haveValue) {
  130.  
    throw new IllegalArgumentException(String.format("required param %s is not present", key));
  131.  
    }
  132.  
    return result;
  133.  
    }
  134.  
    }
  135.  
     
  136.  
    /**
  137.  
    * 基本类型解析
  138.  
    */
  139.  
    private Object parsePrimitive(String parameterTypeName, Object value) {
  140.  
    final String booleanTypeName = "boolean";
  141.  
    if (booleanTypeName.equals(parameterTypeName)) {
  142.  
    return Boolean.valueOf(value.toString());
  143.  
    }
  144.  
    final String intTypeName = "int";
  145.  
    if (intTypeName.equals(parameterTypeName)) {
  146.  
    return Integer.valueOf(value.toString());
  147.  
    }
  148.  
    final String charTypeName = "char";
  149.  
    if (charTypeName.equals(parameterTypeName)) {
  150.  
    return value.toString().charAt(0);
  151.  
    }
  152.  
    final String shortTypeName = "short";
  153.  
    if (shortTypeName.equals(parameterTypeName)) {
  154.  
    return Short.valueOf(value.toString());
  155.  
    }
  156.  
    final String longTypeName = "long";
  157.  
    if (longTypeName.equals(parameterTypeName)) {
  158.  
    return Long.valueOf(value.toString());
  159.  
    }
  160.  
    final String floatTypeName = "float";
  161.  
    if (floatTypeName.equals(parameterTypeName)) {
  162.  
    return Float.valueOf(value.toString());
  163.  
    }
  164.  
    final String doubleTypeName = "double";
  165.  
    if (doubleTypeName.equals(parameterTypeName)) {
  166.  
    return Double.valueOf(value.toString());
  167.  
    }
  168.  
    final String byteTypeName = "byte";
  169.  
    if (byteTypeName.equals(parameterTypeName)) {
  170.  
    return Byte.valueOf(value.toString());
  171.  
    }
  172.  
    return null;
  173.  
    }
  174.  
     
  175.  
    /**
  176.  
    * 基本类型包装类解析
  177.  
    */
  178.  
    private Object parseBasicTypeWrapper(Class<?> parameterType, Object value) {
  179.  
    if (Number.class.isAssignableFrom(parameterType)) {
  180.  
    Number number = (Number) value;
  181.  
    if (parameterType == Integer.class) {
  182.  
    return number.intValue();
  183.  
    } else if (parameterType == Short.class) {
  184.  
    return number.shortValue();
  185.  
    } else if (parameterType == Long.class) {
  186.  
    return number.longValue();
  187.  
    } else if (parameterType == Float.class) {
  188.  
    return number.floatValue();
  189.  
    } else if (parameterType == Double.class) {
  190.  
    return number.doubleValue();
  191.  
    } else if (parameterType == Byte.class) {
  192.  
    return number.byteValue();
  193.  
    }
  194.  
    } else if (parameterType == Boolean.class) {
  195.  
    return value.toString();
  196.  
    } else if (parameterType == Character.class) {
  197.  
    return value.toString().charAt(0);
  198.  
    }
  199.  
    return null;
  200.  
    }
  201.  
     
  202.  
    /**
  203.  
    * 判断是否为基本数据类型包装类
  204.  
    */
  205.  
    private boolean isBasicDataTypes(Class clazz) {
  206.  
    Set<Class> classSet = new HashSet<>();
  207.  
    classSet.add(Integer.class);
  208.  
    classSet.add(Long.class);
  209.  
    classSet.add(Short.class);
  210.  
    classSet.add(Float.class);
  211.  
    classSet.add(Double.class);
  212.  
    classSet.add(Boolean.class);
  213.  
    classSet.add(Byte.class);
  214.  
    classSet.add(Character.class);
  215.  
    return classSet.contains(clazz);
  216.  
    }
  217.  
     
  218.  
    /**
  219.  
    * 获取请求体JSON字符串
  220.  
    */
  221.  
    private String getRequestBody(NativeWebRequest webRequest) {
  222.  
    HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
  223.  
     
  224.  
    // 有就直接获取
  225.  
    String jsonBody = (String) webRequest.getAttribute(JSONBODY_ATTRIBUTE, NativeWebRequest.SCOPE_REQUEST);
  226.  
    // 没有就从请求中读取
  227.  
    if (jsonBody == null) {
  228.  
    try {
  229.  
    jsonBody = IOUtils.toString(servletRequest.getReader());
  230.  
    webRequest.setAttribute(JSONBODY_ATTRIBUTE, jsonBody, NativeWebRequest.SCOPE_REQUEST);
  231.  
    } catch (IOException e) {
  232.  
    throw new RuntimeException(e);
  233.  
    }
  234.  
    }
  235.  
    return jsonBody;
  236.  
    }
  237.  
    }

 

2、编写解析的方法注解:

  1.  
    package com.chujianyun.web.annotation;
  2.  
     
  3.  
    import java.lang.annotation.ElementType;
  4.  
    import java.lang.annotation.Retention;
  5.  
    import java.lang.annotation.RetentionPolicy;
  6.  
    import java.lang.annotation.Target;
  7.  
     
  8.  
    /**
  9.  
    * Controller中方法接收多个JSON对象
  10.  
    *
  11.  
    * @author 明明如月
  12.  
    * @date 2018/08/27
  13.  
    */
  14.  
    @Target(ElementType.PARAMETER)
  15.  
    @Retention(RetentionPolicy.RUNTIME)
  16.  
    public @interface MultiRequestBody {
  17.  
    /**
  18.  
    * 是否必须出现的参数
  19.  
    */
  20.  
    boolean required() default true;
  21.  
     
  22.  
    /**
  23.  
    * 当value的值或者参数名不匹配时,是否允许解析最外层属性到该对象
  24.  
    */
  25.  
    boolean parseAllFields() default true;
  26.  
     
  27.  
    /**
  28.  
    * 解析时用到的JSON的key
  29.  
    */
  30.  
    String value() default "";
  31.  
    }

3、在配置Bean中注入

特别注意: 如果加入本配置导致页面访问404 可以去掉 @EnableWebMvc注解

  1.  
    package com.chujianyun.web.config;
  2.  
     
  3.  
    import com.chujianyun.web.bean.MultiRequestBodyArgumentResolver;
  4.  
    import org.springframework.context.annotation.Bean;
  5.  
    import org.springframework.context.annotation.Configuration;
  6.  
    import org.springframework.http.converter.HttpMessageConverter;
  7.  
    import org.springframework.http.converter.StringHttpMessageConverter;
  8.  
    import org.springframework.web.method.support.HandlerMethodArgumentResolver;
  9.  
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
  10.  
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
  11.  
     
  12.  
    import java.nio.charset.Charset;
  13.  
    import java.util.List;
  14.  
     
  15.  
    /**
  16.  
    * 添加多RequestBody解析器
  17.  
    * @author 明明如月
  18.  
    * @date 2018/08/27
  19.  
    */
  20.  
    @Configuration
  21.  
    @EnableWebMvc
  22.  
    public class WebConfig extends WebMvcConfigurerAdapter {
  23.  
    @Override
  24.  
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
  25.  
    argumentResolvers.add(new MultiRequestBodyArgumentResolver());
  26.  
    }
  27.  
     
  28.  
    @Bean
  29.  
    public HttpMessageConverter<String> responseBodyConverter() {
  30.  
    return new StringHttpMessageConverter(Charset.forName("UTF-8"));
  31.  
    }
  32.  
     
  33.  
    @Override
  34.  
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
  35.  
    super.configureMessageConverters(converters);
  36.  
    converters.add(responseBodyConverter());
  37.  
    }
  38.  
    }

 

xml配置方式(感谢网友 "熔 岩"提供了的xml参考配置方式)

  1.  
    <mvc:annotation-driven>
  2.  
    <mvc:message-converters>
  3.  
    <bean class="org.springframework.http.converter.StringHttpMessageConverter">
  4.  
    <constructor-arg value="UTF-8"/>
  5.  
    </bean>
  6.  
    <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
  7.  
    <property name="supportedMediaTypes">
  8.  
    <list>
  9.  
    <value>application/json</value>
  10.  
    <value>text/html</value>
  11.  
    <value>text/plain</value>
  12.  
    </list>
  13.  
    </property>
  14.  
    <property name="fastJsonConfig" ref="fastJsonConfig"/>
  15.  
    </bean>
  16.  
    </mvc:message-converters>
  17.  
     
  18.  
    <mvc:argument-resolvers>
  19.  
    <bean class="io.github.chujianyun.bean.MultiRequestBodyArgumentResolver"/>
  20.  
    </mvc:argument-resolvers>
  21.  
    </mvc:annotation-driven>

使用方法:

  1.  
    package com.chujianyun.web.controller;
  2.  
     
  3.  
    import com.chujianyun.web.annotation.MultiRequestBody;
  4.  
    import com.chujianyun.web.domain.Dog;
  5.  
    import com.chujianyun.web.domain.User;
  6.  
    import org.springframework.stereotype.Controller;
  7.  
    import org.springframework.web.bind.annotation.RequestMapping;
  8.  
    import org.springframework.web.bind.annotation.ResponseBody;
  9.  
     
  10.  
    /**
  11.  
    * 演示控制器
  12.  
    * @author 明明如月
  13.  
    * @date 2018/08/27
  14.  
    */
  15.  
    @Controller
  16.  
    @RequestMapping("/xhr/test")
  17.  
    public class DemoController {
  18.  
     
  19.  
    @RequestMapping("/demo")
  20.  
    @ResponseBody
  21.  
    public String multiRequestBodyDemo1(@MultiRequestBody Dog dog, @MultiRequestBody User user) {
  22.  
    System.out.println(dog.toString()+user.toString());
  23.  
    return dog.toString()+";"+user.toString();
  24.  
    }
  25.  
     
  26.  
     
  27.  
    @RequestMapping("/demo2")
  28.  
    @ResponseBody
  29.  
    public String multiRequestBodyDemo2(@MultiRequestBody("dog") Dog dog, @MultiRequestBody User user) {
  30.  
    System.out.println(dog.toString()+user.toString());
  31.  
    return dog.toString()+";"+user.toString();
  32.  
    }
  33.  
     
  34.  
    @RequestMapping("/demo3")
  35.  
    @ResponseBody
  36.  
    public String multiRequestBodyDemo3(@MultiRequestBody("dog") Dog dog, @MultiRequestBody("user") User user) {
  37.  
    System.out.println(dog.toString()+user.toString());
  38.  
    return dog.toString()+";"+user.toString();
  39.  
    }
  40.  
     
  41.  
     
  42.  
     
  43.  
    @RequestMapping("/demo4")
  44.  
    @ResponseBody
  45.  
    public String multiRequestBodyDemo4(@MultiRequestBody("dog") Dog dog, @MultiRequestBody Integer age) {
  46.  
    System.out.println(dog.toString() + age.toString());
  47.  
    return dog.toString() + ";age属性为:"+age.toString();
  48.  
    }
  49.  
     
  50.  
     
  51.  
    @RequestMapping("/demo5")
  52.  
    @ResponseBody
  53.  
    public String multiRequestBodyDemo5(@MultiRequestBody("color") String color, @MultiRequestBody("age") Integer age) {
  54.  
    return "color="+color + "; age=" + age;
  55.  
    }
  56.  
     
  57.  
    @RequestMapping("/demo6")
  58.  
    @ResponseBody
  59.  
    public String multiRequestBodyDemo6(@MultiRequestBody("dog") Dog dog, @MultiRequestBody Integer age) {
  60.  
    System.out.println(dog.toString() + age.toString());
  61.  
    return dog.toString() + ";age属性为:"+age.toString();
  62.  
    }
  63.  
     
  64.  
     
  65.  
    @RequestMapping("/demo7")
  66.  
    @ResponseBody
  67.  
    public String multiRequestBodyDemo7(@MultiRequestBody Dog color2, @MultiRequestBody("age") Integer age) {
  68.  
    return "color="+color2 + "; age=" + age;
  69.  
    }
  70.  
     
  71.  
     
  72.  
    @RequestMapping("/demo9")
  73.  
    @ResponseBody
  74.  
    public String multiRequestBodyDemo9( @MultiRequestBody Dog dog) {
  75.  
    return dog.toString();
  76.  
    }
  77.  
    @RequestMapping("/demo10")
  78.  
    @ResponseBody
  79.  
    public String multiRequestBodyDemo10( @MultiRequestBody(parseAllFields = false,required = false) Dog dog) {
  80.  
    return dog.toString();
  81.  
    }
  82.  
     
  83.  
    @RequestMapping("/testList")
  84.  
    @ResponseBody
  85.  
    public String multiRequestBodyDemo1(@MultiRequestBody List test, @MultiRequestBody String str) {
  86.  
     
  87.  
    return test.toString() + str;
  88.  
    }
  89.  
     
  90.  
    }

两个实体:

  1.  
    package com.chujianyun.web.domain;
  2.  
     
  3.  
    /**
  4.  
    * @author 明明如月
  5.  
    * @date 2018/08/27
  6.  
    */
  7.  
    public class Dog {
  8.  
     
  9.  
    private String name;
  10.  
     
  11.  
    private String color;
  12.  
     
  13.  
     
  14.  
    public String getName() {
  15.  
    return name;
  16.  
    }
  17.  
     
  18.  
    public void setName(String name) {
  19.  
    this.name = name;
  20.  
    }
  21.  
     
  22.  
    public String getColor() {
  23.  
    return color;
  24.  
    }
  25.  
     
  26.  
    public void setColor(String color) {
  27.  
    this.color = color;
  28.  
    }
  29.  
     
  30.  
    @Override
  31.  
    public String toString() {
  32.  
    return "Dog{" +
  33.  
    "name='" + name + '\'' +
  34.  
    ", color='" + color + '\'' +
  35.  
    '}';
  36.  
    }
  37.  
    }
  1.  
    package com.chujianyun.web.domain;
  2.  
     
  3.  
    /**
  4.  
    * @author 明明如月
  5.  
    * @date 2018/08/27
  6.  
    */
  7.  
    public class User {
  8.  
     
  9.  
    private String name;
  10.  
     
  11.  
    private Integer age;
  12.  
     
  13.  
    public String getName() {
  14.  
    return name;
  15.  
    }
  16.  
     
  17.  
    public void setName(String name) {
  18.  
    this.name = name;
  19.  
    }
  20.  
     
  21.  
    public Integer getAge() {
  22.  
    return age;
  23.  
    }
  24.  
     
  25.  
    public void setAge(Integer age) {
  26.  
    this.age = age;
  27.  
    }
  28.  
     
  29.  
    @Override
  30.  
    public String toString() {
  31.  
    return "User{" +
  32.  
    "name='" + name + '\'' +
  33.  
    ", age=" + age +
  34.  
    '}';
  35.  
    }
  36.  
    }

效果:

demo 

demo2

demo3

 

参考文章:https://stackoverflow.com/questions/12893566/passing-multiple-variables-in-requestbody-to-a-spring-mvc-controller-using-ajax

 
 
posted @ 2021-01-27 19:18  牧之丨  阅读(641)  评论(0编辑  收藏  举报