一步一步自定义SpringMVC参数解析器

随心所欲,自定义参数解析器绑定数据。

题图:from Zoommy

干货

  1. SpringMVC解析器用于解析request请求参数并绑定数据到Controller的入参上。
  2. 自定义一个参数解析器需要实现HandlerMethodArgumentResolver接口,重写supportsParameterresolveArgument方法,配置文件中加入resolver配置。
  3. 如果需要多个解析器同时生效需要在一个解析器中对其他解析器做兼容

缘起

为什么要自定义一个解析器呢?

源于需要对前端请求参数进行手动URLDecode,也即除了Web容器自动decode一次,代码内还需要再decode一次。

针对这种需求,首先想到的是filter或者interceptor实现,但是由于HttpServletRequest对象本身是不提供setParameter()方法的,因此想要修改request中的参数值为decode后的值是不易达到的。

SpringMVC的HandlerMethodArgumentResolver,解析器;其功能就是解析request请求参数并绑定数据到Controller的入参上。因此自定义解析器加入URLDecode逻辑即可完全满足需求。

下面,就一步一步的完成一个解析器由简到繁的实现过程。

实现一个极其简单的参数解析器

具体如何自定义一个参数解析器呢?

其实很简单,一句话——实现HandlerMethodArgumentResolver接口,重写supportsParameterresolveArgument方法,配置文件中加入resolver配置。

