Correct posture of multiple @ requestbodies in SpringBoot Controller

Recently, I encountered the situation that multiple @ requestbodies are required in the Controller, but I found that this writing method is not supported,

This leads to

1. A single string and other packing types need to write an object before they can be received with @ RequestBody;

2. Multiple objects need to be encapsulated in one object before they can be received with @ RequestBody.

Referring to StackOverFlow, inspired by a solution, I improved it to the following version and gave detailed comments, hoping to be helpful to you.

Improved program support:

1. Support parsing objects by specifying JSON key through annotated value.

2. It supports parsing objects directly according to parameter names through annotation without value

3. Support GET mode and other request modes

4. Support basic type attribute injection

5. Support parsing the object according to the attribute when the annotation has no value and the parameter name does not match the JSON string key.

6. Support redundant attributes (no parsing, no error reporting), support parameter "sharing" (when value is not specified, the parameter name is not the key of JSON string)

7. Support whether the object matches all attributes when the value and attribute name cannot find a matching key.

Important update records:

New xml mode reference configuration on February 25, 2019

February 7, 2019 fix when the list parameter is empty, parametertype Newinstance causes an exception.

On December 28, 2018, new test cases were added to improve the analysis part of the code

Improve the project format on October 23, 2018

The first edition was created on August 28, 2018

The project is for reference only. Please be responsible for any problems caused by improper use. If there are problems, please discuss and improve them.

Project address (it is recommended to pull the latest code):

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

In addition, the code should be continuously updated and improved as much as possible. You are welcome to contribute code.

The steps are as follows:

0. Main Maven dependencies involved except the Jar package of spring

            commons-lang
            commons-lang
            2.4
        

        
            com.alibaba
            fastjson
            1.2.35
        

        
            commons-io
            commons-io
            2.6

fastjson is used to parse json objects, commons Lang is used to empty the string (you can also write by yourself), and Commons IO is used to encapsulate the read request as a string type (you can also encapsulate by yourself).

1. Rewrite method parameter parser

package com.chujianyun.web.bean;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.github.chujianyun.annotation.MultiRequestBody;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
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.IOException;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;

/**
 * Multi RequestBody parser
 *
 * @author As bright as the moon
 * @date 2018/08/27
 */
public class MultiRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {

  private static final String JSONBODY_ATTRIBUTE = "JSON_REQUEST_BODY";

  /**
   * Set supported method parameter types
   *
   * @param parameter Method parameters
   * @return Supported types
   */
  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    // Parameters with @ MultiRequestBody annotation are supported
    return parameter.hasParameterAnnotation(MultiRequestBody.class);
  }

  /**
   * Parameter analysis, using fastjson
   * Note: if a non basic type returns null, it will report a null pointer exception. Create a null object through reflection or JSON tool class
   */
  @Override
  public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

    String jsonBody = getRequestBody(webRequest);

    JSONObject jsonObject = JSON.parseObject(jsonBody);
    // According to the @ MultiRequestBody annotation, value is used as the key parsed by json
    MultiRequestBody parameterAnnotation = parameter.getParameterAnnotation(MultiRequestBody.class);
    //The value of the annotation is the key of JSON
    String key = parameterAnnotation.value();
    Object value;
    // If the @ MultiRequestBody annotation does not set value, the parameter name FrameworkServlet is taken as the key of json parsing
    if (StringUtils.isNotEmpty(key)) {
      value = jsonObject.get(key);
      // If value is set but cannot be parsed, an error is reported
      if (value == null && parameterAnnotation.required()) {
        throw new IllegalArgumentException(String.format("required param %s is not present", key));
      }
    } else {
      // If the annotation is set to value, the parameter name is used as the key of json
      key = parameter.getParameterName();
      value = jsonObject.get(key);
    }

    // Gets the type Long after the annotation
    Class parameterType = parameter.getParameterType();
    // By parsing the annotated value or parameter name, you can get the value for parsing
    if (value != null) {
      //Basic type
      if (parameterType.isPrimitive()) {
        return parsePrimitive(parameterType.getName(), value);
      }
      // Basic type packing class
      if (isBasicDataTypes(parameterType)) {
        return parseBasicTypeWrapper(parameterType, value);
        // String type
      } else if (parameterType == String.class) {
        return value.toString();
      }
      // Other complex objects
      return JSON.parseObject(value.toString(), parameterType);
    }

    // If it cannot be resolved, the whole json string will be resolved to the current parameter type
    if (isBasicDataTypes(parameterType)) {
      if (parameterAnnotation.required()) {
        throw new IllegalArgumentException(String.format("required param %s is not present", key));
      } else {
        return null;
      }
    }

    // If it is not a basic type, it is not allowed to parse all fields. If it is a required parameter, an error will be reported, and if it is not a required parameter, null will be returned
    if (!parameterAnnotation.parseAllFields()) {
      // If it is a required parameter, throw an exception
      if (parameterAnnotation.required()) {
        throw new IllegalArgumentException(String.format("required param %s is not present", key));
      }
      // Otherwise, null is returned
      return null;
    }
    // It is not a basic type. It can be parsed and the outer attribute can be parsed
    Object result;
    try {
      result = JSON.parseObject(jsonObject.toString(), parameterType);
    } catch (JSONException jsonException) {
      // TODO:: is it reasonable for exception handling to return null?
      result = null;
    }

    // If unnecessary parameters are returned directly, otherwise, if no attribute has a value, an error will be reported
    if (!parameterAnnotation.required()) {
      return result;
    } else {
      boolean haveValue = false;
      Field[] declaredFields = parameterType.getDeclaredFields();
      for (Field field : declaredFields) {
        field.setAccessible(true);
        if (field.get(result) != null) {
          haveValue = true;
          break;
        }
      }
      if (!haveValue) {
        throw new IllegalArgumentException(String.format("required param %s is not present", key));
      }
      return result;
    }
  }

  /**
   * Basic type resolution
   */
  private Object parsePrimitive(String parameterTypeName, Object value) {
    final String booleanTypeName = "boolean";
    if (booleanTypeName.equals(parameterTypeName)) {
      return Boolean.valueOf(value.toString());
    }
    final String intTypeName = "int";
    if (intTypeName.equals(parameterTypeName)) {
      return Integer.valueOf(value.toString());
    }
    final String charTypeName = "char";
    if (charTypeName.equals(parameterTypeName)) {
      return value.toString().charAt(0);
    }
    final String shortTypeName = "short";
    if (shortTypeName.equals(parameterTypeName)) {
      return Short.valueOf(value.toString());
    }
    final String longTypeName = "long";
    if (longTypeName.equals(parameterTypeName)) {
      return Long.valueOf(value.toString());
    }
    final String floatTypeName = "float";
    if (floatTypeName.equals(parameterTypeName)) {
      return Float.valueOf(value.toString());
    }
    final String doubleTypeName = "double";
    if (doubleTypeName.equals(parameterTypeName)) {
      return Double.valueOf(value.toString());
    }
    final String byteTypeName = "byte";
    if (byteTypeName.equals(parameterTypeName)) {
      return Byte.valueOf(value.toString());
    }
    return null;
  }

  /**
   * Basic type wrapper class parsing
   */
  private Object parseBasicTypeWrapper(Class parameterType, Object value) {
    if (Number.class.isAssignableFrom(parameterType)) {
      Number number = (Number) value;
      if (parameterType == Integer.class) {
        return number.intValue();
      } else if (parameterType == Short.class) {
        return number.shortValue();
      } else if (parameterType == Long.class) {
        return number.longValue();
      } else if (parameterType == Float.class) {
        return number.floatValue();
      } else if (parameterType == Double.class) {
        return number.doubleValue();
      } else if (parameterType == Byte.class) {
        return number.byteValue();
      }
    } else if (parameterType == Boolean.class) {
      return value.toString();
    } else if (parameterType == Character.class) {
      return value.toString().charAt(0);
    }
    return null;
  }

  /**
   * Judge whether it is a basic data type packing class
   */
  private boolean isBasicDataTypes(Class clazz) {
    Set classSet = new HashSet<>();
    classSet.add(Integer.class);
    classSet.add(Long.class);
    classSet.add(Short.class);
    classSet.add(Float.class);
    classSet.add(Double.class);
    classSet.add(Boolean.class);
    classSet.add(Byte.class);
    classSet.add(Character.class);
    return classSet.contains(clazz);
  }

  /**
   * Get request body JSON string
   */
  private String getRequestBody(NativeWebRequest webRequest) {
    HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);

    // Get it directly if you have it
    String jsonBody = (String) webRequest.getAttribute(JSONBODY_ATTRIBUTE, NativeWebRequest.SCOPE_REQUEST);
    // Read from request without
    if (jsonBody == null) {
      try {
        jsonBody = IOUtils.toString(servletRequest.getReader());
        webRequest.setAttribute(JSONBODY_ATTRIBUTE, jsonBody, NativeWebRequest.SCOPE_REQUEST);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
    return jsonBody;
  }
}

2. Write method notes for parsing:

package com.chujianyun.web.annotation;

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

/**
 * Controller Method in receives multiple JSON objects
 *
 * @author As bright as the moon
 * @date 2018/08/27
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiRequestBody {
    /**
     * Required parameter
     */
    boolean required() default true;

    /**
     * Is it allowed to resolve the outermost attribute to the object when the value or parameter name of value do not match
     */
    boolean parseAllFields() default true;

    /**
     * JSON used when parsing key
     */
    String value() default "";
}

