- Scotland team
- Author: Peace be with you
background
-
Scene 1: Add, delete, modify, and check the front and back end interworking interfaces. At the beginning, the requested parameters are basically a single piece of data, and the JSON format is {“key”:”value”}. The subsequent extension of the product changes the parameter transmission into batch operation. At this time, if the back end changes the receiving object of the original interface to an array, the front and back end grayscale release, there will be incompatible with the old version
-
Scenario 2: The client of the product may consist of the Web, PC, and App. For example, when the parameter structure of an interface is changed to an array, the Web end is updated, but the App and PC end are not updated. Therefore, the client is incompatible with other ends
solution
- The new interface
-
Advantages: Old interfaces are not affected, and the impact range is small
-
Disadvantages: repetitive code, useless interfaces in the late stage
- The front and back ends begin by specifying the array’s request parameters
-
Advantages: more fundamental solution to the problem
-
Disadvantages: Programmer’s disadvantages, not all programmers can prejudge the type of interface parameters
- In most cases, the problem is solved. The idea is that the back end intercepts and processes the received request parameters. After the verification is correct, the JSON data is uniformly encapsulated as a general object or array
- Advantages: You only need to refactor the original interface to be compatible with both cases
- Disadvantages: You need to customize the JSON parsing. If the parsing is not good, the JSON reverse ordering will fail
In the code
Here is the process of trying to solve the above scenario in three ways
Define a receiving front end entity class MyBeanVo
package com.test.config;
public class MyBeanVo {
String value = "";
public String getValue() {
return value;
}
public void setValue(String value) { this.value = value; }}Copy the code
Variable parameter (cannot be solved)
The Java variable Object… When calling a method, it is possible to pass a single parameter or multiple parameters, but this cannot be resolved. Because the mutable arguments are actually an Object[] array
@RestController
public class MyController {
@PostMapping("/hello")
public String test(@RequestBody MyBeanVo... param) {
MyBeanVo vo = param[0];
returnvo.getValue(); }}Copy the code
Leaflets parameter times wrong: “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]”
Reason: Front-end parameters (single data) cannot be parsed to MyBean[], which involves Json deserialization
Custom deserialization
Plan a
Define a batch entity class
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; }}Copy the code
The @jsonComponent annotation is automatically injected into Spring. Deserialize is automatically implemented when deserchvo <MyBeanVo> is deserialized. However, the drawback is that the T of JsonDeserializer<T> must be a concrete type and cannot carry generics. Different parameters have different Vo, it is very troublesome to write a custom reverse ordering class for different 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(); // The front pass is an arrayif(treeNode.isArray()) { vo.list = JSONObject.parseArray(str, MyBeanVo.class); } // The front-end parameter is a single dataif (treeNode.isObject()) {
vo.list = new ArrayList();
vo.list.add(JSONObject.parseObject(str, MyBeanVo.class));
}
returnvo; }}Copy the code
MyJsonDeserializer deserialize cannot be used for deserialize unless @requestBody is added to the bound parameter
@RestController
public class MyController {
@PostMapping("/hello")
public String test(@RequestBody BatchVo<MyBeanVo>param) {
MyBeanVo vo = param.getList().get(0);
returnvo.getValue(); }}Copy the code
Initiate a request: POST localhost:8080/hello
Body argument: [{“value”:”hello world”}] or {“value”:”hello world”}
Return: Hello world
Analysis: This design is obvious unless MyBean can be designed to be powerful and generic enough to accept all of the request parameters from the front end. Otherwise, every Vo class needs to write a de-ordered column parser class that implements JsonDeserializer, or it needs to de-serialize Json in the Contrller layer every time. This implementation becomes cumbersome and increases the amount of code
Scheme 2
Custom parameter parser Custom parameter parser
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 {/ * * * only intercept BatchBody * annotations and the request of an array parameter Each mapping method executes this method only once */ 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);returnhasAnnotation && (isArray || isList); } /** * Public Object resolveArgument(MethodParameter MethodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { String json = getRequestBodyString(nativeWebRequest); Typetype = methodParameter.getGenericParameterType();
Object obj = JSON.parseObject(json, type);
returnobj; } /** * Format the JSON data as an array * Parsing json strings needs to be more refined, For example, check whether the JSON format is correct. */ Private String getRequestBodyString(NativeWebRequest) 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;
}
returnnull; }}Copy the code
RequestBodyArgumentResolver registered to 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); }}Copy the code
Define the Mapping interface with @batchBody annotation on the parameters
@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];
returnvo.getValue(); }}Copy the code
Pass in {“value”:”hello world”} or [{“value”:”hello world”}]
Return: Hello world
It’s perfectly compatible with arrays, sets, mutable arguments (actually arrays)
Analysis: RequestBodyArgumentResolver parsing Json string, the format to check whether it is right, need to be compatible with a single data and the parameters of the bulk data, only need to change the parameter to the List/array [] / variable parameter, again add @ BatchBody annotations can be realized at the front, The Service and DAO layers are designed for batch parameter passing
conclusion
SpringMVC provides a number of custom interceptor/filter interfaces and classes registered in the configuration class, providing a convenient API for developers to meet the needs of most scenarios in development, and its extensibility is really great. At the same time, we are designing an interface and a function, considering its expansion and access scenarios, making each function more robust, designing before coding, and reducing the cost of trial and error