每篇一句

大魔王张怡宁:女儿,这堆金牌你拿去玩吧,但我的银牌不能给你玩。你要想玩银牌就去找你王浩叔叔吧,他那银牌多

前言

为了讲述好Spring MVC最为复杂的数据绑定这块,我前面可谓是做足了功课,对此部分知识此处给小伙伴留一个学习入口,有兴趣可以点开看看:聊聊Spring中的数据绑定 --- WebDataBinder、ServletRequestDataBinder、WebBindingInitializer...【享学Spring】

@InitBinder这个注解是Spring 2.5后推出来,用于数据绑定、设置数据转换器等,字面意思是“初始化绑定器”。

关于数据绑定器的概念,前面的功课中有重点详细讲解,此处默认小伙伴是熟悉了的~

Spring MVC的web项目中,相信小伙伴们经常会遇到一些前端给后端传值比较棘手的问题:比如最经典的问题:

  • Date类型(或者LocalDate类型)前端如何传?后端可以用Date类型接收吗?
  • 字符串类型,如何保证前段传入的值两端没有空格呢?(99.99%的情况下多余的空格都是木有用的)

对于这些看似不太好弄的问题,看了这篇文章你就可以优雅的搞定了~



说明:关于Date类型的传递,业界也有两个通用的解决方案

  1. 使用时间戳
  2. 使用String字符串(传值的万能方案)

使用者两种方式总感觉不优雅,且不够面向对象。那么本文就介绍一个黑科技:使用@InitBinder来便捷的实现各种数据类型的数据绑定(咱们Java是强类型语言且面向对象的,如果啥都用字符串,是不是也太low了~)

一般的string, int, long会自动绑定到参数,但是自定义的格式spring就不知道如何绑定了 .所以要继承PropertyEditorSupport,实现自己的属性编辑器PropertyEditor,绑定到WebDataBinder ( binder.registerCustomEditor),覆盖方法setAsText



@InitBinder原理

本文先原理,再案例的方式,让你能够彻头彻尾的掌握到该注解的使用。

1、@InitBinder是什么时候生效的?

这就是前面文章埋下的伏笔:Spring在绑定请求参数到HandlerMethod的时候(此处以RequestParamMethodArgumentResolver为例),会借助WebDataBinder进行数据转换:

// RequestParamMethodArgumentResolver的父类就是它,resolveArgument方法在父类上
// 子类仅仅只需要实现抽象方法resolveName,即:从request里根据name拿值
AbstractNamedValueMethodArgumentResolver: @Override
@Nullable
public final Object resolveArgument( ... ) {
...
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
...
if (binderFactory != null) {
// 创建出一个WebDataBinder
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
// 完成数据转换(比如String转Date、String转...等等)
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
...
}
...
return arg;
}

它从请求request拿值得方法便是:request.getParameterValues(name)

2、web环境使用的数据绑定工厂是:ServletRequestDataBinderFactory

虽然在前面功课中有讲到,但此处为了连贯性还是有必要再简单过一遍:

// @since 3.1 org.springframework.web.bind.support.DefaultDataBinderFactory
public class DefaultDataBinderFactory implements WebDataBinderFactory { @Override
@SuppressWarnings("deprecation")
public final WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {
WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest); // WebBindingInitializer initializer在此处解析完成了 全局生效
if (this.initializer != null) {
this.initializer.initBinder(dataBinder, webRequest);
}
// 解析@InitBinder注解,它是个protected空方法,交给子类复写实现
// InitBinderDataBinderFactory对它有复写
initBinder(dataBinder, webRequest);
return dataBinder;
}
} public class InitBinderDataBinderFactory extends DefaultDataBinderFactory {
// 保存所有的,
private final List<InvocableHandlerMethod> binderMethods;
...
@Override
public void initBinder(WebDataBinder dataBinder, NativeWebRequest request) throws Exception {
for (InvocableHandlerMethod binderMethod : this.binderMethods) {
if (isBinderMethodApplicable(binderMethod, dataBinder)) {
// invokeForRequest这个方法不用多说了,和调用普通控制器方法一样
// 方法入参上也可以写格式各样的参数~~~~
Object returnValue = binderMethod.invokeForRequest(request, null, dataBinder); // 标注有@InitBinder注解方法必须返回void
if (returnValue != null) {
throw new IllegalStateException("@InitBinder methods must not return a value (should be void): " + binderMethod);
}
}
}
} // dataBinder.getObjectName()在此处终于起效果了 通过这个名称来匹配
// 也就是说可以做到让@InitBinder注解只作用在指定的入参名字的数据绑定上~~~~~
// 而dataBinder的这个ObjectName,一般就是入参的名字(注解指定的value值~~) // 形参名字的在dataBinder,所以此处有个简单的过滤~~~~~~~
protected boolean isBinderMethodApplicable(HandlerMethod initBinderMethod, WebDataBinder dataBinder) {
InitBinder ann = initBinderMethod.getMethodAnnotation(InitBinder.class);
Assert.state(ann != null, "No InitBinder annotation");
String[] names = ann.value();
return (ObjectUtils.isEmpty(names) || ObjectUtils.containsElement(names, dataBinder.getObjectName()));
}
}

