preface
A few days ago, when I was writing Rest interfaces, I saw a way to pass values that I had never written before, so I had an idea to explore it. There was a previous article that touched on this topic, but that article focused on the analysis of the process rather than going into depth.
This article focuses on several ways of passing parameters and how they are parsed and applied to method parameters.
I. HTTP request processing process
In either SpringBoot or SpringMVC, an HTTP request is received by the DispatcherServlet class, which is essentially a Servlet because it inherits from HttpServlet. Here, Spring is responsible for parsing the request, matching the method to the Controller class, parsing the parameters and executing the method, and finally processing the return value and rendering the view.
Our focus today is on the step of parsing the parameters, corresponding to the target method call in the figure above. Speaking of parameter parsing, there must be different parsers for different types of parameters. Spring has already registered a bunch of these for us.
They have a common interface HandlerMethodArgumentResolver. SupportsParameter is used to determine whether method arguments can be parsed by the current parser and if so, call resolveArgument to resolve them.
Public interface HandlerMethodArgumentResolver {/ / whether the method parameters can be the current parser parsed Boolean supportsParameter (MethodParameter var1); @nullable Object resolveArgument(MethodParameter var1, @nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4)throws Exception; }Copy the code
Second, the RequestParam
In the Controller method, if your parameter is annotated with a RequestParam annotation, or is a simple data type.
@RequestMapping("/test1")
@ResponseBody
public String test1(String t1, @RequestParam(name = "t2",required = false) String t2,HttpServletRequest request){
logger.info("Parameters: {}, {}",t1,t2);
return "Java";
}
Copy the code
Our request path is this: http://localhost:8080/test1? t1=Jack&t2=Java
If we were writing it as before, we would simply get the value from the Request object based on the parameter name or the name of the RequestParam annotation. Like this:
String parameter = request.getParameter("t1");
In Spring, the parser is RequestParamMethodArgumentResolver here the corresponding parameters. The idea is to get the parameter name and get the value directly from the Request.
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); / /... Omit some code...if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = paramValues.length == 1 ? paramValues[0] : paramValues;
}
}
return arg;
}
Copy the code
Third, RequestBody
If we need the front end to transfer more parameter content, it is better to transfer the parameters in the Body via a POST request. Of course, the more user-friendly data format is JSON.
Faced with such a request, we can receive it in the Controller method via the RequestBody annotation and automatically convert it to the appropriate Java Bean object.
@ResponseBody
@RequestMapping("/test2")
public String test2(@RequestBody SysUser user){
logger.info("Parameter information :{}",JSONObject.toJSONString(user));
return "Hello";
}
Copy the code
In the absence of Spring, how do we solve this problem?
So, first of all, we’re going to rely on the Request object. The Body data can be retrieved using the request.getreader () method, then read the string, and finally converted to an appropriate Java object using the JSON utility class.
For example:
@RequestMapping("/test2")
@ResponseBody
public String test2(HttpServletRequest request) throws IOException {
BufferedReader reader = request.getReader();
StringBuilder builder = new StringBuilder();
String line;
while((line = reader.readLine()) ! = null){ builder.append(line); } logger.info("Body data: {}",builder.toString());
SysUser sysUser = JSONObject.parseObject(builder.toString(), SysUser.class);
logger.info("Transformed Bean: {}",JSONObject.toJSONString(sysUser));
return "Java";
}
Copy the code
Of course, in a real world scenario, the sysuser.class above would need to get the parameter types dynamically.
In Spring, the parameters of the RequestBody annotations by RequestResponseBodyMethodProcessor class to parse.
Its resolution shall be the responsibility of the superclass AbstractMessageConverterMethodArgumentResolver. The whole process can be divided into three steps.
1. Obtain request auxiliary information
Before you start, you need to get some auxiliary information about the request, such as the HTTP request data format, context Class information, parameter type Class, HTTP request method type, and so on.
protected <T> Object readWithMessageConverters(){
boolean noContentType = false;
MediaType contentType;
try {
contentType = inputMessage.getHeaders().getContentType();
} catch (InvalidMediaTypeException var16) {
throw new HttpMediaTypeNotSupportedException(var16.getMessage());
}
if (contentType == null) {
noContentType = true; contentType = MediaType.APPLICATION_OCTET_STREAM; } Class<? > contextClass = parameter.getContainingClass(); Class<T> targetClass = targetType instanceof Class ? (Class)targetType : null;if(targetClass == null) { ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter); targetClass = resolvableType.resolve(); } HttpMethod httpMethod = inputMessage instanceof HttpRequest ? ((HttpRequest)inputMessage).getMethod() : null; / /... }Copy the code
2. Determine the message converter
The auxiliary information obtained above is useful to identify a message converter. There are many message converters, and their common interface is HttpMessageConverter. Here, Spring registered many converters for us, so we need to loop through them to determine which one to use for message conversion.
If is the JSON data format, will choose MappingJackson2HttpMessageConverter to deal with. Its constructor indicates just that.
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, new MediaType[]{
MediaType.APPLICATION_JSON,
new MediaType("application"."*+json")});
}
Copy the code
3, parsing,
Now that you’ve identified the message converter, the rest is simple. Get the Body via Request and call the converter to parse.
protected <T> Object readWithMessageConverters() {if(message.hasBody()) { HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType); body = genericConverter.read(targetType, contextClass, msgToUse); body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType); }}Copy the code
Further down is the Jackson pack, without further ado. It’s a long process, but it’s really about finding two things:
Methods the parser RequestResponseBodyMethodProcessor
Message converters MappingJackson2HttpMessageConverter
Call method parsing after all found.
GET request parameter conversion Bean
Another way to write this is to use Java beans on the Controller method.
@RequestMapping("/test3")
@ResponseBody
public String test3(SysUser user){
logger.info("Parameter: {}",JSONObject.toJSONString(user));
return "Java";
}
Copy the code
Then use the GET method to request:
http://localhost:8080/test3?id=1001&name=Jack&password=1234&address=, haidian district, Beijing
The parameter name after the URL corresponds to the property name in the Bean object and can also be automatically converted. So, what does it do here?
The first thing that comes to mind is Java’s reflection mechanism. Get the parameter name from the Request object and set the value to the method on the target class.
For example:
public String test3(SysUser user,HttpServletRequest Request)throws Exception {// Obtain all parameters from the request key and value Map<String, String[]> parameterMap = request.getParameterMap(); Iterator<Map.Entry<String, String[]>> iterator = parameterMap.entrySet().iterator(); Object target = user.getClass().newinstance (); Field[] fields = target.getClass().getDeclaredFields();while (iterator.hasNext()){
Map.Entry<String, String[]> next = iterator.next();
String key = next.getKey();
String value = next.getValue()[0];
for (Field field:fields){
String name = field.getName();
if (key.equals(name)){
field.setAccessible(true);
field.set(target,value);
break;
}
}
}
logger.info("userInfo:{}",JSONObject.toJSONString(target));
return "Python";
}
Copy the code
In addition to reflection, Java has an introspective mechanism for doing this. We can get the property descriptor object of the target class and then get its Method object, which is set by invoke.
private void setProperty(Object target,String key,String value) { try { PropertyDescriptor propDesc = new PropertyDescriptor(key, target.getClass()); Method method = propDesc.getWriteMethod(); method.invoke(target, value); } catch (Exception e) { e.printStackTrace(); }}Copy the code
Then in the loop above, we can call this method to implement it.
while (iterator.hasNext()){
Map.Entry<String, String[]> next = iterator.next();
String key = next.getKey();
String value = next.getValue()[0];
setProperty(userInfo,key,value);
}
Copy the code
Why talk about introspection? Because when Spring handles this, it ultimately handles it.
In simple terms, it is handled by BeanWrapperImpl. There is a simple way to use BeanWrapperImpl:
SysUser user = new SysUser();
BeanWrapper wrapper = new BeanWrapperImpl(user.getClass());
wrapper.setPropertyValue("id"."20001");
wrapper.setPropertyValue("name"."Jack");
Object instance = wrapper.getWrappedInstance();
System.out.println(instance);
Copy the code
Wrapper. The setPropertyValue finally is invoked to BeanWrapperImpl# BeanPropertyHandler. The setValue () method.
Its setValue method is much the same as our setProperty method above.
Private class BeanPropertyHandler extends PropertyHandler {// PropertyDescriptor private final PropertyDescriptor pd; public voidsetValue(@nullable Object Value) throws Exception {// ObtainsetMethod writeMethod = this.pd.getwritemethod (); ReflectionUtils.makeAccessible(writeMethod); / / set writeMethod. Invoke (BeanWrapperImpl. This. GetWrappedInstance (), value); }}Copy the code
In this way, the automatic conversion of GET request parameters to Java Bean objects is completed.
Back up, let’s look at Spring. As simple as we’ve written above, there’s a lot to consider when it comes to actual use. Deal with the parameters in the Spring of the parser is ServletModelAttributeMethodProcessor.
The parsing process in its parent class ModelAttributeMethodProcessor. ResolveArgument () method. The whole process can also be divided into three steps.
Get the constructor of the target class
Depending on the parameter type, the object class is a constructor that can be used later when binding data.
2. Create the data binder WebDataBinder
WebDataBinder inherits from DataBinder. The main function of DataBinder, in short, is to use BeanWrapper to set values for properties of objects.
Bind data to the target class and return it
Here, you convert the WebDataBinder into a ServletRequestDataBinder object and call its bind method.
The next important step is to convert the parameters in the request to a MutablePropertyValues PVS object.
Then the next step is to loop through the PVS, calling setPropertyValue to set the property. Of course, is the last call BeanWrapperImpl# BeanPropertyHandler. The setValue ().
Here’s a bit of code to better understand the process, with the same effect:
Map<String,Object> Map = new HashMap(); map.put("id"."1001");
map.put("name"."Jack");
map.put("password"."123456");
map.put("address"."Haidian District, Beijing"); MutablePropertyValues propertyValues = new MutablePropertyValues(map); SysUser sysUser = new SysUser(); // Create data binder ServletRequestDataBinder binder = new ServletRequestDataBinder(sysUser); //bindData binder. The bind (propertyValues); System.out.println(JSONObject.toJSONString(sysUser));Copy the code
Custom parameter resolver
We say that all message parser has realized the interface HandlerMethodArgumentResolver. We could also define a parameter parser that implements this interface.
First, we can define a RequestXuner annotation.
@Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequestXuner { String name() default"";
boolean required() default false;
String defaultValue() default "default";
}
Copy the code
Then is to realize the interface HandlerMethodArgumentResolver parser class.
public class XunerArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
returnparameter.hasParameterAnnotation(RequestXuner.class); } @Override public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory WebDataBinderFactory){// Get the annotation on the parameter RequestXuner annotation = methodParameter.getParameterAnnotation(RequestXuner.class); String name = annotation.name(); / / get parameter values from the Request String parameter = nativeWebRequest. GetParameter (name);return "HaHa,"+parameter; }}Copy the code
Don’t forget to configure it.
@Configuration public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Override protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new XunerArgumentResolver()); }}Copy the code
After one operation, in Controller we can use it like this:
@ResponseBody
@RequestMapping("/test4")
public String test4(@RequestXuner(name="xuner") String xuner){
logger.info("Parameter: {}",xuner);
return "Test4";
}
Copy the code
Six, summarized
This article shows you how some of the parsers in Spring parse parameters through sample code. At the end of the day, no matter how variable the parameters, no matter how complex the parameter types.
They are all sent through HTTP requests, so you can get everything through HttpServletRequest. Spring does what it can with annotations for most application scenarios.