随心所欲,自定义参数解析器绑定数据。
题图:from Zoommy
干货
- SpringMVC解析器用于解析request请求参数并绑定数据到Controller的入参上。
- 自定义一个参数解析器需要实现
HandlerMethodArgumentResolver
接口,重写supportsParameter
和resolveArgument
方法,配置文件中加入resolver配置。
- 如果需要多个解析器同时生效需要在一个解析器中对其他解析器做兼容
缘起
为什么要自定义一个解析器呢?
源于需要对前端请求参数进行手动URLDecode,也即除了Web容器自动decode一次,代码内还需要再decode一次。
针对这种需求,首先想到的是filter或者interceptor实现,但是由于HttpServletRequest
对象本身是不提供setParameter()
方法的,因此想要修改request中的参数值为decode后的值是不易达到的。
SpringMVC的HandlerMethodArgumentResolver
,解析器;其功能就是解析request请求参数并绑定数据到Controller的入参上。因此自定义解析器加入URLDecode逻辑即可完全满足需求。
下面,就一步一步的完成一个解析器由简到繁的实现过程。
实现一个极其简单的参数解析器
具体如何自定义一个参数解析器呢?
其实很简单,一句话——实现HandlerMethodArgumentResolver
接口,重写supportsParameter
和resolveArgument
方法,配置文件中加入resolver配置。
示例代码如下:
自定义解析器实现
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
| public class MyArgumentsResolver implements HandlerMethodArgumentResolver { * 解析器是否支持当前参数 */ @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(MyParam.class); } * 将request中的请求参数解析到当前Controller参数上 * @param parameter 需要被解析的Controller参数,此参数必须首先传给{@link #supportsParameter}并返回true * @param mavContainer 当前request的ModelAndViewContainer * @param webRequest 当前request * @param binderFactory 生成{@link WebDataBinder}实例的工厂 * @return 解析后的Controller参数 */ @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { return null; } }
|
自定义注解
1 2 3 4 5
| @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyParam { }
|
在springmvc配置文件中注册解析器
1 2 3 4 5 6
| <mvc:annotation-driven> <mvc:argument-resolvers> <bean class="xxx.xxx.xxx.MyArgumentsResolver"/> </mvc:argument-resolvers> </mvc:annotation-driven>
|
好了,现在解析器会把所有应用了@MyParam
注解的参数都赋值为null
。
实现一个解析原始类型的参数解析器
对于如何解析原始类型参数,SpringMVC已经有了一个内置的实现——RequestParamMethodArgumentResolver
,因此完全可以参考这个实现来自定义我们自己的解析器。
如上所述,解析器逻辑的主要部分都在resolveArgument
方法内,这里就说说自定义该方法的实现。
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
| @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { Object arg = URLDecoder.decode(webRequest.getParameter(parameter.getParameterName()), "UTF-8"); if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, null, parameter.getParameterName()); try { arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } catch (ConversionNotSupportedException ex) { throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(), parameter.getParameterName(), parameter, ex.getCause()); } catch (TypeMismatchException ex) { throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(), parameter.getParameterName(), parameter, ex.getCause()); } } return arg; }
|
添加解析对象类型参数的功能
对于如何解析对象类型参数,SpringMVC内也有了一个内置的实现——ModelAttributeMethodProcessor
,我们也是参考这个实现来自定义我们自己的解析器。
同样,resolveArgument
方法示例如下
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
| @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String name = parameter.getParameterName(); Object attribute = mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) : this.createAttribute(name, parameter, binderFactory, webRequest); if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null) { this.bindRequestParameters(binder, webRequest); } attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter); } return attribute; } protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) throws UnsupportedEncodingException { Map<String, String> map = new HashMap<>(); Map<String, String[]> params = request.getParameterMap(); for (Map.Entry<String, String[]> entry : params.entrySet()) { String name = entry.getKey(); String value = URLDecoder.decode(entry.getValue()[0], "UTF-8"); map.put(name, value); } PropertyValues propertyValues = new MutablePropertyValues(map); binder.bind(propertyValues); }
|
同时支持多个参数解析器生效
到目前为止,不论对于原始类型或者对象类型的参数,我们都可以自定义一个参数解析器了,但是还有一个很严重的问题存在——无法让自定义解析器和现有解析器同时生效。
举个例子,public String myController(@Valid @MyParam param, BindingResult result){}
,这个方法在执行时是会报错的。他会提示类似如下报错:
An Errors/BindingResult argument is expected to be declared immediately after the model attribute, the @RequestBody or the @RequestPart arguments
是SpringMVC不支持同时使用两个解析器吗?public String myController(@Valid @ModelAttribute param, BindingResult result){}
,也是两个内置解析器,没有任何问题。
再去看ModelAttributeMethodProcessor
的实现,原来是对@Valid
做了兼容处理。
因此, 如果需要多个解析器同时生效需要在一个解析器中对其他解析器做兼容。
这里仅以对@Valid
进行兼容处理为例,在解析对象类型的解析器实现中进行修改
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
| @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String name = parameter.getParameterName(); Object attribute = mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) : this.createAttribute(name, parameter, binderFactory, webRequest); if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null) { this.bindRequestParameters(binder, webRequest); if (parameter.hasParameterAnnotation(Valid.class)) { binder.validate(); } if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) { throw new BindException(binder.getBindingResult()); } } Map bindingResultModel = binder.getBindingResult().getModel(); mavContainer.removeAttributes(bindingResultModel); mavContainer.addAllAttributes(bindingResultModel); attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter); } return attribute; } * 检查参数中是否包含BindingResult参数 */ protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) { int i = methodParam.getParameterIndex(); Class[] paramTypes = methodParam.getMethod().getParameterTypes(); boolean hasBindingResult = paramTypes.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]); return !hasBindingResult; }
|
OK,到这里,我们自定义的解析器已经可以算是一个完善的参数解析器了,如果有对其他解析器做兼容的需要,只要参照此类方法稍作修改即可。
后记
还记得这次自定义解析器的原因吗——需要对前端请求参数进行手动URLDecode,也即除了Web容器自动decode一次,代码内还需要再decode一次。
事实证明,根本不需要进行二次decode,写出的解析器也就无疾而终了,仅存这篇整理,算是对SpringMVC解析器的一次学习总结吧。