This article mainly learns the extension points of springmvc through a custom parameter parser. The
main operation that I want to accomplish is:
through a custom annotation, the json format string in the input parameter is converted to the specified type through the custom parameter parser list array
application
Custom parameter parser
public class MyHandlerMethodArgumentResolverTest implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
Json ann = (Json) methodParameter.getParameterAnnotation(Json.class);
System.out.println(methodParameter);
return ann != null;
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
Json ann = (Json) methodParameter.getParameterAnnotation(Json.class);
String param1 = ann.value();
System.out.println(param1);
System.out.println(ann.array());
String parameter = nativeWebRequest.getParameter(param1);
return JSON.parseArray(parameter, ann.array());
}
}
Add the custom parameter parser to the spring container
@Configuration
@ComponentScan("com.springmvc")
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new MyHandlerMethodArgumentResolverTest());
}
// @Override
// public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// converters.add(new MappingJackson2HttpMessageConverter());
// converters.add(new StringHttpMessageConverter());
// }
}
Custom annotation Json
@Target({
ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Json {
String value() default "";
Class<?> array();
}
controller interface
@RequestMapping("/testMyAnno")
public String testMyAnno(@Json(value = "testList", array = ImageInfo.class) List<ImageInfo> testList,
@RequestParam("haha") String userName) {
System.out.println("list信息是:" + testList);
System.out.println("userName是:" + userName);
UserInfo userInfo = new UserInfo(userName, testList);
System.out.println(userInfo.toString());
return "success";
}
Request url:
http://127.0.0.1:8080/testMyAnno.do?testList=[{
"imageType":3,"imageUrl":"http://f.souche.com/40accdeff31be8bf84fbb69e632e37d9.png"},{
"imageType":4,"imageUrl":"http://f.souche.com/40accdeff31be8bf84fbb69e632e37d9.png"},{
"imageType":6,"imageUrl":"http://f.souche.com/40accdeff31be8bf84fbb69e632e37d9.png"},{
"imageType":1,"imageUrl":"http://f.souche.com/40accdeff31be8bf84fbb69e632e37d9.png"},{
"imageType":2,"imageUrl":"http://f.souche.com/40accdeff31be8bf84fbb69e632e37d9.png"}]&haha=yesdy
Need to explain, the effect I want to achieve here is:
for the parameters annotated with @Json, directly parse the String string into an array of the Class type specified in the @Json annotation
Source code
Add custom parameter parser to spring
First of all, how to give a custom parameter parser to spring.
For springmvc projects, spring will initialize the requestMappingHandlerAdapter object, which is initialized by @Bean in the following class
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
}
In this method, there is a line of code, which is related to adding the custom parameter parser to the container, which is
adapter.setCustomArgumentResolvers(this.getArgumentResolvers());
Here, getArgumentResolvers will be called to obtain all the parameter resolvers. In the processing logic of this method, all addArgumentResolvers() methods of WebMvconfigurer will be called. Therefore, we only need to override addArgumentResolvers() in the implementation class that inherits WebMvcConfigurer. Then add the custom parameter parser to the resolvers
The WebMvcConfigurationSupport class is when we add the @EnableWebMvc annotation, it will inject a full configuration class that inherits this class through @Import
This method will not be explained too much, debug look at the source code, it is clear at a glance, there is no complicated logic
How to use a custom parameter parser
We customize the parameter parser and put it in the container. Then all requests will be executed to org.springframework.web.servlet.DispatcherServlet#doDispatch. In this method, the handlerMapping and handlerAdapter used by the current method are resolved. After that, the target method will be called.
Before calling the target method, the input parameters will be parsed one by one. In
this article, we only care about the logic of parameter parsing, so the call chain is like this
org.springframework.web.servlet.DispatcherServlet#doDispatch
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handleInternal
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest
org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues
Determine which parameter parser to use
The core of parameter parsing is in these lines of code. The outer layer of this code block is a for loop, which will traverse all args.
argumentResolvers.supportsParameter(parameter): In this line of code, all the parameter parsers will be taken to judge the current parameter args[i] to see which parameter parser can parse the parameter
/**
* 遍历所有的参数解析器,看哪个解析器可以解析,如果参数解析器返回true,就表示可以解析
* 如果我们要扩展参数解析器,就需要看下这里的逻辑
* 需要学习下这里的argumentResolvers是在哪里赋值的
*/
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
/**
* 这里就是用parameter对应的解析器去解析该参数
*/
args[i] = this.argumentResolvers.resolveArgument(
parameter, mavContainer, request, this.dataBinderFactory);
continue;
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug(getArgumentResolutionErrorMessage("Failed to resolve", i), ex);
}
throw ex;
}
}
/**
* 遍历所有的参数处理器,找到处理该parameter的处理器,然后存入到map集合中,
* 第二次获取处理该参数的处理器时,就无须再次遍历所有的support方法,直接从map缓存中获取即可
* @param parameter
* @return
*/
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
if (logger.isTraceEnabled()) {
logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +
parameter.getGenericParameterType() + "]");
}
/**
* 核心方法就是这里,上面会遍历所有的参数解析器,依次去调用对应的supports方法,判断是否可以处理parameter
* 如果可以,就返回true,然后会把对应的methodArgumentResolver和对应的parameter关联起来
*/
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
This method is one of the most core methods: to determine whether the parameter parser can parse the current parameters;
here will get all the parameter parsers, including our custom, for example: our custom parameter parser A supports parsing args[ i] After that, we will put A into the argumentResolverCache cache.
The idea here is: we only need to provide a parameter parser, and specify which parameters our custom parameter parser should parse, and spring will get all of them. The parameter parser is to match the parameters to be parsed. This design pattern should be a template design pattern. In short, this is the idea
Use the corresponding parameter parser to parse the parameters
The next core method is to analyze the logic
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
/**
* 这个方法是实际解析参数的逻辑,首先会先获取到参数对应的解析器
*/
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unknown parameter type [" + parameter.getParameterType().getName() + "]");
}
/**
* 这里调用的就是处理的方法
*/
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
As you can see, spring will obtain the parameter parser used by this parameter from the map collection according to the parameter, and then resolve
it. The resolveArgument here will call the parsing method in the custom parameter parser.
In short: the idea is like this
1. Spring provides an interface for the parameter parser. If we want to customize it, we will implement the interface
2. Then put the custom parameter parser in the spring container
3. When the method is called , Spring will take spring's own + our custom parameter parser to traverse
4. After judging which parser can be parsed by the parameter, it will pass the parameter to the corresponding parameter parser for analysis