Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
SugarTeam opened this issue Jun 16, 2020 · 0 comments
Open

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

SugarTeam opened this issue Jun 16, 2020 · 0 comments
Labels

Comments

@SugarTeam
Copy link
Contributor

背景

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

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

解决思路

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

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

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

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

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

上代码

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

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

package com.test.config;

public class MyBeanVo {
    String value = "";

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

可变参数(不能解决)

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

@RestController
public class MyController {

    @PostMapping("/hello")
    public String test(@RequestBody MyBeanVo... param) {
        MyBeanVo vo = param[0];
        return vo.getValue();
    }
}

传单个参数时报错: "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的反序列化

自定义反序列化

方案一

定义一个批量实体类

package com.test.config;

import java.util.List;

public class BatchVo<T> {
    List<T> list;

    public List<T> getList() {
        return list;
    }

    public void setList(List<T> list) {
        this.list = list;
    }
}

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

package com.test.config;

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
import java.util.ArrayList;

@JsonComponent
public class MyJsonDeserializer extends JsonDeserializer<BatchVo<MyBeanVo>> {
    @Override
    public BatchVo<MyBeanVo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser);
        BatchVo vo = new BatchVo<MyBeanVo>();
        String str = treeNode.toString();
        // 前端传参是数组
        if (treeNode.isArray()) {
            vo.list = JSONObject.parseArray(str, MyBeanVo.class);
        }
        // 前端传参是单个数据
        if (treeNode.isObject()) {
            vo.list = new ArrayList();
            vo.list.add(JSONObject.parseObject(str, MyBeanVo.class));
        }
        return vo;
    }
}

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

@RestController
public class MyController {

    @PostMapping("/hello")
    public String test(@RequestBody BatchVo<MyBeanVo>param) {
        MyBeanVo vo = param.getList().get(0);
        return vo.getValue();
    }
}

发起请求:POST localhost:8080/hello

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

返回皆为:hello world

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

方案二

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

package com.test.config;

import com.alibaba.fastjson.JSON;
import org.apache.commons.io.IOUtils;
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.Type;
import java.util.List;

public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver {

    /**
     * 只拦截BatchBody注解且为数组的请求参数
     * 每个mapping的方法只会执行一次此方法
     */
    public boolean supportsParameter(MethodParameter methodParameter) {
        Class paramType = methodParameter.getParameterType();
        boolean isArray = paramType.isArray();
        boolean isList = paramType.isAssignableFrom(List.class);
        boolean hasAnnotation = methodParameter.hasParameterAnnotation(BatchBody.class);
        return hasAnnotation && (isArray || isList);
    }

    /**
     * 通过了supportsParameter校验的mapping方法每次都会执行此方法
     */
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        String json = getRequestBodyString(nativeWebRequest);
        Type type = methodParameter.getGenericParameterType();
        Object obj = JSON.parseObject(json, type);
        return obj;
    }

    /**
     * 格式化json数据,统一为数组形式
     * 解析json字符串需做得更完善,例如校验json格式是否正确
     */
    private String getRequestBodyString(NativeWebRequest webRequest) throws IOException {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        String json = IOUtils.toString(request.getInputStream(), "UTF-8").trim();
        if (json.startsWith("{") && json.endsWith("}")) {
            return "[" + json + "]";
        }
        if (json.startsWith("[") && json.endsWith("]")) {
            return json;
        }
        return null;
    }
}

将RequestBodyArgumentResolver注册到WebMvcConfigurerAdapter当中。

package com.test.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List;

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new RequestBodyArgumentResolver());
        super.addArgumentResolvers(argumentResolvers);
    }
}

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

@RestController
public class MyController {

    @PostMapping("/hello2")
    public String test2(@BatchBody MyBeanVo[] param) {
        MyBeanVo vo = param[0];
        return vo.getValue();
    }

    @PostMapping("/hello3")
    public String test3(@BatchBody List<MyBeanVo> param) {
        MyBeanVo vo = param.get(0);
        return vo.getValue();
    }

    @PostMapping("/hello4")
    public String test4(@BatchBody MyBeanVo... param) {
        MyBeanVo vo = param[0];
        return vo.getValue();
    }
}

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

返回皆为:hello world

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

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

总结

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant