[Java复习] Spring IoC

流过昼夜 提交于 2020-11-30 00:41:06

1. Spring bean的生命周期?

1. 对Bean进行实例化(相当于new)

  • 对于BeanFactory

 当客户向容器请求一个尚未初始化的bean(或初始化bean需要注入另外一个尚未初始化的依赖bean)时,容器会调用createBean进行实例化。            

例如XmlBeanFactory通过Resource装载Spring配置信息启动Ioc容器,然后通过BeanFactory.getBean获取Bean。注意是,启动IoC容器时,不会初始化Bean,初始化是在getBean调用时。

 容器通过获取BeanDefinition对象中的信息进行实例化,这步只是实例化,并没有依赖注入。

实例化对象被包装在BeanWrapper对象中,BeanWrapper提供了设置对象属性的接口。

BeanWrapper是对Bean的包装,

一般情况下是在spring ioc内部使用,提供了访问bean的属性值、属性编辑器注册、类型转换等功能,方便ioc容器用统一的方式来访问bean的属性.

  • 对于ApplicationContext

                当容器启动结束后,便已经实例化所有的bean。

 

2. 设置对象属性(依赖注入)

        实例化后的对象被封装在BeanWrapper对象中,并且此时对象仍然是一个原生的状态,并没有进行依赖注入。

紧接着,Spring根据BeanDefinition中的信息进行依赖注入,并且通过BeanWrapper提供的设置属性的接口完成依赖注入。

 

3. 注入Aware接口

       Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给bean。

      3.1. 如果Bean实现了BeanNameAware接口,Spring将Bean的ID传递给setBeanName()方法(实现BeanNameAware主要是为了通过Bean的引用来获得Bean的ID,一般业务中是很少有用到Bean的ID的)。

    3.2. 如果Bean实现了BeanFactoryAware接口,Spring将调用setBeanDactory(BeanFactory bf)方法并把BeanFactory容器实例作为参数传入。(实现BeanFactoryAware 主要目的是为了获取Spring容器,如Bean通过Spring容器发布事件等)。

   3.3. 如果Bean实现了ApplicationContextAware接口,Spring容器将调用setApplicationContext(ApplicationContext ctx)方法,把应用上下文作为参数传入.

(作用与BeanFactory类似都是为了获取Spring容器,不同的是Spring容器在调用setApplicationContext方法时会把它自己作为setApplicationContext 的参数传入,而Spring容器在调用setBeanDactory前需要程序员自己指定(注入)setBeanDactory里的参数BeanFactory )。

4. BeanPostProcessor

   当经过上述步骤,bean对象已经被正确构造,如果想要对象被使用之前再进行一些自定义处理,就可以通过实现 BeanPostProcessor接口

(该接口有2个函数,postProcessBeforeInitialzation,postProcessAfterInitialzation)

   如果Bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessBeforeInitialization(前置处理)方法

(作用是在Bean实例创建成功后对进行增强处理,如对Bean进行修改,增加某个功能)

  这个函数会先于InitializatingBean执行,因此称为前置处理。

  postProcessAfterInitialzation(),这个函数会在InitializatingBean完成后执行,因此称为后置处理。

 

5. InitializatingBean与init-method

     当BeanPostProcessor的前置处理完成后就会进入本阶段,InitializatingBean接口只有一个函数,afterPropertiesSet()。

如果Bean实现了InitializatingBean接口,Spring将调用它们的afterPropertiesSet方法,作用与在配置文件中对Bean使用init-method声明初始化的作用一样,都是在Bean的全部属性设置成功后执行的初始化方法。

由于该函数并不会把当前bean对象传进来,因此在这一步没办法处理对象本身,只能增加一些额外的逻辑。 若要使用它,我们需要让bean实现该接口,并把要增加的逻辑写在该函数中。然后Spring会在前置处理完成后检测当前bean是否实现了该接口,并执行afterPropertiesSet函数。

经过以上的工作后,Bean将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁。

 

6. DisposableBean和destroy-method

  如果Bean实现了DispostbleBean接口,Spring将调用它的destory方法,作用与在配置文件中对Bean使用destory-method属性的作用一样,

  都是在Bean实例销毁前执行的方法。

 