WebBindingInitializer接口方式是优先于@InitBinder注解方式执行的(API方式是去全局的,注解方式可不一定,所以更加的灵活些)

子类ServletRequestDataBinderFactory就做了一件事:new ExtendedServletRequestDataBinder(target, objectName)

ExtendedServletRequestDataBinder只做了一件事:处理path变量。

binderMethods是通过构造函数进来的,它表示和本次请求有关的所有的标注有@InitBinder的方法,所以需要了解它的实例是如何被创建的,那就是接下来这步。

3、ServletRequestDataBinderFactory的创建

任何一个请求进来,最终交给了HandlerAdapter.handle()方法去处理,它的创建流程如下:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
...
@Override
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
...
// 处理请求,最终其实就是执行控制器的方法,得到一个ModelAndView
mav = invokeHandlerMethod(request, response, handlerMethod);
...
} // 执行控制器的方法,挺复杂的。但本文我只关心WebDataBinderFactory的创建,方法第一句便是
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
...
} // 创建一个WebDataBinderFactory
// Global methods first(放在前面最先执行) 然后再执行本类自己的
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
// handlerType:方法所在的类(控制器方法所在的类,也就是xxxController)
// 由此可见,此注解的作用范围是类级别的。会用此作为key来缓存
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) { // 缓存没命中,就去selectMethods找到所有标注有@InitBinder的方法们~~~~
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods); // 缓存起来
} // 此处注意:Method最终都被包装成了InvocableHandlerMethod,从而具有执行的能力
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>(); // 上面找了本类的,现在开始看看全局里有木有@InitBinder
// Global methods first(先把全局的放进去,再放个性化的~~~~ 所以小细节:有覆盖的效果哟~~~)
// initBinderAdviceCache它是一个缓存LinkedHashMap(有序哦~~~),缓存着作用于全局的类。
// 如@ControllerAdvice,注意和`RequestBodyAdvice`、`ResponseBodyAdvice`区分开来 // methodSet:说明一个类里面是可以定义N多个标注有@InitBinder的方法~~~~~
this.initBinderAdviceCache.forEach((clazz, methodSet) -> { // 简单的说就是`RestControllerAdvice`它可以指定:basePackages之类的属性,看本类是否能被扫描到吧~~~~
if (clazz.isApplicableToBeanType(handlerType)) { // 这个resolveBean() 有点意思:它持有的Bean若是个BeanName的话,会getBean()一下的
// 大多数情况下都是BeanName,这在@ControllerAdvice的初始化时会讲~~~
Object bean = clazz.resolveBean();
for (Method method : methodSet) {
// createInitBinderMethod:把Method适配为可执行的InvocableHandlerMethod // 特点是把本类的HandlerMethodArgumentResolverComposite传进去了
// 当然还有DataBinderFactory和ParameterNameDiscoverer等
initBinderMethods.add(createInitBinderMethod(bean, method));
}
}
});
// 后一步:再条件标注有@InitBinder的方法
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(createInitBinderMethod(bean, method));
} // protected方法,就一句代码:new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer())
return createDataBinderFactory(initBinderMethods);
}
...
}

到这里,整个@InitBinder的解析过程就算可以全部理解了。关于这个过程,我有如下几点想说:

  • 对于binderMethods每次请求过来都会新new一个(具有第一次惩罚效果),它既可以来自于全局(Advice),也可以来自于Controller本类
  • 倘若Controller上的和Advice上标注有次注解的方法名一毛一样,也是不会覆盖的(因为类不一样)
  • 关于注解有@InitBinder的方法的执行,它和执行控制器方法差不多,都是调用了InvocableHandlerMethod#invokeForRequest方法,因此可以自行类比

目前方法执行的核心,无非就是对参数的解析、封装,也就是对HandlerMethodArgumentResolver的理解。强烈推荐你可以参考 这个系列的所有文章~


有了这些基础理论的支撑,接下来当然就是它的使用Demo Show

@InitBinder的使用案例

我抛出两个需求,借助@InitBinder来实现:

  1. 请求进来的所有字符串trim一下
  2. yyyy-MM-dd这种格式的字符串能直接用Date类型接收(不用先用String接收再自己转换,不优雅)

为了实现如上两个需求,我需要先自定义两个属性编辑器:

1、StringTrimmerEditor

public class StringTrimmerEditor extends PropertyEditorSupport {

    // 将属性对象用一个字符串表示,以便外部的属性编辑器能以可视化的方式显示。缺省返回null,表示该属性不能以字符串表示
//@Override
//public String getAsText() {
// Object value = getValue();
// return (value != null ? value.toString() : null);
//} // 用一个字符串去更新属性的内部值,这个字符串一般从外部属性编辑器传入
// 处理请求的入参:test就是你传进来的值(并不是super.getValue()哦~)
@Override
public void setAsText(String text) throws IllegalArgumentException {
text = text == null ? text : text.trim();
setValue(text);
}
}

说明:Spring内置有org.springframework.beans.propertyeditors.StringTrimmerEditor,默认情况下它并没有装配进来,若你有需要可以直接使用它的(此处为了演示,我就用自己的)。Spring内置注册了哪些?参照PropertyEditorRegistrySupport#createDefaultEditors方法

Spring的属性编辑器和传统的用于IDE开发时的属性编辑器不同,它们没有UI界面,仅负责将配置文件中的文本配置值转换为Bean属性的对应值,所以Spring的属性编辑器并非传统意义上的JavaBean属性编辑器

2、CustomDateEditor

关于这个属性编辑器,你也可以像我一样自己实现。本文就直接使用Spring提供了的,参见:org.springframework.beans.propertyeditors.CustomDateEditor

// @since 28.04.2003
// @see java.util.Date
public class CustomDateEditor extends PropertyEditorSupport {
...
@Override
public void setAsText(@Nullable String text) throws IllegalArgumentException {
...
setValue(this.dateFormat.parse(text));
...
}
...
@Override
public String getAsText() {
Date value = (Date) getValue();
return (value != null ? this.dateFormat.format(value) : "");
}
}

定义好后,如何使用呢?有两种方式:

  1. API方式WebBindingInitializer ,关于它的使用,请参阅这里,本文略。

    1. 重写initBinder注册的属性编辑器是全局的属性编辑器,对所有的Controller都有效(全局的)
  2. @InitBinder注解方式

Controller本类上使用@InitBinder,形如这样:

@Controller
@RequestMapping
public class HelloController { @InitBinder
public void initBinder(WebDataBinder binder) {
//binder.setDisallowedFields("name"); // 不绑定name属性
binder.registerCustomEditor(String.class, new StringTrimmerEditor()); // 此处使用Spring内置的CustomDateEditor
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
} @ResponseBody
@GetMapping("/test/initbinder")
public String testInitBinder(String param, Date date) {
return param + ":" + date;
}
}

请求:/test/initbinder?param= ds&date=2019-12-12。结果为:ds:Thu Dec 12 00: 00: 00 CST 2019,符合预期。

注意,若date为null返回值为ds: null(因为我设置了允许为null)

但若你不是yyyy-MM-dd格式,那就抛错喽(格式化异常)

本例的@InitBinder方法只对当前Controller生效。要想全局生效,可以使用@ControllerAdvice/WebBindingInitializer

通过@ControllerAdvice可以将对于控制器的全局配置放置在同一个位置,注解了@ControllerAdvice的类的方法可以使用@ExceptionHandler@InitBinder@ModelAttribute等注解到方法上,这对所有注解了@RequestMapping的控制器内的方法有效(关于全局的方式本文略,建议各位自己实践~)。

@InitBinder的value属性的作用

获取你可能还不知道,它还有个value属性呢,并且还是数组

public @interface InitBinder {
// 用于限定次注解标注的方法作用于哪个模型key上
String[] value() default {};
}

说人话:若指定了value值,那么只有方法参数名(或者模型名)匹配上了此注解方法才会执行(若不指定,都执行)。

@Controller
@RequestMapping
public class HelloController { @InitBinder({"param", "user"})
public void initBinder(WebDataBinder binder, HttpServletRequest request) {
System.out.println("当前key:" + binder.getObjectName());
} @ResponseBody
@GetMapping("/test/initbinder")
public String testInitBinder(String param, String date,
@ModelAttribute("user") User user, @ModelAttribute("person") Person person) {
return param + ":" + date;
}
}

请求:/test/initbinder?param=fsx&date=2019&user.name=demoUser,控制台打印:

