使用多个 @RequestBody 接收参数传递给 Controller

8 篇文章 0 订阅
6 篇文章 0 订阅

常规情况下, 因为 request 请求的 body 只能读取一次,我们使用 @RequestBody 只能解析一次,如果在方法参数中增加第二个 @RequestBody 注解的话,stream 流已经关闭,无法读取,返回 400 错误

我们想要这么做:在一个 Controller 提供的接口中,使用多个 @RequestBody 注解接收参数

@RestController
@RequestMapping("/demo")
public class DemoController {
    @RequestMapping(value = "/test", method = RequestMethod.POST)
    public String test(@RequestBody Param1 param1, @RequestBody param2 param2) {}
}

并使用这样的 JSON 进行请求:

{
    "param1":{},
    "param2":{}
}

我们有多个解决办法

方法一:

没有什么是多包一层解决不了的,如果有,那么久再来一层~~~

Controller:

@RestController
@RequestMapping("/demo")
public class DemoController {
    @RequestMapping(value = "/test", method = RequestMethod.POST)
    public String test(@RequestBody Param param) {}
}

然后请求用这个 JSON

{
    "param":{
        "param1":{},
        "param2":{}
    }
}

嗯……是这样没错了,有问题吗?没有问题。。。。没有什么是套娃解决不了的

方法二:

上面的方法显然没有问题,只要我封装的够多,我就能解决所有问题,当然我们不推荐这么干

虽然 @RequestBody 必须映射到单个对象,但是对象可以是一个 map,所以我们有了第二种方法

@RestController
@RequestMapping("/demo")
public class DemoController {
    @RequestMapping(value = "/test", method = RequestMethod.POST)
    public String test(@RequestBody Map<String, String> json) {}
}

或者使用 Jackson 的 ObjectNode

@RestController
@RequestMapping("/demo")
public class DemoController {
    @RequestMapping(value = "/test", method = RequestMethod.POST)
    public String test(@RequestBody ObjectNode json) {}
}

这样我们可以很容易的传递一个 JSON 串过去了,反正它会给我们解析成一个个键值对的对吧~~~

{
    "param":{
        "param1":{},
        "param2":{}
    }
}

这样做有错吗?当然没错!虽然使用 map 让前端来请求后端接口很方便,既不用定义类,想塞什么数据就放什么,后端统一 map 接收,但我个人还是不太赞成的!!!原因就仁者见仁智者见智了

  • 使用 map 总给我一种面向 JSON 编程的感觉,而不是面向对象
  • 如果使用 map 进行传参的话,不看代码根本不知道里面放了什么,别人也能随便塞一些东西进去
  • 判空?@NotNull 是什么?map 传参还想判空?老老实实手动一个个来吧
  • 每次拿到数据都要自己转一下,还有可能发生类型转换异常
  • 方法调用之间使用 map 的话,不太方便我们维护,而且 JSON 没有携带我们的类型信息,反序列化可能会出现问题
  • 维护不便,参数验证,接口文档等等都是问题呀、、、

方法三:

让我们来试试第三种方法,自定义注解,并将它注册到 Spring MVC,像是这样:

@RestController
@RequestMapping("/demo")
public class DemoController {
    @RequestMapping(value = "/test", method = RequestMethod.POST)
    public String test(@MultiRequestBody Param1 param1, @MultiRequestBody Param2 param2) {}
}

以下代码使用的 Jackson 进行参数解析,可自行替换为 fastJson

首先自定义一个注解 @MultiRequestBody 当然,取什么名字随意
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author sqd
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiRequestBody {

    /**
     * 解析时用到的 JSON 中的 key
     */
    String value() default "";

    /**
     * 是否必传的参数
     */
    boolean required() default true;

    /**
     * 当 value 的值或者参数名不匹配时,是否允许解析最外层属性得到该对象
     */
    boolean parseAllFields() default true;

}

实现 HandlerMethodArgumentResolver,用来解析使用了我们定义了注解的参数
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sqd.annotation.MultiRequestBody;
import com.sqd.util.IOUtils;
import com.sqd.util.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
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.BufferedReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;

/**
 * MultiRequestBody 解析器
 * 1、支持通过注解的 value 指定 JSON 的 key 来解析对象
 * 2、支持通过注解无 value,直接根据参数名来解析对象
 * 3、支持基本类型的注入
 * 4、支持通过注解无 value 且参数名不匹配 JSON 串的 key 时,根据属性解析对象
 *
 * @author sqd
 */
public class MultiRequestBodyResolver implements HandlerMethodArgumentResolver {

    private static final Set<Class> classSet = new HashSet<>(16);
    private static ObjectMapper objectMapper = new ObjectMapper();
    private static final String JSON_REQUEST_BODY = "JSON_REQUEST_BODY";