2. IoC容器是如何解决循环依赖的?

  

 

 

以两个类A和B为例进行讲解,A和B中各自都以对方为自己的全局属性。Spring实例化bean是通过ApplicationContext.getBean()方法来进行的。

  • 首先Spring尝试通过ApplicationContext.getBean()方法获取A对象的实例,由于Spring容器中还没有A对象实例,因而其会创建一个A对象。
  • 然后发现其依赖了B对象,因而会尝试递归的通过ApplicationContext.getBean()方法获取B对象的实例。
  • 但是Spring容器中此时也没有B对象的实例,因而其还是会先创建一个B对象的实例。
  • 注意这个时间点,此时A对象和B对象都已经创建了,并且保存在Spring容器中了,只不过A对象的属性b和B对象的属性a都还没有设置进去。
  • 在前面Spring创建B对象之后,Spring发现B对象依赖了属性A,因而还是会尝试递归的调用ApplicationContext.getBean()方法获取A对象的实例。
  • 因为Spring中已经有一个A对象的实例,虽然只是半成品(其属性b还未初始化),但其也还是目标bean,因而会将该A对象的实例返回。
  • 此时,B对象的属性a就设置进去了,然后还是ApplicationContext.getBean()方法递归的返回,也就是将B对象的实例返回,此时就会将该实例设置到A对象的属性b中。

  从源码角度分析:

 

  Spring是使用ApplicationContext的属性SetsingletonsCurrentlyInCreation来保存的,而半成品的A对象则是通过MapsingletonFactories来保存的
  这里的ObjectFactory是一个工厂对象,可通过调用其getObject()方法来获取目标对象。在AbstractBeanFactory.doGetBean()方法中获取对象的方法如下:

protected  T doGetBean(final String name, @Nullable final Class requiredType,
    @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
  
  // 尝试通过bean名称获取目标bean对象,比如这里的A对象
  Object sharedInstance = getSingleton(beanName);
  // 我们这里的目标对象都是单例的
  if (mbd.isSingleton()) {
    
    // 这里就尝试创建目标对象,第二个参数传的就是一个ObjectFactory类型的对象,这里是使用Java8的lamada
    // 表达式书写的,只要上面的getSingleton()方法返回值为空,则会调用这里的getSingleton()方法来创建
    // 目标对象
    sharedInstance = getSingleton(beanName, () -> {
      try {
        // 尝试创建目标对象
        return createBean(beanName, mbd, args);
      } catch (BeansException ex) {
        throw ex;
      }
    });
  }
  return (T) bean;
}

  这里的doGetBean()方法是非常关键的一个方法(中间省略了其他代码),上面也主要有两个步骤:
    第一个步骤的getSingleton()方法的作用是尝试从缓存中获取目标对象,如果没有获取到,则尝试获取半成品的目标对象;

    如果第一个步骤没有获取到目标对象的实例,那么就进入第二个步骤。
    第二个步骤的getSingleton()方法的作用是尝试创建目标对象,并且为该对象注入其所依赖的属性。

 

 

  前面图中已经标明,在整个过程中会调用三次doGetBean()方法:

  1.     第一次调用的时候会尝试获取A对象实例,此时走的是第一个getSingleton()方法,由于没有已经创建的A对象的成品或半成品,因而这里得到的是null。
  2.     然后就会调用第二个getSingleton()方法,创建A对象的实例,然后递归的调用doGetBean()方法,尝试获取B对象的实例以注入到A对象中。
  3.     此时由于Spring容器中也没有B对象的成品或半成品,因而还是会走到第二个getSingleton()方法,在该方法中创建B对象的实例。
  4.     创建完成之后,尝试获取其所依赖的A的实例作为其属性,因而还是会递归的调用doGetBean()方法。
  5.     此时需要注意的是,在前面由于已经有了一个半成品的A对象的实例,因而这个时候,再尝试获取A对象的实例的时候,会走第一个getSingleton()方法。
  6.     在该方法中会得到一个半成品的A对象的实例,然后将该实例返回,并且将其注入到B对象的属性a中,此时B对象实例化完成。
  7.     然后,将实例化完成的B对象递归的返回,此时就会将该实例注入到A对象中,这样就得到了一个成品的A对象。

 

 

  第一个getSingleton()方法:

 

 

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  
  // 尝试从缓存中获取成品的目标对象,如果存在,则直接返回
  Object singletonObject = this.singletonObjects.get(beanName);
  
  // 如果缓存中不存在目标对象,则判断当前对象是否已经处于创建过程中,在前面的讲解中,第一次尝试获取A对象
  // 的实例之后,就会将A对象标记为正在创建中,因而最后再尝试获取A对象的时候,这里的if判断就会为true
  if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
    
    synchronized (this.singletonObjects) {
      singletonObject = this.earlySingletonObjects.get(beanName);
      if (singletonObject == null && allowEarlyReference) {
        
        // 这里的singletonFactories是一个Map,其key是bean的名称,而值是一个ObjectFactory类型的
        // 对象,这里对于A和B而言,调用图其getObject()方法返回的就是A和B对象的实例,无论是否是半成品
        ObjectFactory singletonFactory = this.singletonFactories.get(beanName);
        if (singletonFactory != null) {
          
          // 获取目标对象的实例
          singletonObject = singletonFactory.getObject();
          this.earlySingletonObjects.put(beanName, singletonObject);
          this.singletonFactories.remove(beanName);
        }
      }
    }
  }
  return singletonObject;
}

 

  这里存在一个问题就是A的半成品实例是如何实例化的?然后是如何将其封装为一个ObjectFactory类型的对象,并且将其放到上面的singletonFactories属性中的?
  这主要是在前面的第二个getSingleton()方法中,其最终会通过其传入的第二个参数,从而调用createBean()方法,该方法的最终调用是委托给了另一个doCreateBean()方法:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
  throws BeanCreationException {
  // 实例化当前尝试获取的bean对象,比如A对象和B对象都是在这里实例化的
  BeanWrapper instanceWrapper = null;
  if (mbd.isSingleton()) {
    instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
  }
  if (instanceWrapper == null) {
    instanceWrapper = createBeanInstance(beanName, mbd, args);
  }
  // 判断Spring是否配置了支持提前暴露目标bean,也就是是否支持提前暴露半成品的bean
  boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences 
    && isSingletonCurrentlyInCreation(beanName));
  if (earlySingletonExposure) {
    
    // 如果支持,这里就会将当前生成的半成品的bean放到singletonFactories中,这个singletonFactories
    // 就是前面第一个getSingleton()方法中所使用到的singletonFactories属性,也就是说,这里就是
    // 封装半成品的bean的地方。而这里的getEarlyBeanReference()本质上是直接将放入的第三个参数,也就是
    // 目标bean直接返回
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  }
  try {
    // 在初始化实例之后,这里就是判断当前bean是否依赖了其他的bean,如果依赖了,
    // 就会递归的调用getBean()方法尝试获取目标bean
    populateBean(beanName, mbd, instanceWrapper);
  } catch (Throwable ex) {
    // 省略...
  }
  return exposedObject;
}

 Spring 为了解决单例的循环依赖问题,使用了 三级缓存,递归调用时发现 Bean 还在创建中即为循环依赖。

 

/** 一级缓存:用于存放完全初始化好的 bean **/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** 二级缓存:存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

/** 三级级缓存:存放 bean 工厂对象,用于解决循环依赖 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

/**
bean 的获取过程:先从一级获取,失败再从二级、三级里面获取。
创建中状态:是指对象已经 new 出来了但是所有的属性均为 null 等待被 init
*/

 

  Spring整个解决循环依赖问题的实现思路总结:
    1. Spring是通过递归的方式获取目标bean及其所依赖的bean的。
    2. Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。

 

 

3. Spring的哪种循环依赖是不能被解决的?

    只能解决Setter注入循环依赖。 有参构造器不能解决循环依赖。

    Spring解决循环依赖主要是依赖三级缓存,但是的在调用构造方法之前还未将其放入三级缓存之中,

    因此后续的依赖调用构造方法的时候并不能从三级缓存中获取到依赖的Bean,因此不能解决。

 

 