当前key:param
当前key:user

从打印结果中很清楚的看出了value属性的作用~

需要说明一点:虽然此处有key是user.name,但是User对象可是不会封装到此值的(因为request.getParameter('user')没这个key嘛~)。如何解决???需要绑定前缀,原理可参考这里

其它应用场景

上面例举的场景是此注解最为常用的场景,大家务必掌握。它还有一些奇淫技巧的使用,心有余力的小伙伴不妨也可以消化消化:

若你一次提交需要提交两个"模型"数据,并且它们有重名的属性。形如下面例子:

@Controller
@RequestMapping
public class HelloController { @Getter
@Setter
@ToString
public static class User {
private String id;
private String name;
} @Getter
@Setter
@ToString
public static class Addr {
private String id;
private String name;
} @InitBinder("user")
public void initBinderUser(WebDataBinder binder) {
binder.setFieldDefaultPrefix("user.");
} @InitBinder("addr")
public void initBinderAddr(WebDataBinder binder) {
binder.setFieldDefaultPrefix("addr.");
} @ResponseBody
@GetMapping("/test/initbinder")
public String testInitBinder(@ModelAttribute("user") User user, @ModelAttribute("addr") Addr addr) {
return user + ":" + addr;
}
}

请求:/test/initbinder?user.id=1&user.name=demoUser&addr.id=10&addr.name=北京市海淀区,结果为:HelloController.User(id=1, name=demoUser):HelloController.Addr(id=10, name=北京市海淀区)

至于加了前缀为何能绑定上,这里简要说说:

1、ModelAttributeMethodProcessor#resolveArgument里依赖attribute = createAttribute(name, parameter, binderFactory, webRequest)方法完成数据的封装、转换

2、createAttributerequest.getParameter(attributeName)看请求域里是否有值(此处为null),若木有就反射创建一个空实例,回到resolveArgument方法。

3、继续利用WebDataBinder来完成对这个空对象的数据值绑定,这个时候这些FieldDefaultPrefix就起作用了。执行方法是:bindRequestParameters(binder, webRequest),实际上是((WebRequestDataBinder) binder).bind(request);。对于bind方法的原理,就不陌生了~

4、完成Model数据的封装后,再进行@Valid校验...

参考解析类:ModelAttributeMethodProcessor对参数部分的处理

总结

本文花大篇幅从原理层面总结了@InitBinder这个注解的使用,虽然此注解在当下的环境中出镜率并不是太高,但我还是期望小伙伴能理解它,特别是我本文举例说明的例子的场景一定能做到运用自如。

