一文掌握 Spring 的启动扩展点

在 Java 的世界中,我们知道 Spring 是当下最主流的开发框架,没有之一。而在使用 Dubbo、MyBatis 等开源框架时,我们发现可以采用和 Spring 完全一样的方式来使用它们。

图 1 基于统一的方法使用不同的框架

可能你平时使用时,并没有意识到这一点,但仔细想一想,你会觉得这是一件比较神奇的事情。本来就是不同的框架,怎么能够无缝地集成在一起呢?这就是今天我们要讨论的话题。Spring 为我们内置了一组功能非常强大的启动扩展点。通过这些启动扩展点,可以实现我们想要的集成效果。

系统初始化

我们先来看两个非常常见的 Spring 启动扩展点 InitializingBean 和 DisposableBean。在 Spring 中,这两个扩展点分别作用于 Bean 的初始化和销毁阶段,开发人员可以通过它们实现一些定制化的处理逻辑。

顾名思义,InitializingBean 用于初始化 Bean,接口定义如下:

csharp 复制代码
public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}

我们看到 InitializingBean 接口只有一个方法,即 afterPropertiesSet。从命名上看,这个方法应该作用于属性被设置之后。也就是说,这个方法的初始化会晚于属性的初始化。

实际上,InitializingBean 只是 Spring 初始化时可以采用的其中一个扩展点。与 InitializingBean 类似的一种机制是 InitMethod。我们知道在 Spring 中可以配置 Bean 的 init-method 属性,具体使用方式是这样的:

csharp 复制代码
  <bean class="com.xiaoyiran.springinitialization. TestInitBean" init-method="initMethod"></bean>

这两种 Spring 初始化扩展机制都非常常见,我们在阅读 Dubbo、MyBatis、Spring Cloud 等框架源码时会经常遇到。那么,这里就有一个问题,既然它们都能对初始化过程做一定的控制,执行顺序是怎么样的呢?我们通过一个示例来分析各个机制的执行顺序,示例代码如下所示:

csharp 复制代码
public class TestInitBean implements InitializingBean {
    public TestInitBean (){
        System.out.println("constructMethod");
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("afterPropertiesSet");
    }
    public void initMethod() {
        System.out.println("initMethod");
    }
}

上述示例的执行结果如下所示:

erlang 复制代码
constructMethod
afterPropertiesSet
initMethod

显然,基于以上结果,我们可以得出这三者生效的先后顺序。

图 2 三种初始化方法的执行顺序

结论已经有了,我们简单地对这个结论做一个源码分析。在 Spring 中,我们找到 AbstractAutowireCapableBeanFactory 的 initializeBean 方法,这个方法完成了我们提到的相关操作。在表现形式上,我们对这个方法上做一些简化,可以得到这样的代码结构:

scss 复制代码
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
     //执行 Aware 方法
 invokeAwareMethods(beanName, bean);
    Object wrappedBean = bean;
    //在初始化之前执行 PostProcessor 方法
    wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
     //执行初始化方法
    invokeInitMethods(beanName, wrappedBean, mbd);
    //在初始化之后执行 PostProcessor 方法
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    return wrappedBean;
}

你可以看一下图 3,是这段代码的执行流程。我们先执行 Aware 方法,然后在初始化之前执行 PostProcessor 方法,接着执行初始化方法,最后在初始化之后执行 PostProcessor 方法。

图 3 initializeBean 执行流程

我们来看这里的 invokeInitMethods 方法。从命名上看,这个方法的作用是调用一批初始化方法,我们继续对这个方法的代码结构做一些简化调整,以便我们理解。

java 复制代码
protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable {
         boolean isInitializingBean = (bean instanceof InitializingBean);
         //判断是否实现 InitializingBean 接口
         if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
              //直接调用 afterPropertiesSet 方法
              ((InitializingBean) bean).afterPropertiesSet();
         }
         if (mbd != null) {
              String initMethodName = mbd.getInitMethodName();
              if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&!mbd.isExternallyManagedInitMethod(initMethodName)) {
                    //执行自定义的 init-method
                    invokeCustomInitMethod(beanName, bean, mbd);
               }
         }
  }

可以看到,这里我们首先判断当前 Bean 是不是一个 InitializingBean 接口的实例,如果是就直接调用它的 afterPropertiesSet 方法,我们再根据 Bean 的定义获取它的 init-method 属性,如果设置了这个属性,那么就调用一个 invokeCustomInitMethod 方法。这个方法会找到 init-method 属性并执行指定的方法。因为在代码执行流程上的前后顺序,决定了 afterPropertiesSet 方法在 init-method 之前被触发。

Aware 机制

我们在前面的执行流程图中还看到了一个 invokeAwareMethods 方法。这个 invokeAwareMethods 就涉及了接下来我们要介绍的 Spring 中提供的 Aware 系列扩展机制。

在 Spring 中,Aware 接口是一个空接口,但却有一大批直接或间接的子接口。我们以常见的 ApplicationContextAware 接口为例来说明问题。

java 复制代码
public interface ApplicationContextAware extends Aware {
      void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

ApplicationContextAware 的使用方法非常简单,我们可以直接使用它提供的 setApplicationContext 方法,把传入的 ApplicationContext 暂存起来使用。通过这种方法,我们就可以获取上下文对象 ApplicationContext。一旦获取了 ApplicationContext,那么我们就可以对 Spring 中所有的 Bean 进行操作了。

图 4 ApplicationContextAware 的效果图

事实上,各种 Aware 接口中都只有一个类似 setApplicationContext 的 set 方法。如果一个 Bean 想要获取并使用 Spring 容器中的相关对象,我们就不需要再次执行重复的启动过程,而是可以通过 Aware 接口提供的这些方法直接引入相关对象即可。

Dubbo 基于启动扩展点集成 Spring 原理分析

了解了 Spring 内置的系统初始化方法和 Aware 机制,接下来我们基于具体的开源框架分析一下,怎么和 Spring 完成启动过程的无缝集成。今天,我们讨论的对象是非常经典的分布式服务框架 Dubbo。

Dubbo 服务器端启动过程分析

在 Dubbo 中,负责执行服务器端启动的是 ServiceBean。我们先来看 ServiceBean 这个 Bean 的类定义以及主体代码结构。

typescript 复制代码
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware {
         public void afterPropertiesSet() {}
         ...
         public void setApplicationContext(ApplicationContext applicationContext) {}
         ...
         public void setBeanName(String name) {}
         ...
         public void onApplicationEvent(ApplicationEvent event) {}
         ...
         public void destroy() {}
}

可以看到 ServiceBean 实现了 Spring 的 InitializingBean、DisposableBean ApplicationContextAware 和 ApplicationListener 等接口,重写了 afterPropertiesSet、destroy、setApplicationContext、onApplicationEvent 等方法。这些方法就是 Dubbo 和 Spring 整合的关键,我们在自己实现与 Spring 框架的集成时,通常也会使用这些方法。

我们先来关注一下 ServiceBean 中实现 InitializingBean 接口的 afterPropertiesSet 方法,这个方法非常长,但结构并不复杂,你可以看一下这个方法的结构。

scss 复制代码
public void afterPropertiesSet(){
         getProvider();
         getApplication()
         getModule();
         getRegistries();
         getMonitor();
         getProtocols();
         getPath();
         if (!isDelay()) {
              export();
         }
}

这个代码结构中的 getProvider、getApplication、getModule、getMonitor 等方法的执行逻辑和流程基本一致。以 getProvider 方法为例,Dubbo 首先会从配置文件中读取 dubbo:provide 配置项。显然,Provider、Application 和 Module 在 Dubbo 中应该只能出现一次。通过执行上述的 afterPropertiesSet 方法,相当于在 Dubbo 框架启动的同时,执行了 Spring 容器的初始化过程,并把从这里获取的一组 Dubbo 对象加载到了 Spring 容器中。

而 ServiceBean 也实现了 Spring 的 ApplicationContextAware 接口,所以我们不难想象存在这样的 setApplicationContext 方法。

typescript 复制代码
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        SpringExtensionFactory.addApplicationContext(applicationContext);
}

