oracle存储过程如何传递一个bean对象_一文讲清楚 Spring IoC 实现原理和过程

之前发了一个 Spring IoC 的预热篇 「想要理解 Spring IoC,先要知道如何扩展 Spring 自定义 Bean」,有兴趣的可以看一看,如何在 Spring 中扩展自定义的 Bean,比如 标签中有属性 id 和 name,是如何实现的,我们怎么样能够扩展出一个和 功能类似的标签,但是属性却不一样的功能呢?

了解了自定义扩展 Bean 之后,再理解 Spring IoC 的过程相信会更加清楚。

好了,正文开始。

Spring IoC,全称 Inversion of Control - 控制反转,还有一种叫法叫做 DI( Dependency Injection)-依赖注入。也可以说控制反转是最终目的,依赖注入是实现这个目的的具体方法。

什么叫控制反转

为什么叫做控制反转呢。

在传统的模式下,我想要使用另外一个非静态对象的时候会怎么做呢,答案就是 new 一个实例出来。

举个例子,假设有一个 Logger 类,用来输出日志的。定义如下:

public class Logger {    public void log(String text){        System.out.println("log:" + text);    }}复制代码

那现在我要调用这个 log 方法,会怎么做呢。

Logger logger = new Logger();logger.log("日志内容");复制代码

对不对,以上就是一个传统的调用模式。何时 new 这个对象实例是由调用方来控制,或者说由我们开发者自己控制,什么时候用就什么时候 new 一个出来。

而当我们用了 Spring IoC 之后,事情就变得不一样了。简单来看,结果就是开发者不需要关心 new 对象的操作了。还是那个 Logger 类,我们在引入 Spring IoC 之后会如何使用它呢?

public class UserController {    @Autowired    private Logger logger;        public void log(){        logger.log("please write a log");    }    }复制代码

开发者不创建对象,但是要保证对象被正常使用,不可能没有 new 这个动作,这说不通。既然如此,肯定是谁帮我们做了这个操作,那就是 Spring 框架做了,准确的说是 Spring IoC Container 帮我们做了。这样一来,控制权由开发者转变成了第三方框架,这就叫做控制反转。

什么叫依赖注入

依赖注入的主谓宾补充完整,就是将调用者所依赖的类实例对象注入到调用者类。拿前面的那个例子来说,UserController 类就是调用者,它想要调用 Logger 实例化对象出来的 log 方法,logger 作为一个实例化(也就是 new 出来的)对象,就是 UserController 的依赖对象,我们在代码中没有主动使用 new 关键字,那是因为 Spring IoC Container 帮我们做了,这个对于开发者来说透明的操作就叫做注入。

注入的方式有三种:构造方法的注入、setter 的注入和注解注入,前两种方式基本上现在很少有人用了,开发中更多的是采用注解方式,尤其是 Spring Boot 越来越普遍的今天。我们在使用 Spring 框架开发时,一般都用 @Autowired,当然有时也可以用 @Resource

@Autowiredprivate IUserService userService;@Autowiredprivate Logger logger;复制代码

Spring IoC Container

前面说了注入的动作其实是 Spring IoC Container 帮我们做的,那么 Spring IoC Container 究竟是什么呢?

5a27658b62441ce3201f9df22e34d51d.png

本次要讨论的就是上图中的 Core Container 部分,包括 Beans、Core、Context、SpEL 四个部分。

Container 负责实例化,配置和组装Bean,并将其注入到依赖调用者类中。Container 是管理 Spring 项目中 Bean 整个生命周期的管理者,包括 Bean 的创建、注册、存储、获取、销毁等等。

先从一个基础款的例子说起。前面例子中的 @Bean 是用注解的方式实现的,这个稍后再说。既然是基础款,那就逃不掉 xml 的,虽然现在都用 Spring Boot 了,但通过原始的 xml 方式能更加清晰的观察依赖注入的过程,要知道,最早还没有 Spring Boot 的时候,xml 可以说是 Spring 项目的纽带,配置信息都大多数都来自 xml 配置文件。

首先添加一个 xml 格式的 bean 声明文件,假设名称为 application.xml,如果你之前用过 Spring MVC ,那大多数情况下对这种定义会非常熟悉。

<?xml version="1.0" encoding="UTF-8"?>复制代码

通过 元素来声明一个 Bean 对象,并指定 id 和 class,这是 xml 方式声明 bean 对象的标准方式,如果你自从接触 Java 就用 Spring Boot 了,那其实这种方式还是有必要了解一下的。

之后通过通过一个控制台程序来测试一下,调用 Logger 类的 log 方法。

public class IocTest {    public static void main(String[] args){        ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");        Logger logger = (Logger) ac.getBean("logger");        logger.log("hello log");    }}复制代码

ApplicationContext是实现容器的接口类, 其中 ClassPathXmlApplicationContext就是一个 Container 的具体实现,类似的还有 FileSystemXmlApplicationContext,这两个是都是解析 xml 格式配置的容器。我们来看一下 ClassPathXmlApplicationContext 的继承关系图。

6ee9a2639fc0577af266fbe5122e1a7d.png

有没有看起来很复杂的意思,光是到 ApplicationContext 这一层就经过了好几层。

这是我们在控制台中主动调用 ClassPathXmlApplicationContext,一般在我们的项目中是不需要关心 ApplicationContext的,比如我们使用的 Spring Boot 的项目,只需要下面几行就可以了。

@SpringBootApplicationpublic class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }}复制代码

但是,这几行并不代表 Spring Boot 就不做依赖注入了,同样的,内部也会实现 ApplicationContext,具体的实现叫做 AnnotationConfigServletWebServerApplicationContext,下面看一下这个实现类的继承关系图,那更是复杂的很,先不用在乎细节,了解一下就可以了。

92e74ac5b306ee287042dd6767ff964c.png

注入过程分析

继续把上面那段基础款代码拿过来,我们的分析就从它开始。

