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调用测试结果如下:

相关推荐
pangtao20251 小时前
【瑞萨RA × Zephyr评测】看门狗
java·后端·spring
任子菲阳2 小时前
学Javaweb第四天——springboot入门
java·spring·mybatis
Coder_Boy_3 小时前
基于SpringAI的智能平台基座开发-(十一)
人工智能·spring·langchain·langchain4j
爱吃山竹的大肚肚3 小时前
优化SQL:如何使用 EXPLAIN
java·数据库·spring boot·sql·spring
向上的车轮3 小时前
Apache Camel 与 Spring Integration的区别是什么?
java·spring·apache
URBBRGROUN4673 小时前
Spring AI Alibaba入门
java·人工智能·spring
码界奇点5 小时前
基于Spring Boot和Vue.js的视频点播管理系统设计与实现
java·vue.js·spring boot·后端·spring·毕业设计·源代码管理
爱吃山竹的大肚肚5 小时前
MySQL 支持的各类索引
java·数据库·sql·mysql·spring·spring cloud
高老庄小呆子5 小时前
SpringBoot3.5.4 引入Knife4j的官方start包
spring
廋到被风吹走5 小时前
【Spring】Spring Boot详细介绍
java·spring boot·spring