Spring IOC 的原理及源码

IOC 前置知识

为什么要设计模式

首先我想讲一个比较抽象的概念,没有在实际项目上写过代码的小伙伴可能不知道设计模式有什么用。以下通过一个例子描述一下为什么需要设计模式

首先,我有一只笔

public class Pen {

    public void draw() {
        System.out.println("draw");
    }
}

然后有一个画家用了这只笔

public class Painter {
    
    public void usePen() {
        Pen pen = new Pen();
        pen.paint();
    }
}

这段代码在正常情况下没有什么问题,但是,在企业生产环境中往往有很多画家要用这只笔。现在我有10000个类,Painter1,Painter2…Painter10000,他们都调用了这个笔的 paint 方法

这时候,这支笔突然换路径了。10000个画家都找不到这只笔了,如果想要正常使用这只笔,必须将10000个画家的调用路径都修改一遍。如果这么做的话程序员也不用写代码了,天天在这为画家换笔

再比如,这支笔的 paint 方法换成了 write 方法,等等等等,像这种问题我们称之为强依赖(强耦合),10000个画家都依赖与这只笔

我们需要一定的方式去解决强耦合的问题,这种方式被称之为设计模式,设计模式的目的是让程序中的某个类发生改变是其他的类不用发生巨大修改,比如下面的工厂模式

public class PenFactory {
    
    public Pen getPen() {
        return new Pen();
    }
}

我现在有一个工厂去造这支笔,所有的画家都从这个工厂的 getPen 中获得笔,现在笔的路径改了,对我们10000个画家都不会产生什么影响,我们只要在工厂中修改一次笔的路径就行了。这样画家和笔产生了解耦

下面的 IOC 中无时无刻不体现了这种思想

依赖倒置原则

IOC 根据依赖倒置原则演变而来,那依赖倒置原则解决了什么问题呢?

如果是面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本

底层是上层的组件,依赖倒置原则是面向接口编程的一种思路。一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化

依赖倒置原则是设计模式的六大原则之一,简单来说就是:针对接口编程,依赖于抽象而不依赖于具体

IOC(控制反转)与 DI(依赖注入)

为什么要有 IOC 呢?

试想一下,如果一个系统有大量的组件,其生命周期和相互之间的依赖关系如果由组件自身来维护,也就是对各个类的控制权在我们手中,这样的话我们需要手动的去创建各个类并且维持他们的依赖关系,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合

举个例子:现有类 A 依赖于类 B,如果没有 IOC 的话,可能需要在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来。如果 B 还依赖 C 的话,C 可能也需要 new 一个 B,并且这个 B 里面还要 new 一个 A

而解决问题的方案就是 IOC。既然我们手动去操作非常麻烦,我们可以写一段代码去操作这些依赖关系啊。IOC 核心概念就是将你设计好的对象以及他们的依赖关系交给第三方容器控制。IOC 容器和 DI 让上层从被动接受一个底层组件,变成主动选择一个底层组件,这样我们就不需要去思考这些对象的依赖关系,使系统耦合度降低,所以我们得出以下结论:

  • 控制:指的是对象创建(实例化、管理)的权力
  • 反转:控制权交给外部环境(IOC 容器)

而所谓的依赖注入,就是把底层类作为参数传入上层类、将依赖项注入到被依赖项中,实现上层类对下层类的控制。此时就体现出依赖抽象的好处了:我们可以注入任何抽象类的子类,让程序变的更加多样性

IOC 容器

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。这句话在设计模式中体现的淋漓尽致,任何强耦合的对象在他们之间加一次中间件就能解耦,IOC 容器就是负责解耦的容器,你可以把它看成是一个装对象的集合

IOC 容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖

IOC 容器有两个好处:一是容器管理所有的对象,我们不需要去手动创建对象;二是容器管理所有的依赖项,在创建实例的时候不需要了解其中的细节

而 Spring 对 IOC 容器的具体实现就是 BeanFactory

Spring IOC 的大体思路

首先 IOC 容器需要知道这个类的具体位置才能找到对应的对象,而我们就用 xml 或者注解的方式标记这个类的位置

然后 Spring 容器通过反射以获得该类的对象(反射的作用是可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性)

IOC 容器中存放了程序需要的所有的对象,这些对象只会被创建一次(单例),容器将这些对象包装成一个个 bean(带有对象信息以及该对象的结构体),IOC 容器简单来说不过是一个以该对象名称为 key、对应 bean 为 value 的 map。当然,只是简单来说

IOC 容器完成创建这一步之后会根据我们所需要的方式来管理 bean 的依赖

源码

来看看 IOC 容器是如何实现上面的过程的

最常用的 IOC 容器是 ClassPathXmlApplicationContext,我们就从它开始,它的类继承图如下
在这里插入图片描述
不要害怕,比较重要的接口也只有几个

BeanFactory 接口定义了容器的最基本功能,它可以读取类的配置文档,管理类的加载,实例化,维护类之间的依赖关系。实现了此接口的类才能叫容器(实现它的容器实例化时并不会初始化配置文件中定义的类,初始化动作发生在第一次调用时)

ApplicationContext 接口除了提供容器的基本功能外还提供了很多的扩展功能(实现它的容器实例化时就会将配置文件中定义的类初始化)

其他的比较重要的接口:

ApplicationContext 继承了 ListableBeanFactory,这个 Listable 的意思就是,通过这个接口,我们可以获取多个 Bean,大家看源码会发现,最顶层 BeanFactory 接口的方法都是获取单个 Bean 的

ApplicationContext 继承了 HierarchicalBeanFactory,Hierarchical 单词本身已经能说明问题了,也就是说我们可以在应用中起多个 BeanFactory,然后可以将各个 BeanFactory 设置为父子关系

AutowireCapableBeanFactory 就是用来自动装配 Bean 用的,上图并没有显示它。但是不继承不代表不可以使用组合

容器创建

从创建 bean 工厂开始看看它的过程。需要重点关注的点是:refresh 重建工厂,bean 定义,别名处理,bean 覆盖

refresh

	//这一行我们开始创建bean工厂
	ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

	//最终都会调用这个方法来创建工厂
    public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {
        super(parent);
        //读取配置
        this.setConfigLocations(configLocations);
        if (refresh) {
        	//主要方法,调用这个方法创建容器,这个方法可以手动调用
            this.refresh();
        }
        
    }

this.refresh() 其实调用了它父类 AbstractApplicationContext 的 refresh 方法

有没有想过这个方法为什么叫 refresh 而不叫 create?

因为容器建立以后,我们可以通过调用 refresh() 方法重建,refresh() 会将原来的容器销毁,然后再重新执行一次初始化操作,因此命名为 refresh 而不是 init 或者其他名字

    public void refresh() throws BeansException, IllegalStateException {
    	// 该操作是上锁的,保证线程安全
        synchronized(this.startupShutdownMonitor) {
            this.prepareRefresh();
            // 创建bean工厂,如果有一个在运行的工厂,先销毁再创建一个
            ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
            // 这步比较关键,这步完成后,配置文件就会解析成一个个 Bean 定义,注册到 BeanFactory 中
            this.prepareBeanFactory(beanFactory);

            try {
            	//bean的初始化过程
                this.postProcessBeanFactory(beanFactory);
                this.invokeBeanFactoryPostProcessors(beanFactory);
                this.registerBeanPostProcessors(beanFactory);
                this.initMessageSource();
                this.initApplicationEventMulticaster();
                this.onRefresh();
                this.registerListeners();
                //初始化所有单例bean
                this.finishBeanFactoryInitialization(beanFactory);
                this.finishRefresh();
            } catch (BeansException var9) {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
                }

                this.destroyBeans();
                this.cancelRefresh(var9);
                throw var9;
            } finally {
                this.resetCommonCaches();
            }

        }
    }
	  //这一步的重点是以下两个方法
      //设置BeanFactory的两个配置属性:是否允许 Bean 覆盖、是否允许循环引用
      customizeBeanFactory(beanFactory);

      //加载Bean到BeanFactory中
      loadBeanDefinitions(beanFactory);

BeanDefinition

这个接口非常重要,配置文件被Springxml解析后,这里的 BeanDefinition 就是我们所说的 Spring 的 Bean,我们配置的一个个类的信息都会存放在一个个BeanDefinition中

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
    String SCOPE_SINGLETON = "singleton";
    String SCOPE_PROTOTYPE = "prototype";
    int ROLE_APPLICATION = 0;
    .....
    }

注册 Bean

读取完配置之后,会以 beanname 为键,beanfefinition 为值,存放在一个 map 中,这个过程叫注册 bean

   //调用此函数开始注册
   registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

别名处理:

使用 map 保持别名,遇到别名时,先把别名变成 beanName

   String[] aliases = definitionHolder.getAliases();
   if (aliases != null) {
      for (String alias : aliases) {
         registry.registerAlias(beanName, alias);
      }
   }

bean 覆盖:指多个 bean 的名字相同,默认情况下 spring 支持 bean 覆盖,在注册 bean 遇到 bean 覆盖时会这样处理

   BeanDefinition oldBeanDefinition;
   oldBeanDefinition = this.beanDefinitionMap.get(beanName);
   if (oldBeanDefinition != null) {
      if (!isAllowBeanDefinitionOverriding()) {
         // 如果不允许覆盖的话,抛异常
         throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription()...
      }
      else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) {
         // log...用框架定义的 Bean 覆盖用户自定义的 Bean 
      }
      else if (!beanDefinition.equals(oldBeanDefinition)) {
         // log...用新的 Bean 覆盖旧的 Bean
      }
      else {
         // log...用同等的 Bean 覆盖旧的 Bean,这里指的是 equals 方法返回 true 的 Bean
      }
      // 覆盖
      this.beanDefinitionMap.put(beanName, beanDefinition);
   }

完成注册的是这些语句,会有两个集合保存信息

         // 将 BeanDefinition 放到这个 map 中,这个 map 保存了所有的 BeanDefinition
         this.beanDefinitionMap.put(beanName, beanDefinition);
         // 这是个 ArrayList,所以会按照 bean 配置的顺序保存每一个注册的 Bean 的名字
         this.beanDefinitionNames.add(beanName);

bean 创建

需要关注的点是:两次循环依赖、作用域判断、对象创建、实例化时 CGlib 处理,并且有很多步骤都是认证这个 bean 是否合法

getBean

在以上这些结束后,Spring 遍历 map 集合,将所有的 bean 初始化,初始化的过程封装到了 getBean 方法里

getBean 应该是我们获取 bean 使用的方法(你也可以使用这个方法来获取对象),所以这个方法有一层判断,如果在单例池中存在这个 bean,直接返回这个 bean

   final String beanName = transformedBeanName(name);
   Object bean; 
   Object sharedInstance = getSingleton(beanName);
   if (sharedInstance != null && args == null) {
   	   ......
   	   bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
   }

接下来是一大串 bean 认证环节(认证这个 bean 是否合法)

在认证的过程中有一步,需要先初始化依赖的 bean,此时初始化的是 depends-on 中定义的依赖,而不是它调用的依赖,因此必定不支持循环依赖

以下过程,判断该对象依赖是否为空,如果有依赖,使用 isDependent 方法判断是否循环依赖,如果是,抛异常

         String[] dependsOn = mbd.getDependsOn();
         if (dependsOn != null) {
            for (String dep : dependsOn) {
               if (isDependent(beanName, dep)) {
                  throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                        "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
               }
               registerDependentBean(dep, beanName);
               getBean(dep);
            }
         }

然后进行 bean 作用域的判断,不管这个 bean 的作用域是什么(单例原型其他),总是要进入 createBean 方法中,createBean 进行一些认证又会转入 doCreateBean 中

实例化 bean

      instanceWrapper = createBeanInstance(beanName, mbd, args);

进入此方法,又是一段认证过程,然后实例化时分两种情况,存在方法覆写时使用 CGLIB 代理返回一个代理对象,没有就使用 java 反射直接创建一个 bean 对象实例(之间可能还会使用有参构造创建对象或者无参构造创建对象)

随后会进入 DI 的流程,在依赖注入的时候,如果发现需要注入的对象尚未初始化,还需要触发注入对象的初始化动作,同时在注入的时候也会分为按名称注入和按类型注入(除此之外还有构造器注入等方式)

循环依赖

循环依赖指的是当我们有两个类 A 和 B,其中 A 依赖 B,B 又依赖了 A,或者多个类也一样,只要形成了一个环状依赖那就属于循环依赖

如果换成我们来开发,我们会如何解决这个问题呢?其实方法也很简单,大家应该都能想到,那就是当我们把 Bean 初始化之后,在没有注入属性之前,就先缓存起来,这样,就相当于缓存了一个半成品 Bean 来提前暴露出来供注入时使用,这也是 Spring 处理循环依赖的方法

但是以下三种情况无法解决循环依赖问题:

  • 只有 set 注入才能解循环依赖,构造器注入不行,因为 set 在对象被实例化之后才进行 DI,在实例化与 DI 之间有操作空间,如果在构造器是解单例的循环依赖可能就要在 JVM 上操作操作了,构造器注入代码如下:
@Controller
public class FooController {
  
  private final FooService fooService;
  
  @Autowired
  public FooController(FooService fooService) {
      this.fooService = fooService;
  }
}
  • 非单例模式 Bean,因为只有在单例模式下才会对 Bean 进行缓存。如果是原型模式,会走到 AbstractBeanFactory 类中下面的判断,抛出异常。原因很好理解,创建新的 A 时,发现要注入原型字段 B,又创建新的 B 发现要注入原型字段 A。这就套娃了,你猜是先 StackOverflow 还是 OutOfMemory?Spring 怕你不好猜,就先抛出了 BeanCurrentlyInCreationException
if (isPrototypeCurrentlyInCreation(beanName)) {
  throw new BeanCurrentlyInCreationException(beanName);
}
  • 手动设置了 allowCircularReferences=false,则表示不允许循环依赖

来看看 Spring 是怎么处理的,容器会创建三级缓存,表示此 bean 是否正在创建,这些缓存主要用来解决循环依赖。这就是面试中常常被提到的 Spring 如何处理循环依赖的问题

  • 一级缓存:Map<String,Object> singletonObjects,单例池,用于保存实例化、属性赋值(注入)、初始化完成的 bean 实例。这里面的 bean 已经完成了 DI,项目启动完成后获取 bean 实例时从此获取,因此这其实不是缓存而是 IOC 容器
  • 二级缓存:Map<String,Object> earlySingletonObjects,早期曝光对象,用于保存实例化完成的 bean 实例,但是并没有进行 DI
  • 三级缓存:Map<String,ObjectFactory<?>> singletonFactories,早期曝光对象工厂,用于保存 bean 创建工厂,以便于后面扩展有机会创建代理对象

刨除掉所有的异常情况,进入判断这个 bean 是否允许循环依赖的代码,如下:

   boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
         isSingletonCurrentlyInCreation(beanName));
   if (earlySingletonExposure) {
      if (logger.isDebugEnabled()) {
         logger.debug("Eagerly caching bean '" + beanName +
               "' to allow for resolving potential circular references");
      }
      addSingletonFactory(beanName, new ObjectFactory<Object>() {
         @Override
         public Object getObject() throws BeansException {
            return getEarlyBeanReference(beanName, mbd, bean);
         }
      });
   }

具体判断循环依赖的逻辑是这样的:

1,创建 A 实例,实例化的时候把 A 对象工厂放⼊三级缓存,表示 A 开始实例化了,虽然我这个对象还不完整,但是先曝光出来让大家知道。这时候曝光的对象仅仅是构造完成,还没注入属性和初始化,提前曝光的对象被放入 Map<String, ObjectFactory<?>> singletonFactories 缓存中,这里并不是直接将 Bean 放入缓存,而是包装成 ObjectFactory 对象再放入

2,A 注⼊属性时,发现依赖 B,此时 B 还没有被创建出来,所以去实例化 B

3,同样,B 注⼊属性时发现依赖 A,它就会从缓存里找 A 对象。依次从⼀级到三级缓存查询 A,从三级缓存通过对象⼯⼚拿到 A,发现 A 虽然不太完善,但是存在,此时构建 A,把 A 放⼊⼆级缓存,同时删除三级缓存中的 A,B 已经实例化并且初始化完成,把 B 放入⼀级缓存

4,接着 A 继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的 B 对象,A 对象创建也完成,删除⼆级缓存中的 A,同时把 A 放⼊⼀级缓存

那么问题来了,为什么要三级缓存,为什么需要包装?如果只实现这个功能的话二级缓存不是够了吗?二级缓存中存放的,就是未完成的 bean 啊。这么做的好处是什么?二级缓存绰绰有余不是吗

其实三级缓存中放的是生成具体对象的匿名内部类,在一个 bean 被 AOP 的时候,它才会⽣成代理对象,没有的话就会返回普通对象。Spring 不提前创建好代理对象,在出现循环依赖被其他对象注入时,才会根据三级缓存实时生成代理对象或者非代理对象。AOP 的操作是通过 AnnotationAwareAspectJAutoProxyCreator 后置处理器来实现的,也就是 Spring Aop 是 BeanPostProcessor 的一种

知道了这些,我们可以猜测开发者的意图:三级缓存中存放的对象里有生成代理对象的相关代码。spring 开发者不希望循环依赖的处理影响了原本 bean 的生命周期,原本是在 bean 初始化之后,再通过后置处理来创建代理对象;如果只使用二级缓存,那势必要在实例化 bean 后直接创建代理对象放入缓存,这样就仅仅因为循环依赖破坏了原本的流程

如此,处理完循环依赖后,Spring 继续设值,DI。DI 完成之后,这还不是一个完整的 bean,它还需要进行初始化 initializeBean,也就是 bean 生命周期的一部分。Spring 处理问题的思路,简单来说就是缓存,更简单一些,是使用 map,将创建过的对象都保存起来。这也就是为什么有人说,处理该问题的思路是一道简单的算法题

