定制小程序开发费用常规情况下, 因为 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) {}}
- 1
- 2
- 3
- 4
- 5
- 6
并使用这样的 JSON 进行请求:
{ "param1":{}, "param2":{}}
- 1
- 2
- 3
- 4
我们有多个解决办法
方法一:
没有什么是多包一层解决不了的,如果有,那么久再来一层~~~
Controller:
@RestController@RequestMapping("/demo")public class DemoController { @RequestMapping(value = "/test", method = RequestMethod.POST) public String test(@RequestBody Param param) {}}
- 1
- 2
- 3
- 4
- 5
- 6
然后请求用这个 JSON
{ "param":{ "param1":{}, "param2":{} }}
- 1
- 2
- 3
- 4
- 5
- 6
嗯……是这样没错了,有问题吗?没有问题。。。。没有什么是套娃解决不了的
方法二:
上面的方法显然没有问题,只要我封装的够多,我就能解决所有问题,当然我们不推荐这么干
虽然 @RequestBody 必须映射到单个对象,但是对象可以是一个 map,所以我们有了第二种方法
@RestController@RequestMapping("/demo")public class DemoController { @RequestMapping(value = "/test", method = RequestMethod.POST) public String test(@RequestBody Map<String, String> json) {}}
- 1
- 2
- 3
- 4
- 5
- 6
或者使用 Jackson 的 ObjectNode
@RestController@RequestMapping("/demo")public class DemoController { @RequestMapping(value = "/test", method = RequestMethod.POST) public String test(@RequestBody ObjectNode json) {}}
- 1
- 2
- 3
- 4
- 5
- 6
这样我们可以很容易的传递一个 JSON 串过去了,反正它会给我们解析成一个个键值对的对吧~~~
{ "param":{ "param1":{}, "param2":{} }}
- 1
- 2
- 3
- 4
- 5
- 6
这样做有错吗?当然没错!虽然使用 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) {}}
- 1
- 2
- 3
- 4
- 5
- 6
以下代码使用的 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;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
实现 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); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
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); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
让配置被 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(); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
在 resource 下创建文件夹 META-INF > spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.sqd.config.SpringMvcConfigurerAutoConfig
- 1
- 2
最后附上 gitgub 代码地址:
文章参考: