SpringMVC系列-4 参数解析器

背景:

本文作为SpringMVC系列的第四篇,介绍参数解析器。本文讨论的参数解析表示从HTTP消息中解析出JAVA对象或流对象并传参给Controller接口的过程。

本文内容包括介绍参数解析器工作原理、常见的参数解析器、自定义参数解析器等三部分。其中,原理部分会结合源码进行说明。

1.工作原理

说明:本文重点在于说明参数解析器的工作原理和使用方式,为避免文章过于冗长,会刻意省略对异步请求和文件上传部分的分支逻辑,读者可在理解主线逻辑后自定阅读该部分源码。

源码介绍时,会忽略所有的日志打印以及与主线逻辑无关的try-catch-finnally块

1.1 解析过程

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

javascript 复制代码
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);

	//...
}

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

javascript 复制代码
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;
	}
}

invokeAndHandle方法从逻辑上可以分为两个部分:(1)调用controller接口并获取返回值;(2)处理返回值。

本文关注第一部分:Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);

其中:webRequest包装了HTTP请求的request和response对象,mavContainer是MVC对象,providedArgs传入的是null.
跟进invokeForRequest方法:

javascript 复制代码
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
	Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
	return doInvoke(args);
}

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

javascript 复制代码
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;
}

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

javascript 复制代码
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)遍历参数数组,根据参数解析器对每个参数对象依此进行处理,如果没有匹配的参数处理器,则抛出IllegalStateException异常。

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

javascript 复制代码
private final List<HandlerMethodArgumentResolver> argumentResolvers

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

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

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

javascript 复制代码
public interface HandlerMethodArgumentResolver {
	boolean supportsParameter(MethodParameter parameter);

	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

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

1.2 初始化过程

ServletInvocableHandlerMethods的resolvers属性

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

java 复制代码
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);
		
		// ...
	}
}

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

RequestMappingHandlerAdapter的argumentResolvers属性

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

【1】实例化阶段

java 复制代码
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
		@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
		@Qualifier("mvcConversionService") FormattingConversionService conversionService,
		@Qualifier("mvcValidator") Validator validator) {

	RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
	adapter.setCustomArgumentResolvers(getArgumentResolvers());
	
	// ...
	return adapter;
}

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

看一下getArgumentResolvers()逻辑:

java 复制代码
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);
}

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

java 复制代码
@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);
		}
	}
}

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

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

【2】初始化阶段

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

dart 复制代码
@Override
public void afterPropertiesSet() {
	// ...
	if (this.argumentResolvers == null) {
		List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
		this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
	}
	// ...
}

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

dart 复制代码
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)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:

支持的类型较多,如下所示:

javascript 复制代码
@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);
}

MapMethodProcessor:

javascript 复制代码
public boolean supportsParameter(MethodParameter parameter) {
	return Map.class.isAssignableFrom(parameter.getParameterType()) &&
			parameter.getParameterAnnotations().length == 0;
}

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

ServletModelAttributeMethodProcessor:

javascript 复制代码
@Override
public boolean supportsParameter(MethodParameter parameter) {
	return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
			(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
	}

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

java 复制代码
@Override
public boolean supportsParameter(MethodParameter parameter) {
	return parameter.hasParameterAnnotation(ModelAttribute.class) ||
			!BeanUtils.isSimpleProperty(parameter.getParameterType());
	}

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

java 复制代码
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);
}

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

2.2 常用注解及消息解析器

略(待补充)

3.使用方式

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

javascript 复制代码
@Data
public class User {
    private String name;

    private Integer age;

    private String token;

    private LocalDateTime time;
}

定义Controller接口:

javascript 复制代码
@GetMapping("/test")
public Object queryByType(User user) {
   return user;
}

自定义参数解析器:

javascript 复制代码
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;
    }
}

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

javascript 复制代码
@Configuration
public class BaseWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new MyArgumentResolver());
    }
}

使用postman调用测试结果如下:

相关推荐
Ling_suu9 分钟前
SpringMVC——请求和响应
spring
ZERO空白13 分钟前
Spring MVC:原理、配置与基础应用详解
java·spring·mvc
真心喜欢你吖8 小时前
Spring Boot与MyBatis-Plus的高效集成
java·spring boot·后端·spring·mybatis
码上一元9 小时前
掌握 Spring 事务管理:深入理解 @Transactional 注解
数据库·spring
程序员学姐9 小时前
基于SpringBoot+Vue的高校社团管理系统
java·开发语言·vue.js·spring boot·后端·mysql·spring
儿时可乖了12 小时前
优化 Spring Boot 性能
spring boot·spring
cooldream200914 小时前
SpringMVC 执行流程详解
java·spring·springmvc
重生成为码农‍14 小时前
SpringMVC-Day1
mvc
ModelBulider14 小时前
SpringMVC应用专栏介绍
java·开发语言·后端·spring·springmvc
leeyayai_xixihah15 小时前
0-1实现SpringBoot项目开发(1)-SpringBoot+mybatis+mysql+Navicat
java·spring boot·spring