示例代码如下:

  • 自定义解析器实现

    1234567891011121314151617181920212223242526
    public class MyArgumentsResolver implements HandlerMethodArgumentResolver {    /**     * 解析器是否支持当前参数     */    @Override    public boolean supportsParameter(MethodParameter parameter) {        // 指定参数如果被应用MyParam注解,则使用该解析器。        // 如果直接返回true,则代表将此解析器用于所有参数        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;    }}
  • 自定义注解
    12345
    @Target({ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface MyParam {}
  • 在springmvc配置文件中注册解析器
    123456
    <mvc:annotation-driven>        <!--MyArgumentsResolver-->        <mvc:argument-resolvers>            <bean class="xxx.xxx.xxx.MyArgumentsResolver"/>        </mvc:argument-resolvers></mvc:annotation-driven>

好了,现在解析器会把所有应用了@MyParam注解的参数都赋值为null

实现一个解析原始类型的参数解析器

对于如何解析原始类型参数,SpringMVC已经有了一个内置的实现——RequestParamMethodArgumentResolver,因此完全可以参考这个实现来自定义我们自己的解析器。

如上所述,解析器逻辑的主要部分都在resolveArgument方法内,这里就说说自定义该方法的实现。

12345678910111213141516171819202122232425262728
@Overridepublic Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,                              NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

// 解析器中的自定义逻辑——urldecode    Object arg = URLDecoder.decode(webRequest.getParameter(parameter.getParameterName()), "UTF-8");

// 将解析后的值绑定到对应的Controller参数上,利用DataBinder提供的方法便捷的实现类型转换    if (binderFactory != null) {

// 生成参数绑定器,第一个参数为request请求对象,第二个参数为需要绑定的目标对象,第三个参数为需要绑定的目标对象名        WebDataBinder binder = binderFactory.createBinder(webRequest, null, parameter.getParameterName());

try {

// 将参数转到预期类型,第一个参数为解析后的值,第二个参数为绑定Controller参数的类型,第三个参数为绑定的Controller参数            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方法示例如下

12345678910111213141516171819202122232425262728293031323334353637383940414243
@Overridepublic Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,                              NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

String name = parameter.getParameterName();

// 查找是否已有名为name的参数值的映射,如果没有则创建一个    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);        }

// 将参数转到预期类型,第一个参数为解析后的值,第二个参数为绑定Controller参数的类型,第三个参数为绑定的Controller参数        attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);    }

return attribute;}

protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) throws UnsupportedEncodingException {    // 将key-value封装为map,传给bind方法进行参数值绑定    Map<String, String> map = new HashMap<>();    Map<String, String[]> params = request.getParameterMap();

for (Map.Entry<String, String[]> entry : params.entrySet()) {        String name = entry.getKey();        // 执行urldecode        String value = URLDecoder.decode(entry.getValue()[0], "UTF-8");        map.put(name, value);    }

PropertyValues propertyValues = new MutablePropertyValues(map);

// 将K-V绑定到binder.target属性上    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进行兼容处理为例,在解析对象类型的解析器实现中进行修改

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
@Overridepublic Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,                              NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

String name = parameter.getParameterName();

// 查找是否已有名为name的参数值的映射,如果没有则创建一个    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);

// -----------------------------------对@Valid做兼容----------------------------------------------------

// 如果使用了validation校验, 则进行相应校验            if (parameter.hasParameterAnnotation(Valid.class)) {                // 如果有校验报错,会将结果放在binder.bindingResult属性中                binder.validate();            }

// 如果参数中不包含BindingResult参数,直接抛出异常            if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {                throw new BindException(binder.getBindingResult());            }        }

// 关键,使Controller中接下来的BindingResult参数可以接收异常        Map bindingResultModel = binder.getBindingResult().getModel();        mavContainer.removeAttributes(bindingResultModel);        mavContainer.addAllAttributes(bindingResultModel);

// -----------------------------------对@Valid做兼容----------------------------------------------------

// 将参数转到预期类型,第一个参数为解析后的值,第二个参数为绑定Controller参数的类型,第三个参数为绑定的Controller参数        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解析器的一次学习总结吧。

 

http://coderec.cn/2016/08/27/%E4%B8%80%E6%AD%A5%E4%B8%80%E6%AD%A5%E8%87%AA%E5%AE%9A%E4%B9%89SpringMVC%E5%8F%82%E6%95%B0%E8%A7%A3%E6%9E%90%E5%99%A8/

 

时间: 2024-12-31 03:51:37

一步一步自定义SpringMVC参数解析器的相关文章

springmvc 视图解析器

问题描述 我的项目是由三个部分组成,一个是放后台代码,一个是放html页面,在配置文件里,配置视图时,前缀应该怎么写? 解决方案 解决方案二:不太明白楼主问题,SpringMVC的视图解析器跟你项目组成放哪里有什么关系.?你指的前缀又是什么...解决方案三:是这样,我的项目是分开的,就是java代码一个项目,页面一个项目,那我在springmvc的配置文件里的视图解析器是不是要配找页面的路径和页面的后缀,如果是在一个项目里边就没问题,现在项目是分离了,找不到另一个放页面的项目的路径了...解决方

使用70行Python代码实现一个递归下降解析器的教程_python

 第一步:标记化 处理表达式的第一步就是将其转化为包含一个个独立符号的列表.这一步很简单,且不是本文的重点,因此在此处我省略了很多. 首先,我定义了一些标记(数字不在此中,它们是默认的标记)和一个标记类型:   token_map = {'+':'ADD', '-':'ADD', '*':'MUL', '/':'MUL', '(':'LPAR', ')':'RPAR'} Token = namedtuple('Token', ['name', 'value']) 下面就是我用来标记 `expr`

springMVC的拦截器

SpringMVC中的Interceptor拦截器也是相当重要和相当有用的,它的主要作用是拦截用户的请求并进行相应的处理.比如通过它来进行权限验证,或者是来判断用户是否登陆,或者是像12306那样子判断当前时间是否是购票时间.    一.定义Interceptor实现类    SpringMVC中的Interceptor拦截请求是通过HandlerInterceptor来实现的.在SpringMVC中定义一个Interceptor非常简单,主要有两种方式,第一种方式是要定义的Intercepto

一步一步写算法(之 可变参数)

原文:一步一步写算法(之 可变参数) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com]     可变参数是C语言编程的一个特色.在我们一般编程中,函数的参数个数都是确定的,事先定下来的.然而就有那么一部分函数,它的个数是不确定的,长度也不一定,这中间有什么秘密吗?     其实,我们可以回忆一下哪些函数是可变参数的函数?其实也就是sprintf.printf这样的函数而已.那么这些函数有什么规律吗?关键就是在这个字符串上面.我们可以举一个例

Android-6步教你自定义View

如果你打算完全定制一个View,那么你需要实现View类(所有的Android View都实现于这个类),并且实现确定尺寸的onMeasure(-))方法和确认绘图的onDraw(-))方法. 自定义View一共分为6步 第一步 public class SmileyView extends View {      private Paint mCirclePaint;      private Paint mEyeAndMouthPaint;        private float mCen

使用ANTLR进行命令行参数解析

关于命令行参数的解析没有特定的规则,目前比较流行的有unix风格和微软风格.其实除了unix风格 的比较一致外,微软自己提供的命令行参数解析就有很多种风格.在.net平台下的main函数中,仅仅把参 数分解为以空格分割的数组,这对需要加开关,并且有的开关有自己的参数的情况是不够的,而且为了解 析这些参数需要学习部分词法分析的知识,这对用处不是很大的命令行参数显得有些"鸡肋",当然用 Antlr来处理命令行参数更显得有些鸡肋,并且是大才小用,因为Antlr的语法规则比较复杂,学习起来有

大流量网站性能优化:一步一步打造一个适合自己的BigRender插件(转)

BigRender 当一个网站越来越庞大,加载速度越来越慢的时候,开发者们不得不对其进行优化,谁愿意访问一个需要等待 10 秒,20 秒才能出现的网页呢? 常见的也是相对简单易行的一个优化方案是 图片的延迟加载.一个庞大的页面,有时我们并不会滚动去看下面的内容,这样就浪费了非首屏部分的渲染,而这些无用的渲染,不仅包括图片,还包括其他的 DOM 元素,甚至一些 js/css(某些js/css 是根据模块请求的,比如一些 ajax),理论上,每增加一个 DOM,都会增加渲染的时间.有没有办法能使得

一步一步学习SignalR进行实时通信_5_Hub

原文:一步一步学习SignalR进行实时通信_5_Hub 一步一步学习SignalR进行实时通信\_5_Hub SignalR 一步一步学习SignalR进行实时通信_5_Hub 前言 Hub命名规则 Hub封装好的常用方法 Hub常用方法解释 保持状态 前后台交互 结束语 参考文献 前言 上一讲,我们简单的介绍了下Hub的配置以及实现方法,这一将我希望把更多的细节梳理清楚,不至于让大家在细节上面摸不着头脑,理解深了,那么做项目自然就会相对轻松一些. Hub命名规则 Hub与Persistent

一步一步封装自己的HtmlHelper组件BootstrapHelper(二)_javascript技巧

前言:上篇介绍了下封装BootstrapHelper的一些基础知识,这篇继续来完善下.参考HtmlHelper的方式,这篇博主先来封装下一些常用的表单组件.关于BootstrapHelper封装的意义何在,上篇评论里面已经讨论得太多,这里也不想过多纠结.总之一句话:凡事有得必有失,就看你怎么去取舍.有兴趣的可以看看,没兴趣的权当博主讲了个"笑话"吧. BootstrapHelper系列文章目录 C#进阶系列--一步一步封装自己的HtmlHelper组件:BootstrapHelper