• SpringMVC系列-4 参数解析器


    背景:

    本文作为SpringMVC系列的第四篇,介绍参数解析器。本文讨论的参数解析表示从HTTP消息中解析出JAVA对象或流对象并传参给Controller接口的过程。
    本文内容包括介绍参数解析器工作原理、常见的参数解析器、自定义参数解析器等三部分。其中,原理部分会结合源码进行说明。

    1.工作原理

    说明:本文重点在于说明参数解析器的工作原理和使用方式,为避免文章过于冗长,会刻意省略对异步请求和文件上传部分的分支逻辑,读者可在理解主线逻辑后自定阅读该部分源码。
    源码介绍时,会忽略所有的日志打印以及与主线逻辑无关的try-catch-finnally块

    1.1 解析过程

    SpringMVC系列-2 HTTP请求调用链 中介绍过:收到http请求后,进入DispatcherServlet的dispatcherServlet方法:

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	//...
    	
    	// 1.preHandle
    	if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    		return;
    	}
    
    	// 2.call handler
    	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    	// 3.postHandle
    	mappedHandler.applyPostHandle(processedRequest, response, mv);
    
    	//...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在执行HandlerInterceptor的preHandle和postHandle之间,会通过ha.handle(processedRequest, response, mappedHandler.getHandler())反射调用Controller接口,跟踪该调用链进入:

    public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
    	//	⚠️1.调用controller接口
    	Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    	
    	//	⚠️2.处理返回值
    	setResponseStatus(webRequest);
    	if (returnValue == null) {
    		if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
    			disableContentCachingIfNecessary(webRequest);
    			mavContainer.setRequestHandled(true);
    			return;
    		}
    	} else if (StringUtils.hasText(getResponseStatusReason())) {
    		mavContainer.setRequestHandled(true);
    		return;
    	}
    
    	mavContainer.setRequestHandled(false);
    	Assert.state(this.returnValueHandlers != null, "No return value handlers");
    	try {
    		this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
    	} catch (Exception ex) {
    		if (logger.isTraceEnabled()) {
    			logger.trace(formatErrorForReturnValue(returnValue), ex);
    		}
    		throw ex;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    invokeAndHandle方法从逻辑上可以分为两个部分:(1)调用controller接口并获取返回值;(2)处理返回值。
    本文关注第一部分:Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    其中:webRequest包装了HTTP请求的request和response对象,mavContainer是MVC对象,providedArgs传入的是null.
    跟进invokeForRequest方法:

    public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
    	Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    	return doInvoke(args);
    }
    
    • 1
    • 2
    • 3
    • 4

    包括两个步骤:(1)调用getMethodArgumentValues获取参数;(2)将步骤(1)获取的参数传递给doInvoke,通过反射调用Controller接口并返回结果。
    跟进getMethodArgumentValues方法:

    protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,	Object... providedArgs) throws Exception {
    	MethodParameter[] parameters = getMethodParameters();
    	if (ObjectUtils.isEmpty(parameters)) {
    		return new Object[0];
    	}
    
    	Object[] args = new Object[parameters.length];
    	for (int i = 0; i < parameters.length; i++) {
    		MethodParameter parameter = parameters[i];
    		parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
    		args[i] = findProvidedArgument(parameter, providedArgs);
    		if (args[i] != null) {
    			continue;
    		}
    		if (!this.resolvers.supportsParameter(parameter)) {
    			throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
    		}
    		try {
    			args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
    		} catch (Exception ex) {
    			throw ex;
    		}
    	}
    	return args;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    由于入参providedArgs为null, 因此上述逻辑可以简化为:

    protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,	Object... providedArgs) throws Exception {
    	MethodParameter[] parameters = getMethodParameters();
    	if (ObjectUtils.isEmpty(parameters)) {
    		return new Object[0];
    	}
    
    	Object[] args = new Object[parameters.length];
    	for (int i = 0; i < parameters.length; i++) {
    		MethodParameter parameter = parameters[i];
    		parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
    		if (!this.resolvers.supportsParameter(parameter)) {
    			throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
    		}
    		try {
    			args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
    		} catch (Exception ex) {
    			throw ex;
    		}
    	}
    	return args;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    上述解析参数的逻辑可以分为两步:(1)获取目标接口的参数数组,并判断是否为空(参数为空即不需要处理参数);(2)遍历参数数组,根据参数解析器对每个参数对象依此进行处理,如果没有匹配的参数处理器,则抛出IllegalStateException异常。

    这里的参数解析器this.resolvers类型是HandlerMethodArgumentResolverComposite,是一个组合模型,内部维持了一个HandlerMethodArgumentResolver数组:

    private final List<HandlerMethodArgumentResolver> argumentResolvers
    
    • 1

    参数解析最终都会派发给argumentResolvers的各个元素,派发原则是选择第一个满足匹配规则的参数解析器

    在后续SpringMVC源码介绍过程中,会发现框架大量使用了组合设计模式;大部分采取匹配+处理的组合手段完成。

    因此HandlerMethodArgumentResolver接口需要有两个接口:

    public interface HandlerMethodArgumentResolver {
    	boolean supportsParameter(MethodParameter parameter);
    
    	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
    			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    supportsParameter方法用于判断该HandlerMethodArgumentResolver是否与参数匹配,resolveArgument方法用于解析参数并返回解析结果。

    1.2 初始化过程

    ServletInvocableHandlerMethods的resolvers属性
    解析过程的核心在于参数解析器,即ServletInvocableHandlerMethod对象中的HandlerMethodArgumentResolverComposite resolvers属性,而每次HTTP调用都会生成一个ServletInvocableHandlerMethod对象,因此关注该对象的resolvers属性如何被初始化即可:

    public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
    	protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    		// ...
    		
    		ServletInvocableHandlerMethod invocableMethod = new ServletInvocableHandlerMethod(handlerMethod);
    		if (this.argumentResolvers != null) {
    			invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
    		}
    		
    		// ...
    		
    		invocableMethod.invokeAndHandle(webRequest, mavContainer);
    		
    		// ...
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如上所示:ServletInvocableHandlerMethod对象的resolvers属性来自RequestMappingHandlerAdapter对象的this.argumentResolvers属性。

    RequestMappingHandlerAdapter的argumentResolvers属性
    继续跟踪RequestMappingHandlerAdapter的this.argumentResolvers属性初始化过程,需要注意的是RequestMappingHandlerAdapter是全局Bean对象,因此可以从头梳理一下该对象关于argumentResolvers属性的初始化过程。
    【1】实例化阶段

    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
    		@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
    		@Qualifier("mvcConversionService") FormattingConversionService conversionService,
    		@Qualifier("mvcValidator") Validator validator) {
    
    	RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
    	adapter.setCustomArgumentResolvers(getArgumentResolvers());
    	
    	// ...
    	return adapter;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    createRequestMappingHandlerAdapter()返回RequestMappingHandlerAdapter实例后,将getArgumentResolvers()获取的自定义参数解析器设置到this.customArgumentResolvers属性中。
    看一下getArgumentResolvers()逻辑:

    protected final List<HandlerMethodArgumentResolver> getArgumentResolvers() {
    	this.argumentResolvers = new ArrayList<>();
    	addArgumentResolvers(this.argumentResolvers);
    	return this.argumentResolvers;
    }
    
    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    	this.configurers.addArgumentResolvers(argumentResolvers);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    argumentResolvers数据来自于this.configurers,看一下这个属性的初始化过程:

    @Configuration(proxyBeanMethods = false)
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    
    	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
    	@Autowired(required = false)
    	public void setConfigurers(List<WebMvcConfigurer> configurers) {
    		if (!CollectionUtils.isEmpty(configurers)) {
    			this.configurers.addWebMvcConfigurers(configurers);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    configurers来自于IOC容器中WebMvcConfigurer类型的对象。

    因此用户可自定义WebMvcConfigurer对象并将其注入到IOC中,在自定义的WebMvcConfigurer类中通过复写addArgumentResolvers方法可实现自定义参数解析器的添加。

    【2】初始化阶段
    RequestMappingHandlerAdapter实现了InitializingBean接口,在Bean的初始化阶段中,会调用其afterPropertiesSet()钩子函数:

    
    public void afterPropertiesSet() {
    	// ...
    	if (this.argumentResolvers == null) {
    		List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
    		this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    	}
    	// ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看出所有的参数解析器来自有getDefaultArgumentResolvers()方法:

    private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
    	List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);
    
    	// Annotation-based argument resolution
    	resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
    	resolvers.add(new RequestParamMapMethodArgumentResolver());
    	resolvers.add(new PathVariableMethodArgumentResolver());
    	resolvers.add(new PathVariableMapMethodArgumentResolver());
    	resolvers.add(new MatrixVariableMethodArgumentResolver());
    	resolvers.add(new MatrixVariableMapMethodArgumentResolver());
    	resolvers.add(new ServletModelAttributeMethodProcessor(false));
    	resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    	resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
    	resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
    	resolvers.add(new RequestHeaderMapMethodArgumentResolver());
    	resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
    	resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
    	resolvers.add(new SessionAttributeMethodArgumentResolver());
    	resolvers.add(new RequestAttributeMethodArgumentResolver());
    
    	// Type-based argument resolution
    	resolvers.add(new ServletRequestMethodArgumentResolver());
    	resolvers.add(new ServletResponseMethodArgumentResolver());
    	resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    	resolvers.add(new RedirectAttributesMethodArgumentResolver());
    	resolvers.add(new ModelMethodProcessor());
    	resolvers.add(new MapMethodProcessor());
    	resolvers.add(new ErrorsMethodArgumentResolver());
    	resolvers.add(new SessionStatusMethodArgumentResolver());
    	resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
    
    	// Custom arguments
    	if (getCustomArgumentResolvers() != null) {
    		resolvers.addAll(getCustomArgumentResolvers());
    	}
    
    	// Catch-all
    	resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
    	resolvers.add(new ServletModelAttributeMethodProcessor(true));
    
    	return resolvers;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    这里包含了框架内置的参数解析器以及自定义参数解析器,需要注意一下几点:
    (1)getCustomArgumentResolvers()来自于【1】实例化阶段中设置的自定义参数解析器;
    (2)自定义参数解析器的顺序比较靠后,需要避免被其他参数解析器拦截,supportsParameter方法可以根据参数类型进行匹配。
    (3)首位端各存在一个RequestParamMethodArgumentResolver类型的参数解析器,区别是内部useDefaultResolution属性前者是false, 后者是true.

    2.常见的参数解析器

    2.1 解析器分类

    Debug是阅读源码的一个很有效的方式。

    Debug
    Spring框架默认的消息解析器超过20个, 红框圈选的是常见的参数解析器(本章节重点对着一部分进行介绍),如下所示:在这里插入图片描述
    上述26个参数解析器按照匹配条件类型可以分为:
    【1】注解类型
    根据Controller接口中参数是否拥有指定注解确定使用的参数解析器:
    @RequestParam -> RequestParamMethodArgumentResolver, RequestParamMapMethodArgumentResolver
    @PathVariable -> PathVariableMethodArgumentResolver, PathVariableMapMethodArgumentResolver
    @MatrixVariable -> MatrixVariableMethodArgumentResolver, MatrixVariableMapMethodArgumentResolver
    @ModelAttribute -> ModelAttributeMethodProcessor
    @RequestBody -> RequestResponseBodyMethodProcessor
    @RequestPart -> RequestPartMethodArgumentResolver
    @RequestHeader -> RequestHeaderMethodArgumentResolver, RequestHeaderMapMethodArgumentResolver
    @CookieValue -> ServletCookieValueMethodArgumentResolver
    @Value -> ExpressionValueMethodArgumentResolver
    @SessionAttribute -> SessionAttributeMethodArgumentResolver
    @RequestAttribute -> RequestAttributeMethodArgumentResolver
    上述参数解析器根据是否有对应注解(以及满足特定条件)确定是否匹配参数。

    【2】参数类型
    根据Controller接口中参数类型确定使用的参数解析器。
    ServletResponse及其子类, OutputStream及其子类, Writer及其子类 -> ServletResponseMethodArgumentResolver
    HttpEntity, RequestEntity -> HttpEntityMethodProcessor
    RedirectAttributes及其子类 -> RedirectAttributesMethodArgumentResolver
    Model及其子类 -> ModelMethodProcessor
    Errors及其子类 -> ErrorsMethodArgumentResolver
    SessionStatus -> SessionStatusMethodArgumentResolver
    UriComponentsBuilder, ServletUriComponentsBuilder -> UriComponentsBuilderMethodArgumentResolver

    部分参数解析器的supportsParameter方法条件比较复杂,如下所示:
    ServletRequestMethodArgumentResolver:
    支持的类型较多,如下所示:

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    	Class<?> paramType = parameter.getParameterType();
    	return (WebRequest.class.isAssignableFrom(paramType) ||
    			ServletRequest.class.isAssignableFrom(paramType) ||
    			MultipartRequest.class.isAssignableFrom(paramType) ||
    			HttpSession.class.isAssignableFrom(paramType) ||
    			(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
    			Principal.class.isAssignableFrom(paramType) ||
    			InputStream.class.isAssignableFrom(paramType) ||
    			Reader.class.isAssignableFrom(paramType) ||
    			HttpMethod.class == paramType ||
    			Locale.class == paramType ||
    			TimeZone.class == paramType ||
    			ZoneId.class == paramType);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    MapMethodProcessor:

    public boolean supportsParameter(MethodParameter parameter) {
    	return Map.class.isAssignableFrom(parameter.getParameterType()) &&
    			parameter.getParameterAnnotations().length == 0;
    }
    
    • 1
    • 2
    • 3
    • 4

    参数不能有注解,且参数为Map类型.

    ServletModelAttributeMethodProcessor:

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    	return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
    			(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    该解析器在实例化时设置的annotationNotRequired为true,因此可以简化为:

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    	return parameter.hasParameterAnnotation(ModelAttribute.class) ||
    			!BeanUtils.isSimpleProperty(parameter.getParameterType());
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    表示:参数有@ModelAttribute注解或者BeanUtils.isSimpleProperty(parameter.getParameterType())返回false;

    public static boolean isSimpleProperty(Class<?> type) {
    	// 如果类型是数据调用isSimpleValueType判读数组的元素
        return isSimpleValueType(type) || type.isArray() && isSimpleValueType(type.getComponentType());
    }
    
    public static boolean isSimpleValueType(Class<?> type) {
        return Void.class != type && Void.TYPE != type 
        && 	(ClassUtils.isPrimitiveOrWrapper(type) || Enum.class.isAssignableFrom(type) || CharSequence.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type) || Temporal.class.isAssignableFrom(type) || URI.class == type || URL.class == type || Locale.class == type || Class.class == type);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    即:参数类型(或参数数组的元素类型)是Void或Void.TYPE或者均不是(Enum,Number,CharSequence,Locale,…)类型的才会匹配。

    2.2 常用注解及消息解析器

    略(待补充)

    3.使用方式

    案例从请求url中解析出name和age、从HTTP请求头中解析出token、并获取当前服务器时间,用于构造User对象,并传参给Controller接口。
    定义参数类:

    @Data
    public class User {
        private String name;
    
        private Integer age;
    
        private String token;
    
        private LocalDateTime time;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    定义Controller接口:

    @GetMapping("/test")
    public Object queryByType(User user) {
       return user;
    }
    
    • 1
    • 2
    • 3
    • 4

    自定义参数解析器:

    public class MyArgumentResolver implements HandlerMethodArgumentResolver {
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return parameter.getParameterType().equals(User.class);
        }
    
        @Override
        public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
            User user = new User();
            HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
            user.setName(request.getParameter("name"));
            user.setAge(Integer.parseInt(request.getParameter("age")));
            user.setToken(request.getHeader("token"));
            user.setTime(LocalDateTime.now());
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    将参数解析器注册到容器中:

    @Configuration
    public class BaseWebMvcConfigurer implements WebMvcConfigurer {
        @Override
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
            argumentResolvers.add(new MyArgumentResolver());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    使用postman调用测试结果如下:
    在这里插入图片描述

  • 相关阅读:
    零成本体验美国云服务器,更方便的体验和选择
    满足多元需求:捷码打造3大一站式开发套餐,助力高效开发
    五台大数据集群生产节点安装规划
    dos2unix命令
    flask 框架web开发视频笔记
    IT互联网的编程语言那么多,最值得学习的编程语言是哪个?
    常见的Web安全漏洞有哪些,Web安全漏洞常用测试方法介绍
    Apache calcite Quickstart
    时间管理的本质,是少做事情
    【C++】特殊类的设计(单例模式为主)(学习复习兼顾)
  • 原文地址:https://blog.csdn.net/Sheng_Q/article/details/133231417