最后,此注解的使用的注意事项我把它总结如下,供各位使用过程中参考:

  1. @InitBinder标注的方法执行是多次的,一次请求来就执行一次(第一次惩罚)
  2. Controller实例中的所有@InitBinder只对当前所在的Controller有效
  3. @InitBinder的value属性控制的是模型Model里的key,而不是方法名(不写代表对所有的生效)
  4. @InitBinder标注的方法不能有返回值(只能是void或者returnValue=null
  5. @InitBinder@RequestBody这种基于消息转换器的请求参数无效

    1. 因为@InitBinder它用于初始化DataBinder数据绑定、类型转换等功能,而@RequestBody它的数据解析、转换时消息转换器来完成的,所以即使你自定义了属性编辑器,对它是不生效的(它的WebDataBinder只用于数据校验,不用于数据绑定和数据转换。它的数据绑定转换若是json,一般都是交给了jackson来完成的
  6. 只有AbstractNamedValueMethodArgumentResolver才会调用binder.convertIfNecessary进行数据转换,从而属性编辑器才会生效

== 若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==

== 若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==

从原理层面掌握@InitBinder的使用【享学Spring MVC】的更多相关文章

  1. Spring MVC内容协商实现原理及自定义配置【享学Spring MVC】

    每篇一句 在绝对力量面前,一切技巧都是浮云 前言 上文 介绍了Http内容协商的一些概念,以及Spring MVC内置的4种协商方式使用介绍.本文主要针对Spring MVC内容协商方式:从步骤.原理 ...

  2. RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】

    每篇一句 人圆月圆心圆,人和家和国和---中秋节快乐 前言 在阅读本篇之前,建议先阅读开山篇效果更佳.RestTemplate是Spring提供的用于访问Rest服务的客户端工具,它提供了多种便捷访问 ...

  3. HandlerMethodArgumentResolver(二):Map参数类型和固定参数类型【享学Spring MVC】

    每篇一句 黄金的导电性最好,为什么电脑主板还是要用铜? 飞机最快,为什么还有人做火车? 清华大学最好,为什么还有人去普通学校? 因为资源都是有限的,我们现实生活中必须兼顾成本与产出的平衡 前言 上文 ...

  4. Spring MVC内置支持的4种内容协商方式【享学Spring MVC】

    每篇一句 十个光头九个富,最后一个会砍树 前言 不知你在使用Spring Boot时是否对这样一个现象"诧异"过:同一个接口(同一个URL)在接口报错情况下,若你用rest访问,它 ...

  5. 内容协商在视图View上的应用【享学Spring MVC】

    每篇一句 人生很有意思:首先就得活得长.活得长才能够见自己,再长就可以见众生 前言 在经过 前两篇 文章了解了Spring MVC的内容协商机制之后,相信你已经能够熟练的运用Spring MVC提供的 ...

  6. HandlerMethodArgumentResolver(三):基于消息转换器的参数处理器【享学Spring MVC】

    每篇一句 一个事实是:对于大多数技术,了解只需要一天,简单搞起来只需要一周.入门可能只需要一个月 前言 通过 前面两篇文章 的介绍,相信你对HandlerMethodArgumentResolver了 ...

  7. RestTemplate相关组件:ClientHttpRequestInterceptor【享学Spring MVC】

    每篇一句 做事的人和做梦的人最大的区别就是行动力 前言 本文为深入了解Spring提供的Rest调用客户端RestTemplate开山,对它相关的一些组件做讲解. Tips:请注意区分RestTemp ...

  8. HandlerMethodArgumentResolver(一):Controller方法入参自动封装器【享学Spring MVC】

    每篇一句 你的工作效率高,老板会认为你强度不够.你代码bug多,各种生产环境救火,老板会觉得你是团队的核心成员. 前言 在享受Spring MVC带给你便捷的时候,你是否曾经这样疑问过:Control ...

  9. 解决多字段联合逻辑校验问题【享学Spring MVC】

    每篇一句 不要像祥林嫂一样,天天抱怨着生活,日日思考着辞职.得罪点说一句:"沦落"到要跟这样的人共事工作,难道自己身上就没有原因? 前言 本以为洋洋洒洒的把Java/Spring数 ...

随机推荐

  1. Mermaid

    graph TD; A-->B; A-->C; B-->D; C-->D;

  2. Python入门基础(10)_异常_1

    最近有点忙,到现在快一个月没写了,罪过罪过,继续学习 异常:python程序在运行时,如果python解释器遇到一个错误,那么程序就会停止执行,并且会提示一些错误信息,这就是异常. 抛出异常:程序停止 ...

  3. Mybatis案例超详解(上)

    Mybatis案例超详解(上) 前言: 本来是想像之前一样继续跟新Mybatis,但由于种种原因,迟迟没有更新,快开学了,学了一个暑假,博客也更新了不少,我觉得我得缓缓,先整合一些案例练练,等我再成熟 ...

  4. java字符串详解

    一.String 类的定义 public final class String implements java.io.Serializable, Comparable<String>, C ...

  5. threejs 学习之

    主要内容: 使用 threejs 创建 20x20 的网格,鼠标移动时,方块跟随移动,点击时在网格任意位置放置方块,按 shift 时,删除当前位置方块. 流程如下: 创建网格 创建一个与网格同样尺寸 ...

  6. Visual Studio 2015&2017 key

    Visual Studio 2015 key Key : HMGNV-WCYXV-X7G9W-YCX63-B98R2 Visual Studio Enterprise 2015 Key :HM6NR- ...

  7. SpringBoot电商项目实战 — Redis实现分布式锁

    最近有小伙伴发消息说,在Springboot系列文第二篇,zookeeper是不是漏掉了?关于这个问题,其实我在写第二篇的时候已经考虑过,但基于本次系列文章是实战练习,在项目里你能看到Zookeepe ...

  8. java 判断 string 转 integer 判断

    NumberUtils.isDigits("1") NumberUtils.isDigits("/") 根据返回 true false 再确定是否转换即可 需要 ...

  9. Java之Exception

    Exception这个东西,程序中必须会有的,尽管我们很不乐意看到它,可是从另一个角度考虑,有异常则说明程序有问题,有助于我们及时改正.有的时候程序出错的原因有很多,比如不合法的输入.类型.空指针甚至 ...

  10. IO核心子系统

    IO核心子系统 一.IO层次结构 IO实现普遍采用了层次式的结构.其基本思想与计算机网络中的层次结构相同:将系统IO的功能组织成一系列的层次,每一层完成整个系统功能的一个子集,其实现依赖于下层完成更原 ...