後臺接收Json請求引數相容陣列和單個物件

蘇格團隊發表於2018-12-21
  • 蘇格團隊
  • 作者:宇你平安

背景

  • 場景一:前後端對接介面,增刪改查,一開始請求的引數,基本是單條資料,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,能滿足開發中的大多數場景的需求,其擴充套件性真的做得很贊。同時,我們在設計一個介面,一個函式,多考慮其擴充套件和接入場景,讓每個函式變得更健壯,先設計再編碼,減少試錯的成本

相關文章