可以看到,Dubbo 通过这个方法获取了 ApplicationContext,然后通过自己的 SpringExtensionFactory 工厂类把上下文对象保存到了框架内部,方便后续使用。

Dubbo 客户器端启动过程分析

有了 Dubbo 服务器端的基础后,我们再看 Dubbo 的客户端就会变得更简单明了。Dubbo 的客户端启动方法需要参考 ReferenceBean 类。

kotlin 复制代码
public class ReferenceBean<T> extends ReferenceConfig<T> implements FactoryBean, ApplicationContextAware, InitializingBean, DisposableBean {
}

可以看到,ReferenceBean 实现了 Spring 提供的 FactoryBean、ApplicationContextAware、InitializingBean 和 DisposableBean 这四个扩展点。这次,我们先挑最简单的进行介绍。FactoryBean 和 ApplicationContextAware 接口实现非常简单,setApplicationContext 方法只是把传入的 applicationContext 同时保存在 ReferenceBean 内部以及 SpringExtensionFactory 中。

接下来,我们看一下 InitializingBean 接口的 afterPropertiesSet 方法,这个方法同样非常长,但结构也不复杂,而且与 ServiceBean 中的 afterPropertiesSet 方法结构比较对称。这里,我们不再给出这个方法的主体代码结构,而是直接来到末端,找到方法中最核心的代码。

scss 复制代码
  getObject();

这个 getObject 方法,实际上并不是 ReferenceBean 自身的代码,而是实现了 FactoryBean 接口的同名方法。这里的 FactoryBean 接口,我们前面没有介绍过,你可以看一下它的定义:

csharp 复制代码
public interface FactoryBean<T> {
         T getObject() throws Exception;
         Class<?> getObjectType();
         boolean isSingleton();
}

实际上,FactoryBean 是 Spring 框架中非常核心的一个接口,负责从容器中获取具体的 Bean 对象。我们重点来看 ReferenceBean 中的 getObject 方法,这个方法又调用了 ReferenceBean 的父类 ReferenceConfig 中的 get 方法。

csharp 复制代码
public synchronized T get() {
        if (destroyed) {
            throw new IllegalStateException("Already destroyed!");
        }
        if (ref == null) {
            init();
        }
        return ref;
}

很明显,这里的核心应该是 init 方法。这个 init 方法和 ServiceConfig 中的 export 方法一样,做了非常多的准备和校验工作,最终得到了这行代码。

ini 复制代码
  ref = createProxy(map);

顾名思义,用 createProxy 方法来创建代理对象;通过代理对象,客户端访问远程服务就像在调用本地方法一样。到这里,Dubbo 客户端启动过程也介绍完毕。

总结

作为系统启动和初始化相关的常见扩展点,今天我们介绍的 InitializingBean 接口和 Aware 系列接口可以说应用非常广泛。我们发现主流的 Dubbo、MyBatis 等框架都是基于这些扩展性接口完成与 Spring 框架的整合的。如果我们需要实现与 Spring 框架的集成和扩展,这些接口是必须要掌握的内容。建议你在日常开发过程中,多关注这些接口的应用场景和方式,并根据情况集成到自己的代码当中。

相关推荐
小鹭同学_6 分钟前
Java基础 Day26
java·开发语言
蓝婷儿8 分钟前
6个月Python学习计划 Day 10 - 模块与标准库入门
java·python·学习
翻滚吧键盘24 分钟前
IDEA,Spring Boot,类路径
java·spring boot·intellij-idea
c无序1 小时前
【Go-补充】Sync包
开发语言·后端·golang
benpaodeDD2 小时前
IO流1——体系介绍和字节输出流
java
guitarjoy6 小时前
Compose原理 - 整体架构与主流程
java·开发语言
babicu1236 小时前
CSS Day07
java·前端·css
小鸡脚来咯6 小时前
spring IOC控制反转
java·后端·spring
[email protected]8 小时前
ASP.NET Core SignalR的基本使用
后端·asp.net·.netcore
怡人蝶梦8 小时前
Java后端技术栈问题排查实战:Spring Boot启动慢、Redis缓存击穿与Kafka消费堆积
java·jvm·redis·kafka·springboot·prometheus