SpringMVC Resolver,Converter在JPA中的体现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/about4years/article/details/88074160

在看公司文档的时候,看到这段话:

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());
    }
}

猜你喜欢

转载自blog.csdn.net/about4years/article/details/88074160