SpringMVC 参数解析 ArgumentResolver

引出

写控制层的时候,我们发现对于一类方法,经常要将一些参数从Session解析,然后固定装入某个pojo中。比如不论创建什么记录,都要将当前用户作为创建人写入相应DO的字段,而这些字段名往往都是一样的。这样势必会有重复编码,那有没有可以偷懒的办法呢?

参数解析器

springmvc提供了HandlerMethodArgumentResolver接口作为定义参数解析策略的入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface HandlerMethodArgumentResolver {

// 该解析器能解析哪些参数
boolean supportsParameter(MethodParameter parameter);

/**
* 参数解析的具体实现
* @param parameter 参数信息
* @param mavContainer mav容器
* @param webRequest 当前请求
* @param binderFactory WebDataBinder 工厂
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;

}

对于上述问题,一般而言,可以在控制层方法入参多写一个SessionBean类的参数,然后写一个参数解析器,将Session中的数据装载进去。最后在方法体中进行Bean之间的属性拷贝即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SessionBeanArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(SessionBean.class);
}

@Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {

return BaseReqPojoBuilder.build(nativeWebRequest.getNativeRequest(HttpServletRequest.class),
MethodType.getMethodType(methodParameter.getMethod().getName()));
}


}
1
2
3
4
5
@PostMapping("/foo")
public ResultDTO foo(FooQuery query, SessionBean session) {
BeanUtils.copyProperties(session, query);
... ...
}

不要忘记注册该参数解析器,单纯@Component注解是无用的。注册方法多种多样,列一个java注解式的:

1
2
3
4
5
6
7
8
9
10
@Configuration
public class Config extends WebMvcConfigurationSupport {

... ...

@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new SessionBeanArgumentResolver()); //要在默认解析器之前注册
super.addArgumentResolvers(argumentResolvers);
}

至此已经省事不少,但看到每个方法体中重复的copyProperties,强迫症又犯。

略加探索,确实还有更隐蔽的做法。

@ModelAttribute 参数解析器

我们想直接对原有的比如FooQuery类型解析动刀,那就要清楚,spring默认的参数解析器里,哪个是干组装POJO类型的活的。

你或许想到对于每个Controller,可以定义@InitBinder标识的方法,在方法里注册自定义的PropertyEditor,似乎可以实现这个需求:

1
2
3
4
5
6
7
@InitBinder
protected void initBinder(WebDataBinder binder){
binder.registerCustomEditor(String.class, new XssStripStringEditor());
... ...
// ??
binder.registerCustomEditor(SessionBean.class, new SessionBeanEditor());
}

但你会发现,到binder这一层范围太窄,PropertyEditor完成的是String到特定类型数据的转换,WebDataBinder完成的是表单到JavaBean属性的绑定,如果前端传来的是bean的json字串,或许还可以一用。

我们需要的是更上一层的定制化参数解析。这就引出了ServletModelAttributeMethodProcessor

该类命名为Processor而不是ArgumentResolver是因为其继承ModelAttributeMethodProcessor, 该类又同时实现了HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler两个接口。我们只关注参数解析那一部分。

  1. 先看其支持的类型:
1
2
3
4
5
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}

其支持@ModelAttribute标注的入参(这一点基本上没人用),以及默认条件下(annotationNotRequired默认设置为true)的非’简单’参数。

何为简单参数?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static boolean isSimpleProperty(Class<?> clazz) {
Assert.notNull(clazz, "Class must not be null");
return isSimpleValueType(clazz) || (clazz.isArray() && isSimpleValueType(clazz.getComponentType()));
}

public static boolean isSimpleValueType(Class<?> clazz) {
return (ClassUtils.isPrimitiveOrWrapper(clazz) ||
Enum.class.isAssignableFrom(clazz) ||
CharSequence.class.isAssignableFrom(clazz) ||
Number.class.isAssignableFrom(clazz) ||
Date.class.isAssignableFrom(clazz) ||
URI.class == clazz || URL.class == clazz ||
Locale.class == clazz || Class.class == clazz);
}

总结来说:基本类型及其包装类、枚举、字串、日期、URI、URL、Local、Class类型和上述类型的数组。

不是上述类型都会归该解析器管。自然,自定义的pojo也是。

  1. 解析参数的过程

简单而言分为两部:创建对象,属性绑定。

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
// 不可重写
@Override
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
// 创建对象
Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
createAttribute(name, parameter, binderFactory, webRequest));
// 参数绑定
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}

// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);

return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}

该方法为final,不可扩展。

  1. 1创建对象

    其父类直接用默认无参构造器构造一个对象,该方法可继承扩展,到了ServletModelAttributeMethodProcessor,会先判断是否有同名参数,若有,试图用ConversionService转化成bean,若无,就用父类的方法。

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
// 不可重写
@Override
protected final Object createAttribute(String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
// 从请求中拿取同名参数的值
String value = getRequestValueForAttribute(attributeName, request);
if (value != null) {
// 不为空则试图用此值创建对象
Object attribute = createAttributeFromRequestValue(
value, attributeName, parameter, binderFactory, request);
if (attribute != null) {
return attribute;
}
}
// 尝试失败就用父类创建方式,即默认无参构造方法
return super.createAttribute(attributeName, parameter, binderFactory, request);
}

// 创建方法
protected Object createAttributeFromRequestValue(String sourceValue, String attributeName,
MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest request)
throws Exception {

DataBinder binder = binderFactory.createBinder(request, null, attributeName);
// 使用ConversionService
ConversionService conversionService = binder.getConversionService();
if (conversionService != null) {
// 将String
TypeDescriptor source = TypeDescriptor.valueOf(String.class);
// 转成对象
TypeDescriptor target = new TypeDescriptor(parameter);
if (conversionService.canConvert(source, target)) {
return binder.convertIfNecessary(sourceValue, parameter.getParameterType(), parameter);
}
}
return null;
}

有趣的是,这里用DataBinderConversionService,前面不是有PropertyEditor了吗,感觉干的是类似的事情。

1
2
3
// 一个DataBinder同时实现了属性编辑器注册(propertyEditorRegitry) 和 类型转换器
public class DataBinder implements PropertyEditorRegistry, TypeConverter {
... ...

仔细翻看源码,粗略理解:

a. DataBinder实现TypeConverterPropertyEditorRegistry的方式,是代理了SimpleTypeConverter

b. SimpleTypeConverter的父类PropertyEditorRegistrySupport实现了属性编辑器注册。

c. PropertyEditorRegistrySupport内含 ConversionService与一系列PropertyEditor(默认和自定义)。

d. converter是spring自定义的一套数据转换逻辑,关于ConverterPropertiyEditor的区别可以参考博文https://blog.csdn.net/pentiumchen/article/details/44066173

2.2 参数绑定

直接用binder,不过与其父类不同,ServletModelAttributeMethodProcessor使用的是ServletRequestDataBinder

  1. 问题实现

回到问题上,既然这些属性都是公共的,那就定义一个基类(或接口)作为父类,抽出公共部分的字段:

1
2
3
4
5
6
@Bean
public class SessionBase {
private String createMan;
private Long createManId;
... ...
}

然后继承扩展Processor:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class SessionBaseBeanArgumentResolver extends ModelAttributeMethodProcessor {


public BaseDOArgumentResolver() {
super(true);
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> type = parameter.getParameterType();
return super.supportsParameter(parameter) &&
(type == SessionBase.class || SessionBase.class.isAssignableFrom(type));
}

/**
* 生成一个已经注入session信息的SessionBase bean
* @param attributeName
* @param parameter
* @param binderFactory
* @param webRequest
* @return
* @throws Exception
*/
@Override
protected Object createAttribute(String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
String methodName = parameter.getMethod().getName();
SessionBase plain = (SessionBase) super.createAttribute(attributeName, parameter, binderFactory, webRequest);
MethodType type = MethodType.parseMethodType(methodName);
HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest();
String token = httpServletRequest.getParameter(LoginConstants.SESSION_KEY);
LoginSessionBean sessionBean = getSession(token);
doAssembleDoBase(plain, type, sessionBean);
return plain;
}

/**
* 实际上用的是 ExtendedServletRequestDataBinder,而源码默认强转为 WebRequestDataBinder
* 两者同祖但无父子关系,故会报错,所以这里要重写这个方法。
* RequestMappingHandlerAdapter -> ServletRequestDataBinderFactory -> ExtendedServletRequestDataBinder
* @param binder
* @param request
*/
@Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
servletBinder.bind(servletRequest);
}

private void doAssembleSessionBase(SessionBase plain, MethodType type, LoginSessionBean loginService) {

plain.setIsDeleted(EnumSysConstant.IsDeletedFlag.NO.getFlag());
if (type instanceof MethodType.CREATE) {
plain.setCreateOperatorId(Long.valueOf(loginService.getOperatorid()));
plain.setCreateOperatorName(loginService.getOperator());
plain.setGmtCreate(new Date());
} else if (type instanceof MethodType.UPDATE) {
plain.setUpdateOperatorId(Long.valueOf(loginService.getOperatorid()));
plain.setUpdateOperatorName(loginService.getOperator());
plain.setGmtModified(new Date());
}

if (plain instanceof LoginIdentified) {
((LoginIdentified) plain).setPartyId(Long.valueOf(loginService.getPartyid()));
}

}
}

因为 ServletModelAttributeMethodProcessor难于扩展,故使用其父类。

这样便实现了要求。