3. Inject in configuration Bean

Special note: if adding this configuration leads to page access 404, the @ EnableWebMvc annotation can be removed

package com.chujianyun.web.config;

import com.chujianyun.web.bean.MultiRequestBodyArgumentResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.nio.charset.Charset;
import java.util.List;

/**
 * Add multi RequestBody parser
 * @author As bright as the moon
 * @date 2018/08/27
 */
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addArgumentResolvers(List argumentResolvers) {
        argumentResolvers.add(new MultiRequestBodyArgumentResolver());
    }

    @Bean
    public HttpMessageConverter responseBodyConverter() {
        return new StringHttpMessageConverter(Charset.forName("UTF-8"));
    }

    @Override
    public void configureMessageConverters(List> converters) {
        super.configureMessageConverters(converters);
        converters.add(responseBodyConverter());
    }
}

xml configuration mode (thanks for the xml reference configuration mode provided by the netizen "lava")

                        application/json
                        text/html
                        text/plain

usage method:

package com.chujianyun.web.controller;

import com.chujianyun.web.annotation.MultiRequestBody;
import com.chujianyun.web.domain.Dog;
import com.chujianyun.web.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * Demo controller
 * @author As bright as the moon
 * @date 2018/08/27
 */
@Controller
@RequestMapping("/xhr/test")
public class DemoController {

    @RequestMapping("/demo")
    @ResponseBody
     public String multiRequestBodyDemo1(@MultiRequestBody Dog dog, @MultiRequestBody User user) {
        System.out.println(dog.toString()+user.toString());
        return dog.toString()+";"+user.toString();
    }


    @RequestMapping("/demo2")
    @ResponseBody
    public String multiRequestBodyDemo2(@MultiRequestBody("dog") Dog dog, @MultiRequestBody User user) {
        System.out.println(dog.toString()+user.toString());
        return dog.toString()+";"+user.toString();
    }

    @RequestMapping("/demo3")
    @ResponseBody
    public String multiRequestBodyDemo3(@MultiRequestBody("dog") Dog dog, @MultiRequestBody("user") User user) {
        System.out.println(dog.toString()+user.toString());
        return dog.toString()+";"+user.toString();
    }



    @RequestMapping("/demo4")
    @ResponseBody
    public String multiRequestBodyDemo4(@MultiRequestBody("dog") Dog dog, @MultiRequestBody Integer age) {
        System.out.println(dog.toString() + age.toString());
        return dog.toString() + ";age The attributes are:"+age.toString();
    }


    @RequestMapping("/demo5")
    @ResponseBody
    public String multiRequestBodyDemo5(@MultiRequestBody("color") String color, @MultiRequestBody("age") Integer age) {
        return "color="+color + "; age=" + age;
    }

    @RequestMapping("/demo6")
    @ResponseBody
    public String multiRequestBodyDemo6(@MultiRequestBody("dog") Dog dog, @MultiRequestBody Integer age) {
        System.out.println(dog.toString() + age.toString());
        return dog.toString() + ";age The attributes are:"+age.toString();
    }


    @RequestMapping("/demo7")
    @ResponseBody
    public String multiRequestBodyDemo7(@MultiRequestBody Dog color2, @MultiRequestBody("age") Integer age) {
        return "color="+color2 + "; age=" + age;
    }


    @RequestMapping("/demo9")
    @ResponseBody
    public String multiRequestBodyDemo9( @MultiRequestBody Dog dog) {
        return dog.toString();
    }
   @RequestMapping("/demo10")
    @ResponseBody
    public String multiRequestBodyDemo10( @MultiRequestBody(parseAllFields = false,required = false) Dog dog) {
        return dog.toString();
    }

  @RequestMapping("/testList")
  @ResponseBody
  public String multiRequestBodyDemo1(@MultiRequestBody List test, @MultiRequestBody String str) {

    return test.toString() + str;
  }

}

Two entities:

package com.chujianyun.web.domain;

/**
 * @author As bright as the moon
 * @date 2018/08/27
 */
public class Dog {

    private String name;

    private String color;


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                '}';
    }
}
package com.chujianyun.web.domain;

/**
 * @author As bright as the moon
 * @date 2018/08/27
 */
public class User {

    private String name;

    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

effect:

demo 

demo2

demo3

Reference article: https://stackoverflow.com/questions/12893566/passing-multiple-variables-in-requestbody-to-a-spring-mvc-controller-using-ajax

Added by TheRealPenguin on Tue, 15 Feb 2022 03:41:13 +0200