首页 > 其他分享 >一步一步自定义SpringMVC参数解析器

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

时间:2023-01-02 15:33:02浏览次数:64  
标签:解析器 自定义 一步 SpringMVC 绑定 binder 参数 parameter

​​

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

题图:from Zoommy

干货

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

缘起

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

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

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

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

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

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

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

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

示例代码如下:

  • 自定义解析器实现
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;
}
}
  • 自定义注解
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyParam {
}
  • 在springmvc配置文件中注册解析器
<mvc:annotation-driven>
<!--MyArgumentsResolver-->
<mvc:argument-resolvers>
<bean class="xxx.xxx.xxx.MyArgumentsResolver"/>
</mvc:argument-resolvers>
</mvc:annotation-driven>

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

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

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

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

@Override
public 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​​方法示例如下

@Override
public 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​​进行兼容处理为例,在解析对象类型的解析器实现中进行修改

@Override
public 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/​

 



标签:解析器,自定义,一步,SpringMVC,绑定,binder,参数,parameter
From: https://blog.51cto.com/u_15147537/5983743

相关文章

  • odoo10如何自定义自动生成单据编号
    1.在已有的model中穿件一个字段nameclassqingjiadan(models.Model):_name='qingjia.qingjiadan'name=fields.Char(string='编号',readonly=True)2.创建qingjia_app......
  • jsp自定义标签
    jsp自定义标签   需求:向浏览器输出当前客户的IP地址(只能使用jsp标签)1.自定义标签开发步骤    1. 编写一个普通的java类,继承SimpleTagSupport......
  • Django自定义分页器
    目录Django自定义分页器一、分页器思路二、自定义分页器的使用Django自定义分页器一、分页器思路分页器主要听处理逻辑代码最后很简单推导流程 1.queryset支持切片......
  • SpringMVC文件上传下载实战(单文件、多文件)
    目录​​前言​​​​案例分析​​​​核心思路拆解​​​​案例所涉及知识点​​​​创建Springmvc项目​​​​项目创建​​​​目录介绍​​​​单文件上传​​​​前端......
  • WPF 自定义附加事件
    我们都知道路由事件,而附加路由事件用的比较少。但如果是通用的场景,类似附加属性,附加事件就很有必要的。举个例子,输有这么多输入事件Mouse、Touch、Stylus,另外按钮Click还处......
  • C/S UDP通信实践踩坑记录与对于ICMP的进一步认识
    背景最近有个业务场景需要服务端(简称S)与客户端(简称C)设计一套基于UDP的通信协议--要求尽可能快的前提下可容忍一定丢包率,得以比较深入地学习和了解UDP通信和实践,在开发......
  • 自定义注解
    记录一点关于自定义注解的小事儿记录一些遇到过的问题“Cannotfindmethod'value'”定义了一个自定义注解,@Target({ElementType.FIELD})@Retention(RetentionPolicy......
  • 自定义工具类之“分割所有类型的字符串”
    自定义工具类之“分割所有类型的字符串”/***<p>默认根据,,\t\n\r分隔符分隔出list</p>**<pre>*tokenizeToStringArray(null)......
  • element ui Form 自定义校验规则,验证手机号
    网站快速成型工具Element,一套为开发者、设计师和产品经理准备的基于Vue2.0的桌面端组件库指南了解设计指南,帮助产品设计人员搭建逻辑清晰、结构合理且高效易用的产品。​......
  • 使用Xcode 制作自定义storyboard启动界面(用一张全屏图快速适配全部设备),供uniAPP使用。
    1新建项目想要全屏显示并适应所有尺寸的iPad和iphone需要用750*16242X和1125*24363X大小的图片这里做完就可以导出文件了把文件和图片放到一起见......