    static {
        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);
    }

    /**
     * 支持的方法参数类型
     *
     * @see MultiRequestBody
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(MultiRequestBody.class);
    }

    /**
     * 参数解析
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        Object result;
        Object value;

        // 获取请求体
        String requestBody = getRequestBody(webRequest);
        // 允许使用不带引号的字段
        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        // 解析 JSON 串
        JsonNode rootNode = objectMapper.readTree(requestBody);
        // JSON 串为空抛出异常
        Assert.notNull(rootNode, String.format("param %s parsing failed", requestBody));

        // 获取注解
        MultiRequestBody multiRequestBody = parameter.getParameterAnnotation(MultiRequestBody.class);
        Assert.notNull(multiRequestBody, String.format("param %s parsing failed", requestBody));

        String key = multiRequestBody.value();
        // 根据注解 value 解析 JSON 串,如果没有根据参数的名字解析 JSON
        if (StringUtils.isNotBlank(key)) {
            value = rootNode.get(key);
            // 如果为参数必填但未根据 key 成功得到对应 value 抛出异常
            Assert.isTrue(multiRequestBody.required() && Objects.nonNull(value), String.format("required param %s is not present", key));
        } else {
            key = parameter.getParameterName();
            value = rootNode.get(key);
        }

        // 获取参数的类型
        Class<?> paramType = parameter.getParameterType();
        // 成功从 JSON 解析到对应 key 的 value
        if (Objects.nonNull(value)) {
            return objectMapper.readValue(value.toString(), paramType);
        }

        // 未从 JSON 解析到对应 key(可能是注解的 value 或者是参数名字) 的值,要么没传值,要么传的名字不对
        // 如果参数为基本数据类型,且为必传参数抛出异常
        Assert.isTrue(!(isBasicDataTypes(paramType) && multiRequestBody.required()), String.format("required param %s is not present", key));
        // 参数非基本数据类型,如果不允许解析外层属性,且为必传参数报错抛出异常
        Assert.isTrue(!(!multiRequestBody.parseAllFields() && multiRequestBody.required()), String.format("required param %s is not present", key));

        try {
            // 既然找不到对应参数,而且非基本类型,我们可以解析外层属性,将整个 JSON 作为参数进行解析。解析失败会抛出异常
            result = objectMapper.readValue(requestBody, paramType);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        // 必填参数的话,看解析出来的参数是否对应,非必填直接返回吧
        if (multiRequestBody.required()) {
            Field[] declaredFields = paramType.getDeclaredFields();
            for (Field field : declaredFields) {
                field.setAccessible(true);
                Assert.notNull(field.get(result), String.format("required param %s is not present", key));
            }
        }
        return result;
    }

    /**
     * 获取请求 JSON 字符串
     */
    private String getRequestBody(NativeWebRequest webRequest) {
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        String jsonBody = (String) webRequest.getAttribute(JSON_REQUEST_BODY, NativeWebRequest.SCOPE_REQUEST);
        if (StringUtils.isEmpty(jsonBody)) {
            try (BufferedReader reader = servletRequest.getReader()) {
                jsonBody = IOUtils.toString(reader);
                webRequest.setAttribute(JSON_REQUEST_BODY, jsonBody, NativeWebRequest.SCOPE_REQUEST);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return jsonBody;
    }

    /**
     * 判断是否为基本数据类型包装类
     */
    private boolean isBasicDataTypes(Class clazz) {
        return classSet.contains(clazz);
    }

}
Web MVC 配置,实现 WebMvcConfigurer 接口,将我们的参数解析器添加,然后交给 Spring 管理
import com.sqd.resolver.MultiRequestBodyResolver;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.*;

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

public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 参数解析器
     * @see MultiRequestBodyResolver
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new MultiRequestBodyResolver());
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter jacksonConver = new MappingJackson2HttpMessageConverter();
        ArrayList<MediaType> mediaTypes = new ArrayList<>();
        mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        jacksonConver.setSupportedMediaTypes(mediaTypes);
        jacksonConver.setDefaultCharset(Charset.forName("UTF-8"));
        converters.add(jacksonConver);
    }
让配置被 spring 管理
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@ConditionalOnClass(WebMvcConfigurer.class)
public class SpringMvcConfigurerAutoConfig {

    @Bean
    @ConditionalOnMissingClass
    public WebMvcConfig webMvcConfig(){
        return new WebMvcConfig();
    }
}

在 resource 下创建文件夹 META-INF > spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.sqd.config.SpringMvcConfigurerAutoConfig

最后附上 gitgub 代码地址:https://github.com/sunqingda/project/tree/muilt-requestbody-project

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一起来搬砖呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值