前言
为了讲述好Spring MVC最为复杂的数据绑定这块,我前面可谓是做足了功课,对此部分知识此处给小伙伴留一个学习入口,有兴趣可以点开看看:聊聊Spring中的数据绑定 — WebDataBinder、ServletRequestDataBinder、WebBindingInitializer…【享学Spring】
@InitBinder这个注解是Spring 2.5后推出来,用于数据绑定、设置数据转换器等,字面意思是“初始化绑定器”。
关于数据绑定器的概念,前面的功课中有重点详细讲解,此处默认小伙伴是熟悉了的~
在Spring MVC的web项目中,相信小伙伴们经常会遇到一些前端给后端传值比较棘手的问题:比如最经典的问题:
- Date类型(或者LocalDate类型)前端如何传?后端可以用Date类型接收吗?
- 字符串类型,如何保证前段传入的值两端没有空格呢?(99.99%的情况下多余的空格都是木有用的)
对于这些看似不太好弄的问题,看了这篇文章你就可以优雅的搞定了~
说明:关于Date类型的传递,业界也有两个通用的解决方案:
- 使用时间戳
- 使用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方法,因此可以自行类比