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来实现:
请求进来的所有字符串都trim一下
yyyy-MM-dd这种格式的字符串能直接用Date类型接收(不用先用String接收再自己转换,不优雅)
为了实现如上两个需求,我需要先自定义两个属性编辑器: