当前位置:   article > 正文

后台接收Json请求参数兼容数组和单个对象

java 后端接收数组的方式有很多吗 后端接收json数组 苏格团队
  • 苏格团队
  • 作者:宇你平安

背景

  • 场景一:前后端对接接口,增删改查,一开始请求的参数,基本是单条数据,json格式基本是{"key":"value"},产品后续扩展,传参变成批量操作json格式为[xxx,xxx]或者[{"key":"value"}],此时后端修改原接口的接收对象为数组的话,前后端灰度发布,就会存在旧版本不兼容

  • 场景二:产品的客户端,可能由web端,PC端,App端组成,例如当某个接口的参数结构改造为数组时,web端更新了,而App和PC端未更新,就存在不兼容其他端

解决思路

  1. 新增接口
  • 优点:不影响旧接口,影响范围小

  • 缺点:重复的代码,后期存在无用的接口

  1. 前后端一开始约定数组的请求参数
  • 优点:比较根本解决问题

  • 缺点:程序员的缺点,不是所有程序员都能预先判断接口参数的类型

  1. 绝大多数情况是遇到问题解决问题,思路是后端拦截处理接收的请求参数,校验正确后,统一将json数据封装为一个通用对象或者数组
  • 优点:只需重构原先的接口即可兼容两种情况
  • 缺点:需要自定义json的解析,解析不好会报json反序化失败

上代码

以下是尝试用三种方法解决以上场景的过程

定义一个接收前端的实体类MyBeanVo

  1. package com.test.config;
  2. public class MyBeanVo {
  3. String value = "";
  4. public String getValue() {
  5. return value;
  6. }
  7. public void setValue(String value) {
  8. this.value = value;
  9. }
  10. }
  11. 复制代码

可变参数(不能解决)

开始以为Java中的可变参数Object...,在调用方法时,既可以传单个参数,又可以传多个参数,但是不能解决。因为可变参数实际上是Object[]数组

  1. @RestController
  2. public class MyController {
  3. @PostMapping("/hello")
  4. public String test(@RequestBody MyBeanVo... param) {
  5. MyBeanVo vo = param[0];
  6. return vo.getValue();
  7. }
  8. }
  9. 复制代码

传单个参数时报错: "exception":"org.springframework.http.converter.HttpMessageNotReadableException","message":"JSON parse error: Can not deserialize instance of com.test.config.MyBean[] out of START_OBJECT token; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of com.test.config.MyBean[] out of START_OBJECT token\n at [Source: java.io.PushbackInputStream@24e6f1b2; line: 1, column: 1]"

原因:前端的参数(单个数据)无法解析为MyBean[],而这涉及到了Json的反序列化

自定义反序列化

方案一

定义一个批量实体类

  1. package com.test.config;
  2. import java.util.List;
  3. public class BatchVo<T> {
  4. List<T> list;
  5. public List<T> getList() {
  6. return list;
  7. }
  8. public void setList(List<T> list) {
  9. this.list = list;
  10. }
  11. }
  12. 复制代码

@JsonComponent注解会自动注入到spring中,反序列化BatchVo<MyBeanVo>时会自动执行deserialize方法,但是有个弊端,JsonDeserializer<T>的T必须是具体类型,不能携带泛型,不同参数就有不同的Vo,要针对不同的Vo都写一个自定义反序化的类就很麻烦

  1. package com.test.config;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.fasterxml.jackson.core.JsonParser;
  4. import com.fasterxml.jackson.core.JsonProcessingException;
  5. import com.fasterxml.jackson.core.TreeNode;
  6. import com.fasterxml.jackson.databind.DeserializationContext;
  7. import com.fasterxml.jackson.databind.JsonDeserializer;
  8. import org.springframework.boot.jackson.JsonComponent;
  9. import java.io.IOException;
  10. import java.util.ArrayList;
  11. @JsonComponent
  12. public class MyJsonDeserializer extends JsonDeserializer<BatchVo<MyBeanVo>> {
  13. @Override
  14. public BatchVo<MyBeanVo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
  15. TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser);
  16. BatchVo vo = new BatchVo<MyBeanVo>();
  17. String str = treeNode.toString();
  18. // 前端传参是数组
  19. if (treeNode.isArray()) {
  20. vo.list = JSONObject.parseArray(str, MyBeanVo.class);
  21. }
  22. // 前端传参是单个数据
  23. if (treeNode.isObject()) {
  24. vo.list = new ArrayList();
  25. vo.list.add(JSONObject.parseObject(str, MyBeanVo.class));
  26. }
  27. return vo;
  28. }
  29. }
  30. 复制代码

绑定的参数必须加@RequestBody,不然反序列化无法走MyJsonDeserializer的deserialize方法

  1. @RestController
  2. public class MyController {
  3. @PostMapping("/hello")
  4. public String test(@RequestBody BatchVo<MyBeanVo>param) {
  5. MyBeanVo vo = param.getList().get(0);
  6. return vo.getValue();
  7. }
  8. }
  9. 复制代码

发起请求:POST localhost:8080/hello

body参数:[{"value":"hello world"}] 或者 {"value":"hello world"}

返回皆为:hello world

分析:明显这种设计除非MyBean可以设计得很强大、很通用,可以接收前端所有的请求参数。要不然每个Vo类都需要写一个实现JsonDeserializer的反序化列解析类,或者每次都需要在contrller层做Json的再次反序列化。这样的实现变得繁琐,增加代码量

方案二

自定参数解析器自定义参数解析器

  1. package com.test.config;
  2. import com.alibaba.fastjson.JSON;
  3. import org.apache.commons.io.IOUtils;
  4. import org.springframework.core.MethodParameter;
  5. import org.springframework.web.bind.support.WebDataBinderFactory;
  6. import org.springframework.web.context.request.NativeWebRequest;
  7. import org.springframework.web.method.support.HandlerMethodArgumentResolver;
  8. import org.springframework.web.method.support.ModelAndViewContainer;
  9. import javax.servlet.http.HttpServletRequest;
  10. import java.io.IOException;
  11. import java.lang.reflect.Type;
  12. import java.util.List;
  13. public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
  14. /**
  15. * 只拦截BatchBody注解且为数组的请求参数
  16. * 每个mapping的方法只会执行一次此方法
  17. */
  18. public boolean supportsParameter(MethodParameter methodParameter) {
  19. Class paramType = methodParameter.getParameterType();
  20. boolean isArray = paramType.isArray();
  21. boolean isList = paramType.isAssignableFrom(List.class);
  22. boolean hasAnnotation = methodParameter.hasParameterAnnotation(BatchBody.class);
  23. return hasAnnotation && (isArray || isList);
  24. }
  25. /**
  26. * 通过了supportsParameter校验的mapping方法每次都会执行此方法
  27. */
  28. public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
  29. String json = getRequestBodyString(nativeWebRequest);
  30. Type type = methodParameter.getGenericParameterType();
  31. Object obj = JSON.parseObject(json, type);
  32. return obj;
  33. }
  34. /**
  35. * 格式化json数据,统一为数组形式
  36. * 解析json字符串需做得更完善,例如校验json格式是否正确
  37. */
  38. private String getRequestBodyString(NativeWebRequest webRequest) throws IOException {
  39. HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
  40. String json = IOUtils.toString(request.getInputStream(), "UTF-8").trim();
  41. if (json.startsWith("{") && json.endsWith("}")) {
  42. return "[" + json + "]";
  43. }
  44. if (json.startsWith("[") && json.endsWith("]")) {
  45. return json;
  46. }
  47. return null;
  48. }
  49. }
  50. 复制代码

将RequestBodyArgumentResolver注册到WebMvcConfigurerAdapter当中。

  1. package com.test.config;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.web.method.support.HandlerMethodArgumentResolver;
  4. import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
  5. import java.util.List;
  6. @Configuration
  7. public class WebConfig extends WebMvcConfigurerAdapter {
  8. @Override
  9. public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
  10. argumentResolvers.add(new RequestBodyArgumentResolver());
  11. super.addArgumentResolvers(argumentResolvers);
  12. }
  13. }
  14. 复制代码

定义mapping接口,在参数上加上注解@BatchBody

  1. @RestController
  2. public class MyController {
  3. @PostMapping("/hello2")
  4. public String test2(@BatchBody MyBeanVo[] param) {
  5. MyBeanVo vo = param[0];
  6. return vo.getValue();
  7. }
  8. @PostMapping("/hello3")
  9. public String test3(@BatchBody List<MyBeanVo> param) {
  10. MyBeanVo vo = param.get(0);
  11. return vo.getValue();
  12. }
  13. @PostMapping("/hello4")
  14. public String test4(@BatchBody MyBeanVo... param) {
  15. MyBeanVo vo = param[0];
  16. return vo.getValue();
  17. }
  18. }
  19. 复制代码

传入参数{"value":"hello world"}或者[{"value":"hello world"}]

返回皆为:hello world

可以完美兼容数组,集合,可变参数(实际是数组)

分析:RequestBodyArgumentResolver解析Json字符串,需要检测格式是否正确,需要兼容单个数据和批量数据的参数,只需要把该参数改成List/数组[]/可变参数,再在前面加上@BatchBody注解即可实现,service层和dao层要设计为批量的传参

总结

SpringMVC提供了很多自定义拦截/过滤器的接口和类,注册到配置类中,为开发者提供了方便的api,能满足开发中的大多数场景的需求,其扩展性真的做得很赞。同时,我们在设计一个接口,一个函数,多考虑其扩展和接入场景,让每个函数变得更健壮,先设计再编码,减少试错的成本

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/309832
推荐阅读
相关标签
  

闽ICP备14008679号