在看公司文档的时候,看到这段话:
Controller中使用的是@PathVariable(“id”),可是设置为User对象,为什么可以获取到完整的对象?谁负责根据路径参数id来查询完整对象呢?如下:
public ResponseEntity<?> update(@PathVariable("id") User olduser, @RequestBody User user,BindingResult result)
这里涉及Spring Data对于SpringMVC提供的扩展支持:
- 通过DomainClassConverter,将Request参数和Path参数自动通过repository管理的实体类方法获取实体类对象。
- 通过HandlerMethodArgumentResolver,将Request参数转换为Pageable和Sort对象的实例。
这里给出一个简单的例子:
@RequestMapping("/index/{id}")
public void index(Pageable pageable, @PathVariable(value = "id") User user) {
System.out.println(pageable);
System.out.println(user);
}
如上所示,controller里写了一个简单的方法,接收一个pageable对象和一个user对象。我们发起的请求类似于:
http://localhost:8888/customer/index/1?page=1&sort=name,DESC&size=10
可以看到controller的输出:
Hibernate:
select
user0_.id as id1_7_0_,
user0_.extra_id as extra_id4_7_0_,
user0_.login_name as login_na2_7_0_,
user0_.password as password3_7_0_
from
tbl_user user0_
where
user0_.id=?
Page request [number: 1, size 10, sort: name: DESC]
null
易发现:
请求中的page=1&sort=name,DESC&size=10参数被映射到了一个pageable对象,而传入的id,SpringMVC自动调用了对应的sql语句查询对应id的user,并将查出来的结果对应填充到user对象里(只是例子没有查到)。
Spring是在哪里进行这些逻辑的处理的呢?
先看pageable的映射,SpringMVC为我们提供了一种叫做Resolver的机制,进行参数的绑定,例如我们最熟悉的@RequestParam
注解,就是使用原生提供的Resolver支持的,叫做:RequestParamMethodArgumentResolver
,我们看这个类的源码,会发现顶层接口是一个叫做:HandlerMethodArgumentResolver
的接口,定义了两个方法:
- boolean supportsParameter(MethodParameter parameter);
- Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
而我们的pageable对象的自动填充,对应的其实是PageableHandlerMethodArgumentResolver
这个类。我们来看他是怎么实现顶层接口的两个方法的:
@Override
public boolean supportsParameter(MethodParameter parameter) {
return Pageable.class.equals(parameter.getParameterType());
}
很容易理解,如果入参中的对象是Pageable对象,那么这个对象由我来解析。我们看具体的解析方法:
public Pageable resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
assertPageableUniqueness(methodParameter);
Pageable defaultOrFallback = getDefaultFromAnnotationOrFallback(methodParameter);
String pageString = webRequest.getParameter(getParameterNameToUse(pageParameterName, methodParameter));
String pageSizeString = webRequest.getParameter(getParameterNameToUse(sizeParameterName, methodParameter));
boolean pageAndSizeGiven = StringUtils.hasText(pageString) && StringUtils.hasText(pageSizeString);
if (!pageAndSizeGiven && defaultOrFallback == null) {
return null;
}
int page = StringUtils.hasText(pageString) ? parseAndApplyBoundaries(pageString, Integer.MAX_VALUE, true)
: defaultOrFallback.getPageNumber();
int pageSize = StringUtils.hasText(pageSizeString) ? parseAndApplyBoundaries(pageSizeString, maxPageSize, false)
: defaultOrFallback.getPageSize();
// Limit lower bound
pageSize = pageSize < 1 ? defaultOrFallback.getPageSize() : pageSize;
// Limit upper bound
pageSize = pageSize > maxPageSize ? maxPageSize : pageSize;
Sort sort = sortResolver.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
// Default if necessary and default configured
sort = sort == null && defaultOrFallback != null ? defaultOrFallback.getSort() : sort;
return new PageRequest(page, pageSize, sort);
}
也很容易理解,最终的效果就是提取我们url里的那些参数,封装成一个pageable对象,注入到controller的入参里的那个pageable对象。注意代码里对应的常量pageParameterName
这些,都是代码中写死的:
private static final String DEFAULT_PAGE_PARAMETER = "page";
private static final String DEFAULT_SIZE_PARAMETER = "size";
private static final String DEFAULT_PARAMETER = "sort";
private static final String DEFAULT_PROPERTY_DELIMITER = ",";
private static final String DEFAULT_QUALIFIER_DELIMITER = "_";
这些也对应着我们url里对应的名字,不可以改变。
其实PageableHandlerMethodArgumentResolver
这个类并不能说是SpringMVC原生提供的,算是Spring框架对Spring-JPA的一种特殊支持,SpringMVC原生提供的有哪些Resolver有哪些呢?Resolver的处理逻辑又是怎么样的呢?继续看源码:RequestMappingHandlerAdapter
这个类就向我们展示了原生提供的所有Resolver:
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
// 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;
}
上文提到的pageable的resolver是在getCustomArgumentResolvers
方法中获取到的,这些Resolver又是在哪里被获取的呢?看HandlerMethodArgumentResolverComposite
这个类:
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() + "]");
}
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
可以看到,遍历所有的resolver(自定义的和原生的),首先调用supportsParameter
进行判断,如果支持该种参数的解析,那么返回对应的resovler进行对应的resolve操作。
依葫芦画瓢(葫芦指的是RequestParam),我们来实现一种自己的Resolver。实现的功能很简单:将url里的下划线式的参数映射到我们controller里对应驼峰式的参数里。意思就是例如:
@RequestMapping("/index")
public void index(String firstName) {
System.out.println(firstName);
}
url是:http://localhost:8888/customer/index?first_name=tom
,我们要达到的效果就是将tom映射到firstName上,目前肯定是不可行的,因为Spring做不到自动的转换,我们需要一种Resolver来实现该功能。
首先,定义一种注解:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AwesomeParam {
/**
* Alias for {@link #name}.
*/
@AliasFor("name")
String value() default "";
/**
* The name of the request parameter to bind to.
* @since 4.2
*/
@AliasFor("value")
String name() default "";
/**
* Whether the parameter is required.
*/
boolean required() default true;
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
接着,就是我们的Resolver,为了简化代码,这里我们实现一个Spring为我们提供的抽象类AbstractNamedValueMethodArgumentResolver
,这个类实现了顶层接口。
@Service
public class AwesomeParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
private boolean isEnableLowerUnderscoreName = false;
public AwesomeParamMethodArgumentResolver() {
super();
}
public void setLowerUnderscoreName(boolean isEnable) {
this.isEnableLowerUnderscoreName = isEnable;
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
AwesomeParam ann = parameter.getParameterAnnotation(AwesomeParam.class);
return (ann != null ? new AwesomeParamNamedValueInfo(ann) : new AwesomeParamNamedValueInfo());
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
//对有@AweSomeParam注解的参数进行解析
return parameter.hasParameterAnnotation(AwesomeParam.class);
}
@Override
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
//将参数定义的驼峰式的名字转换为下划线式,再去url里提取。
//意思就是name原本对应的是firstName,是我们写在controller里的那个名字,但是url里的是下划线形式的
// 所以在这里我们进行转换,这样就能提取到对应的值填充进去了。
return internalResolveName(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, name), parameter, request);
}
/**
* @see RequestParamMethodArgumentResolver
*/
private Object internalResolveName(String name, MethodParameter parameter,
NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
return mpArg;
}
Object arg = null;
if (multipartRequest != null) {
List<MultipartFile> files = multipartRequest.getFiles(name);
if (!files.isEmpty()) {
arg = (files.size() == 1 ? files.get(0) : files);
}
}
if (arg == null) {
//提取request里的参数,注意这里的name已经是下划线形式的
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
return arg;
}
private static class AwesomeParamNamedValueInfo extends NamedValueInfo {
public AwesomeParamNamedValueInfo() {
super("", false, ValueConstants.DEFAULT_NONE);
}
public AwesomeParamNamedValueInfo(AwesomeParam annotation) {
super(annotation.name(), annotation.required(), annotation.defaultValue());
}
}
}
注意几个核心方法,我都做了注释。
接着在对应的controller方法的参数加上对应的注解,就可以完成功能了:
@RequestMapping("/index")
public void index(@AwesomeParam String firstName) {
System.out.println(firstName);
}
当然,以上只是为了展示这个功能,真实情况还是推荐直接使用@RequestParam,直接使用该注解的value属性,就可以达到效果,这样不管你的url里和controller里的参数名怎么变,只要将value的值指定为url里的一致即可,这样灵活性更高,可以随意改变格式。其实@RequestParam对应的ResolverRequestParamMethodArgumentResolver
也就是提取对应的value的值,最终传入到resolveName
的name参数中,跟上文我们自己实现的是一致的。
再举一个例子,现在假如有一个需求,我们需要在controller里的方法里拿到请求头里的cache-control
字段,怎么使用Resolver解决?依旧是分两步:定义注解,写resolver:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface CacheControl {
}
public class CacheControlArgumentResolver
implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(CacheControl.class) != null;
}
@Override
public Object resolveArgument(
MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request
= (HttpServletRequest) nativeWebRequest.getNativeRequest();
return request.getHeader("cache-control");
}
}
这样,在controller的方法里添加对应注解即可:
@RequestMapping("/index")
public void index(@AwesomeParam String firstName,@CacheControl Object cacheControl) {
System.out.println(firstName);
}
看到这里,回想一些SpringMVC的用法,比如我们想在controller里拿到请求的header信息,一般我们就会在参数列表里加上@RequestHeader HttpHeaders headers
,这样子header信息就会自动注入进来了,使用的时候觉得很神奇,其实它的原理也是Resolver:RequestHeaderMapMethodArgumentResolver
:
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(RequestHeader.class) &&
Map.class.isAssignableFrom(parameter.getParameterType()));
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Class<?> paramType = parameter.getParameterType();
if (MultiValueMap.class.isAssignableFrom(paramType)) {
MultiValueMap<String, String> result;
if (HttpHeaders.class.isAssignableFrom(paramType)) {
result = new HttpHeaders();
}
else {
result = new LinkedMultiValueMap<String, String>();
}
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) {
String headerName = iterator.next();
String[] headerValues = webRequest.getHeaderValues(headerName);
if (headerValues != null) {
for (String headerValue : headerValues) {
result.add(headerName, headerValue);
}
}
}
return result;
}
else {
Map<String, String> result = new LinkedHashMap<String, String>();
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) {
String headerName = iterator.next();
String headerValue = webRequest.getHeader(headerName);
if (headerValue != null) {
result.put(headerName, headerValue);
}
}
return result;
}
}
有了上文的铺垫,源码应该很容易理解了。
接下来回到最开始,关于DomainClassConverter的问题,为什么Spring会自动发起sql查询填充对象?这里就是SpringMVC提供的另一种机制了:Converter。简单的理解就是对请求数据到参数的转换,某种程度上有点和Resolver相似。来看一个简单的例子:
我们有一种url,格式是:http://localhost:8888/customer/index?data=jim:111
,我们要做的就是将name映射为名字和密码,并且填充到Person对象。Person对象定义为:
public class Person {
private String username;
private String passwd;
}
我们controller的方法为:
@RequestMapping("/index")
public void index(@RequestParam(value = "data") Person person) {
System.out.println(person);
}
怎么做到data=jim:111这串数据自动映射到person对象的效果,我们实现一个Converter:
public class StringToPersonConverter implements Converter<String,Person> {
public Person convert(String source) {
Person p1 = new Person();
if(source != null){
String[] items = source.split(":");
p1.setUsername(items[0]);
p1.setPasswd(items[1]);
}
return p1;
}
}
这样即可。文章最开始提到的自动执行sql就是这样的机制完成的。
以上测试基于Springboot,对应的Resolver和Converter自定义配置:
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AwesomeParamMethodArgumentResolver());
argumentResolvers.add(new CacheControlArgumentResolver());
super.addArgumentResolvers(argumentResolvers);
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToPersonConverter());
}
}