public class IocTest {    public static void main(String[] args){        ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");        Logger logger = (Logger) ac.getBean("logger");        logger.log("hello log");    }}复制代码

注入过程有好多文章都进行过源码分析,这里就不重点介绍源码了。

简单介绍一下,我们如果只分析 ClassPathXmlApplicationContext 这种简单的容器的话,其实整个注入过程的源码很容易读,不得不说,Spring 的源码写的非常整洁。我们从 ClassPathXmlApplicationContext的构造函数进去,一步步找到 refresh() 方法,然后顺着读下去就能理解 Spring IoC 最基础的过程。以下代码是 refresh 方法的核心方法:

@Override public void refresh() throws BeansException, IllegalStateException {  synchronized (this.startupShutdownMonitor) {   // Prepare this context for refreshing.   prepareRefresh();   // Tell the subclass to refresh the internal bean factory.   ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();   // Prepare the bean factory for use in this context.   prepareBeanFactory(beanFactory);   try {    // Allows post-processing of the bean factory in context subclasses.    postProcessBeanFactory(beanFactory);    // Invoke factory processors registered as beans in the context.    invokeBeanFactoryPostProcessors(beanFactory);    // Register bean processors that intercept bean creation.    registerBeanPostProcessors(beanFactory);    // Initialize message source for this context.    initMessageSource();    // Initialize event multicaster for this context.    initApplicationEventMulticaster();    // Initialize other special beans in specific context subclasses.    onRefresh();    // Check for listener beans and register them.    registerListeners();    // Instantiate all remaining (non-lazy-init) singletons.    finishBeanFactoryInitialization(beanFactory);    // Last step: publish corresponding event.    finishRefresh();   }   catch (BeansException ex) {    }    destroyBeans();    cancelRefresh(ex);    throw ex;   }   finally {    resetCommonCaches();   }  } }复制代码

注释都写的非常清楚,其中核心注入过程其实就在这一行:

ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();复制代码

我把这个核心部分的逻辑调用画了一个泳道图,这个图只列了核心方法,但是已经能够清楚的表示这个过程了。

1c09bf9ce05ad6aab5eca781273c6f6a.png

题外话:关于源码阅读 大部分人都不太能读进去源码,包括我自己,别说这种特别庞大的开源框架,就算是自己新接手的项目也看不进去多少。读源码最关键的就是细节,这儿说的细节不是让你抠细节,恰恰相反,千万不能太抠细节了,谁也不能把一个框架的所有源码一行不落的全摸透,找关键的逻辑关系就可以了,不然的话,很有可能你就被一个细节搞到头疼、懊恼,然后就放弃阅读了。

有的同学一看图或者源码会发现,怎么涉及到这么多的类啊,这调用链可真够长的。没关系,你就把它们当做一个整体就可以了(理解成发生在一个类中的调用),通过前面的类关系图就看出来了,继承关系很复杂,各种继承、实现,所以到最后调用链变得很繁杂。

简单概括

那么简单来概括一下注入的核心其实就是解析 xml 文件的内容,找到 元素,然后经过一系列加工,最后把这些加工后的对象存到一个公共空间,供调用者获取使用。

而至于使用注解方式的 bean,比如使用 @Bean、@Service、@Component 等注解的,只是解析这一步不一样而已,剩下的操作基本都一致。

所以说,我们只要把这里面的几个核心问题搞清楚就可以了。

BeanFactory 和 ApplicationContext 的关系

上面的那行核心代码,最后返回的是一个 ConfigurableListableBeanFactory对象,而且后面多个方法都用这个返回的 beanFactory 做为参数。

BeanFactory 是一个接口,ApplicationContext 也是一个接口,而且,BeanFactory 是 ApplicationContext的父接口,有说 BeanFactory才是 Spring IoC 的容器。其实早期的时候只有 BeanFactory,那时候它确实是 Spring IoC 容器,后来由于版本升级扩展更多功能,所以加入了 ApplicationContext。它们俩最大的区别在于,ApplicationContext 初始化时就实例化所有 Bean,而BeanFactory 用到时再实例化所用 Bean,所以早期版本的 Spring 默认是采用懒加载的方式,而新版本默认是在初始化时就实例化所有 Bean,所以 Spring 的启动过程不是那么快,这是其中的一个原因。

BeanDefinition 保存在哪儿

上面概括里提到保存到一个公共空间,那这个公共空间在哪儿呢?其实是一个 Map,而且是一个 ConcurrentHashMap ,为了保证并发安全。它的声明如下,在 DefaultListableBeanFactory 中。

private final Map beanDefinitionMap = new ConcurrentHashMap<>(256)复制代码

其中 beanName 作为 key,也就是例子中的 logger,value 是 BeanDefinition 类型,BeanDefinition 用来描述一个 Bean 的定义,我们在 xml 文件中定义的 元素的属性都在其中,还包括其他的一些必要属性。

向 beanDefinitionMap 中添加元素,叫做 Bean 的注册,只有被注册过的 Bean 才能被使用。

Bean 实例保存在哪儿

另外,还有一个 Map 叫做 singletonObjects,其声明如下:

/** Cache of singleton objects: bean name to bean instance. */private final Map singletonObjects = new ConcurrentHashMap<>(256);复制代码

在 refresh() 过程中,还会将 Bean 存到这里一份,这个存储过程发生在 finishBeanFactoryInitialization(beanFactory) 方法内,它的作用是将非 lazy-init 的 Bean 放到singletonObjects 中。

除了存我们定义的 Bean,还包括几个系统 Bean。

30be1d33d6672d23557a6f189855ee48.png

例如我们在代码中这样调用:

ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");StandardEnvironment env = (StandardEnvironment) ac.getBean("environment");复制代码

使用已注册的 Bean

在这个例子中,我们是通过 ApplicationContext 的 getBean() 方法显示的获取已注册的 Bean。前面说了我们定义的 Bean 除了放到 beanDefinitionMap,还在 singletonObjects 中存了一份,singletonObjects 中的就是一个缓存,当我们调用 getBean 方法的时候,会先到其中去获取。如果没找到(对于那些主动设置 lazy-init 的 Bean 来说),再去 beanDefinitionMap 获取,并且加入到 singletonObjects 中。

获取 Bean 的调用流程图如下

e3855143c863fb8049c110b34bfbe989.png

以下是 lazy-init 方式设置的 Bean 的例子。

复制代码

如果不设置的话,默认都是在初始化的时候注册。

注解的方式

现在已经很少项目用 xml 这种配置方式了,基本上都是 Spring Boot,就算不用,也是在 Spring MVC 中用注解的方式注册、使用 Bean 了。其实整个过程都是类似的,只不过注册和获取的时候多了注解的参与。Srping 中 BeanFactory和ApplicationContext都是接口,除此之外,还有很多的抽象类,使得我们可以灵活的定制属于自己的注册和调用流程,可以认为注解方式就是其中的一种定制。只要找到时机解析好对应的注解标示就可以了。

但是看 Spring Boot 的注册和调用过程没有 xml 方式的顺畅,这都是因为注解的特性决定的。注解用起来简单、方便,好处多多。但同时,注解会割裂传统的流程,传统流程都是一步一步主动调用,只要顺着代码往下看就可以了,而注解的方式会造成这个过程连不起来,所以读起来需要额外的一些方法。

Spring Boot 中的 IoC 过程,我们下次有机会再说。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值