从原理层面掌握@InitBinder的使用【享学Spring MVC】(上)

简介: 从原理层面掌握@InitBinder的使用【享学Spring MVC】(上)

前言


为了讲述好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方法,因此可以自行类比
相关文章
|
25天前
|
Java 关系型数据库 数据库
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
在Java开发中,Spring框架通过事务管理机制,帮我们轻松实现了这种“承诺”。它不仅封装了底层复杂的事务控制逻辑(比如手动开启、提交、回滚事务),还提供了灵活的配置方式,让开发者能专注于业务逻辑,而不用纠结于事务细节。
|
2月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
175 0
|
2月前
|
缓存 安全 Java
Spring 框架核心原理与实践解析
本文详解 Spring 框架核心知识,包括 IOC(容器管理对象)与 DI(容器注入依赖),以及通过注解(如 @Service、@Autowired)声明 Bean 和注入依赖的方式。阐述了 Bean 的线程安全(默认单例可能有安全问题,需业务避免共享状态或设为 prototype)、作用域(@Scope 注解,常用 singleton、prototype 等)及完整生命周期(实例化、依赖注入、初始化、销毁等步骤)。 解析了循环依赖的解决机制(三级缓存)、AOP 的概念(公共逻辑抽为切面)、底层动态代理(JDK 与 Cglib 的区别)及项目应用(如日志记录)。介绍了事务的实现(基于 AOP
110 0
|
2月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
109 0
|
2月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
144 0
|
2月前
|
监控 架构师 NoSQL
spring 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
spring 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
|
4月前
|
前端开发 Java 数据库连接
Spring核心原理剖析与解说
每个部分都是将一种巨大并且复杂的技术理念传达为更易于使用的接口,而这就是Spring的价值所在,它能让你专注于开发你的应用,而不必从头开始设计每一部分。
165 32
|
4月前
|
Java 开发者 Spring
Spring框架 - 深度揭秘Spring框架的基础架构与工作原理
所以,当你进入这个Spring的世界,看似一片混乱,但细看之下,你会发现这里有个牢固的结构支撑,一切皆有可能。不论你要建设的是一座宏大的城堡,还是个小巧的花园,只要你的工具箱里有Spring,你就能轻松搞定。
187 9
|
5月前
|
安全 前端开发 Java
Spring Boot 项目中触发 Circular View Path 错误的原理与解决方案
在Spring Boot开发中,**Circular View Path**错误常因视图解析与Controller路径重名引发。当视图名称(如`login`)与请求路径相同,Spring MVC无法区分,导致无限循环调用。解决方法包括:1) 明确指定视图路径,避免重名;2) 将视图文件移至子目录;3) 确保Spring Security配置与Controller路径一致。通过合理设定视图和路径,可有效避免该问题,确保系统稳定运行。
342 0
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
371 0