4. 为什么原型模式(prototype)不能解决循环依赖?

    多实例Bean是每次调用一次getBean都会执行一次构造方法并且未属性赋值,根本没有三级缓存(Spring不进行缓存),因此不能解决循环依赖。

     Spring容器先创建单例A,A依赖B,然后将A放在“当前创建Bean池”中,此时创建B, B依赖C ,然后将B放在“当前创建Bean池”中,此时创建C,C又依赖A,

     但是此时A已经不在池中,所以会报错,因为在池中的Bean都是未初始化完的,所以会依赖错误 。
(初始化完的Bean会从池中移除)
  执行结果报错信息为:

   Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: 
   Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

精简: 有参构造器,创建实例A时,要在缓存中找不到依赖实例B,再去创建A时抛异常。

 

5.为什么用setter注入方式(单例模式)不报错?

    Spring先是用构造实例化Bean对象,此时Spring会将这个实例化结束的对象放到一个Map(缓存)中,并且Spring提供了获取这个未设置属性的实例化对象引用的方法。   结合我们的实例来看,当Spring实例化了A、B、C后,紧接着会去设置对象的属性,此时A依赖B,就会去Map中取出存在里面的单例B对象,以此类推,不会出来循环的问题。

 

6.Spring里FactoryBean是什么?

    当我们不想或不能使用Spring帮我们自动构建对象的时候,我们可以通过创建一个FactoryBean接口的实例,来自己实现对象的创建。

public interface FactoryBean<T> {
    T getObject() throws Exception;
    Class<T> getObjectType();
    boolean isSingleton();
}

    getObject()方法: 会返回该FactoryBean“生产”的对象实例,我们需要实现该方法以给出自己的对象实例化逻辑;

    getObjectType()方法: 仅返回getObject()方法所返回的对象的类型,如果预先无法确定,则返回null;

    isSingleton()方法:返回结果用于表明,工厂方法getObject()所“生产”的对象是否要以singleton形式存在于容器中。如果以singleton形式存在,则返回true,否则返回false;

 

7. BeanFactory和FactoryBean的区别?

    BeanFactory是IOC容器的核心接口。比如XMLBeanFactory(被废弃)是一种典型的BeanFactory。原始的BeanFactory无法支持AOP,Web应用等。

    ApplicationContext是BeanFactory的派生,包含BeanFactory的所有功能。

    BeanFactory是接口,提供了OC容器最基本的形式,给具体的IOC容器的实现提供了规范。BeanFactory是IOC容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。具体实现有如DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等。

    BeanFactory提供了几种方法:

//  判断工厂中是否包含给定名称的bean定义,若有则返回true。
 boolean containsBean(String beanName);

// 返回给定名称注册的bean实例。根据bean的配置情况,如果是singleton模式将返回一个共享实例,否则将返回一个新建的实例,如果没有找到指定bean,该方法可能会抛出异常。
Object getBean(String);

// 判断给定名称的bean定义是否为单例模式。
boolean isSingleton(String);

    FactoryBean是Spring提供的一个工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。为IOC容器中Bean的实现提供了更加灵活的方式,FactoryBean在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式(如果想了解装饰模式参考:修饰者模式(装饰者模式,Decoration) 我们可以在getObject()方法中灵活配置。其实在Spring源码中有很多FactoryBean的实现类。

    以Bean结尾,表示它是一个Bean,不同于普通Bean的是:它是实现了FactoryBean<T>接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取。

   区别:

    BeanFactory是负责生产和管理bean的一个工厂,也就是IOC容器或对象工厂。FactoryBean是个特殊的Bean在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。

 

8. ObjectFactory是什么?

@FunctionalInterface
public interface ObjectFactory<T> {
    /**
     * Return an instance (possibly shared or independent)
     * of the object managed by this factory.
     * @return the resulting instance
     * @throws BeansException in case of creation errors
     */
    T getObject() throws BeansException;
}

    ObjectFactory就是一个普通的工厂对象接口,对于spring在doGetBean处的使用时,在于创建对象的过程由框架通过ObjectFactory定义,而创建的时机交给拓展接口Scope,除此之外ObjectFactory就是一个普通的接口。

 

9. Spring底层怎么实例化对象的?

    Spring当中提供了两种实例化方案: BeanUtils 和 Cglib 方式。BeanUtils实现机制是通过Java的反射机制,Cglib是一个第三方类库采用的是一种字节码加强方式机制。

    SimpleInstantiationStrategy的instantiate方法中判断bd.hasMethodOverrides()如果没有者直接使用反射,否则需要CGLIB 实例化对象来实例化bean。

 

10. Spring IoC里的对象是线程安全的吗?

    Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。Spring管理的大多数对象其实都是一些无状态的对象,不会因为多线程而导致状态被破坏的对象很适合Spring的默认scope,每个单例的无状态对象都是线程安全的。

    无状态对象包括我们经常使用的DO、DTO、VO这些只作为数据的实体模型的贫血对象,还有Service、DAO和Controller,这些对象并没有自己的状态,它们只是用来执行某些操作的。

 

11. Spring的三种配置方式?

  1. XML配置

  2. 注解配置

       @Service

       @Component

       @Repository

       @Controller

  3. Java配置

       @Configuration 用于配置类,相当于spring的配置文件XML

       @Bean 用到方法上,表示当前方法的返回值是一个bean

  注解配置和XML配置的适用场合?

  • 如果 Bean 的依赖关系是固定的,(如 Service 使用了哪几个 DAO 类),这种配置信息不会在部署时发生调整,那么注释配置优于 XML 配置;反之如果这种依赖关系会在部署时发生调整,XML 配置显然又优于注释配置,因为注释是对 Java 源代码的调整,您需要重新改写源代码并重新编译才可以实施调整。
  • 如果 Bean 不是自己编写的类(如 JdbcTemplate、SessionFactoryBean 等),注释配置将无法实施,此时 XML 配置是唯一可用的方式。
  • 注释配置往往是类级别的,而 XML 配置则可以表现得更加灵活。比如相比于 @Transaction 事务注释,使用 aop/tx 命名空间的事务配置更加灵活和简单。

   注解配置和Java配置区别?

       如果使用注解,需要在类上面进行注解,如控制层 @Controller, 服务层@Service, 数据持久化层@ Repository。

      如果使用Java配置,不需要在类上面写注解,直接在配置类(类上加@Configuration)进行申明(方式上面加@Bean)。

   两种方式分情况使用:

  • 涉及到全局配置的,例如数据库相关配置、MVC相关配置等,就用JAVA配置的方式
  • 涉及到业务配置的,就使用注解方式

 

12. Spring注入对象的三种方式?

  • 隐式的bean扫描发现机制和自动装配
  • 在Java中进行显示配置
  • 在XML中进行显示配置

  1. Spring从两个角度实现自动化装配:组件扫描和自动装配。

        方法一,当对一个类标注@Component注解时,表明该类会作为组件类,spring将为这个类创建bean。

        方法二,在新建一个配置类,类中可以什么不用写,在配置类上加上@ComponentScan注解,spring会自动扫描改配置类所在的包。

  2.  通过Java代码装配bean。

       如果需要使用第三方库的类,就不能使用第一种方法。只能采用显示装配,Java代码或者XML。

       使用Java代码装配,需新建一个配置类XXXConfig,里面都是配置所需要的bean。如restTemplate。

  3. 第三种使用XML。

 

13. Spring容器自动装配方法?

   有五种自动装配的方式,可以用来指导Spring容器用自动装配方式来进行依赖注入。

  • no:默认的方式是不进行自动装配,通过显式设置ref 属性来进行装配。
  • byName:通过参数名 自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byname,之后容器试图匹配、装配和该bean的属性具有相同名字的bean。
  • byType::通过参数类型自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byType,之后容器试图匹配、装配和该bean的属性具有相同类型的bean。如果有多个bean符合条件,则抛出错误。
  • constructor:这个方式类似于byType, 但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。
  • autodetect:首先尝试使用constructor来自动装配,如果无法工作,则使用byType方式。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!