但是,循环依赖的问题还没有结束,就算 Spring 已经处理了循环依赖,在平时的代码中还是可能会出现循环依赖问题(如果设置了不允许支持循环依赖的话)

Error creating bean with name 'trackInfoServiceImpl': Injection of resource dependencies failed; 
nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Errorcreating bean with name 
'salesClueService': Requested bean is currently in creation: Is there an unresolvable circular reference?

这种情况一般出现在系统设计的不好,Service 没有做分层,导致同级业务之间相互调用的问题,这种时候我们一般在方法中使用 getBean 在方法中强行获取某个 service 对象

bean 的作用域

bean 的作用域用于确定哪种类型的 bean 实例应该从 Spring 容器中返回给调用者,从生成方式上来讲只有单例、原型和其他,因为源码是这么写的

1,singleton:单例 bean,获取的每一个 bean 地址都相同,spring 默认使用这个生成方式,当创建起容器时就同时自动创建了一个 bean 的对象,不管你是否使用,他都存在了
2,prototype:原型 bean,每一次使用都会生成一个新对象,创建容器的时候并没有实例化
3,request:每一次 http 请求都会生成一个新对象,这个 bean 只作用在 http 的 request 中
4,session:每一次http请求都会生成一个新对象,这个 bean 只作用在 http 的 session 中

你可以使用 @Scope 来声明作用域,scope 是范围的意思,比如

@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

使用 IOC

如何定义一个 bean

@Component:通用的 bean 定义,如果不知道是哪一层的 bean 就使用这个
@Repository:仓库的意思,对应持久层 dao 层的 bean
@Service:服务层,主要涉及一些复杂的逻辑
@Controller:对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面
@bean:将一个方法的返回值标记为 bean,是对方法使用的注解(可以在没有源码的情况下将第三方代码中的类标记为组件,这种情况下使用 component 就不能解决这种需求了)
@Mapper:设置一个 bean,对应持久层 dao 的注解,同时该 bean 还可以被 mybatis 扫描到

如何使用 bean

使用 Autowired 或者 Resource 来进行 DI,Autowired 根据类型优先注入,Resource 根据名称优先注入

spring 可以注入 List 或者 Map,支持这种基于接口实现类的直接注入,比如如下示例

    @Autowired  
    private List<BeanInterface> list;  
        
    @Autowired  
    private Map<String, BeanInterface> map;  

接口的实现可以使用遍历来让目标数据执行每个实现类的方法

            hotelListProcessors.forEach(item -> item.process(context));

Spring 把 bean 放入了 List 中 那个这个顺序怎么控制呢?

在实现类中加入 @Order(value) 注解即可 ,值越小越先被初始化越先被放入 List

对于 @Autowired 声明的数组、集合类型,spring 并不是根据 beanName 去找容器中对应的 bean,而是把容器中所有类型与集合(数组)中元素类型相同的 bean构造出一个对应集合,注入到目标 bean 中

工厂 bean

Spring 有两种 bean,一种是普通 bean,另外一种是工厂 bean

普通 bean 调用 getBean 方法的时候会返回这个 bean 的对象,而这种工厂 bean 的特性就是在调用 getBean 时不一定返回该 bean 对象,甚至返回值都不一定是该类类型,其返回值是该 FactoryBean 的 getObject 方法所返回的对象。创建出来的对象是否属于单例由 isSingleton 中的返回决定

你可以实现它以重写 FactoryBean

public class FactoryBeanImpl implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        ......
    }
    @Override
    public Class<?> getObjectType() {
        ......
    }
}

那这种 bean 有什么好处呢,它可以基于工厂设计模式创建对象。在某些情况下,实例化 Bean 过程比较复杂,如果按照传统的方式,则需要在 bean 中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring 为此提供了一个 org.springframework.bean.factory.FactoryBean 的工厂类接口,用户可以通过实现该接口定制实例化 Bean 的逻辑

bean 的生命周期

就是 bean 在它生成、使用、销毁过程中可能会发生什么事情,一般这种叫功能扩展

  • Bean 容器找到配置文件中 Spring Bean 的定义
  • Bean 容器利用 Java Reflection API 创建一个 Bean 的实例,此时已经完成 DI
  • 如果涉及到一些属性值,利用 set 方法设置一些属性值
  • 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入 Bean 的名字;如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例;如果实现了其他 *.Aware接口,就调用相应的方法
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法
  • 如果 Bean 实现了InitializingBean接口,执行afterPropertiesSet()方法
  • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法
  • 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy 方法

可以看到上面有很多扩展,当然上面这些不够全面,我们只需要关心 bean 级别与容器级别就行了

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值