spring_06-13 IOC基础

173 阅读16分钟

6、依赖查找高级

SpringFramework 中的容器最核心的是 BeanFactoryApplicationContext

6.1 BeanFactory/ApplicationContext

ApplicationContext 接口是 BeanFactory 的子接口,它包含后者所有的功能,并拓展了很多特性。

  • 面试题:BeanFactory与ApplicationContext的对比

BeanFactory 接口提供了一个抽象的配置和对象的管理机制ApplicationContextBeanFactory 的子接口,它简化了与 AOP 的整合、消息机制、事件机制,以及对 Web 环境的扩展( WebApplicationContext 等),BeanFactory 是没有这些扩展的。

ApplicationContext 主要扩展了以下功能:

AOP 的支持( `AnnotationAwareAspectJAutoProxyCreator` 作用于 Bean 的初始化之后 )

配置元信息( `BeanDefinition``Environment` 、注解等 )

资源管理( `Resource` 抽象 )

事件驱动机制( `ApplicationEvent``ApplicationListener` )

消息与国际化( `LocaleResolver``Environment` 抽象( SpringFramework 3.1 以后)

6.2 依赖查找高级

ApplicationContext 接口拥有 getBeansOfType()方法,传入一个类型;返回给定类型的所有实现类。

ApplicationContext 中有一个方法叫 getBeansWithAnnotation(),它可以传入一个注解的 class ,返回所有被这个注解标注的 bean 。

ApplicationContext 中另一个方法getBeanDefinitionNames,可以取出 IOC 容器中所有bean的id,并可用String[] 类型接受。

6.3 依赖查找高级——延迟查找

7、注解驱动IOC与组件扫描

7.1 注解驱动IOC

对于 xml 驱动的 IOC 容器中,解析使用的是 ClassPathXmlApplicationContext

注解驱动IOC中,解析使用的是 AnnotationConfigApplicationContext

  • 依赖查找

    配置类( @Configuration标注 )的编写与Bean的注册( 在配置类内使用@Bean 注解 )

    在 xml 中,声明 Bean 是通过 <bean> 标签。

     <bean id="person" class="com.linkedbear.spring.basic_dl.a_quickstart_byname.bean.Person"/>
    

    在配置类中,要想替换掉 <bean> 标签,自然也能想到,它是使用 @Bean 注解。

     @Bean
     public Person person() {
     return new Person();
     }
    

    这种使用方式,可以解释为:向 IOC 容器注册一个类型为 Person ,id 为 person 的 Bean方法的返回值代表注册的类型,方法名代表 Bean 的 id 。当然,也可以直接在 @Bean 注解上显式的声明 Bean 的 id ,只不过在注解驱动的范畴里,它不叫 id 而是叫 name

     @Bean(name = "aaa") // 4.3.3之后可以直接写value
     public Person person() {
     return new Person();
     }
    
  • 依赖注入

    在Bean中直接调用setter方法设置属性

7.2 组件注册与组件扫描

组件产生原因:如果需要注册的组件特别多,手动编写 @Bean 工作量大,Spring中提供 模式注解 来快速注册组件。

  1. 组件注册:

    在类上标注 @Component 注解,即代表该类会被注册到 IOC 容器中作为一个 Bean。

    可以直接用 @Component("aaa") 声明Bean的 id 属性。

  2. 组件扫描的方法

    • 使用注解驱动的IOC:

      • 新建配置类,在配置类上额外标注@ComponentScan("路径"),会自动扫描路径下的所有@Component组件
      • 不用配置类,使用 AnnotationConfigApplicationContext 中传参为 String 类型(也是路径)的构造方法
    • 使用xml驱动的IOC:

      • <context:component-scan base-package="路径"/>
  • 使用@Configuration 注解的类也被视为bean,为什么?

    @Configuration 注解也标注了 @Component 注解,所以它也会被视为 bean ,注册到 IOC 容器。

8、依赖注入-属性注入/SpEL表达式

8.1 setter-属性注入

8.2 构造器式-属性注入

有些 bean 本身没有无参构造器,这个时候就必须使用构造器注入,否则会提示 " No matching constructor found in class 'Person "。分为两种:

  • xml式
 <bean id="person" class="num8_di__bean_component.bean.Person">
     <constructor-arg index="0" value="test-person-byconstructor"/>
     <constructor-arg index="1" value="18"/>
 </bean>
  • 注解式

直接在Bean返回时,调用带参构造器来构造对象。

8.3 注解式-属性注入

之前都是处理使用 @Bean 的方式注册 bean 的情况。那声明式注册好的组件,它们的属性怎么处理呢?

  • 在@Component声明的类中,使用 @value 注解在代码中注入属性
 @Component
 public class Black {
 ​
     @Value("black-value-ann")
     private String name;
     ... ...
 }
  • 外部配置文件- @PropertySource

    • 配置文件编写
    • 在配置类上引入配置文件 : @PropertySource("classpath:basic_di/value/red.properties")
    • 字段上方使用 @Value("${red.name}") 进行属性注入
    • 解析配置类,启动

注意两种解析方式(包扫描+value注释注入 与 配置类+外部配置文件注入)不能混用!

  • SpEL

placeholder

9、依赖注入-自动注入/复杂注入

9.1 自动注入(bean中注入另一个bean)

@Autowired

@Autowired 不仅可以用在 普通 Bean 的属性 上,在 配置类中,注册 @Bean 时也可以标注

  • @Autowired 的三种方式:

    • 在 需要注入的属性(bean中bean) 的声明前标注
    • 构造器注入
    • setter注入
  • 若bean中需要的 bean(如Cat类中的Person类)找不到(例如Perosn缺少 @Component 注解),则需要在 @Autowired 注解上加一个属性:required = false

  • 多个同类型Bean时,保证注入的唯一性?

    比如在使用配置类时,已使用 @Autowired 注解自动注入了一个默认的内嵌Bean,但配置类内又定义了一个同类型的Bean,此时需要使用 @Qualifier@Primary 配合@Autowired 注解使用,否则会报错:

    expected single matching bean but found 2: administrator,master

    • @Qualifier 指定注入的Bean名称

      @Qualifier 注解的使用目标是要注入的 Bean:

       @Bean
       @Autowired
       public Cat cat(@Qualifier("administrator") Person person){
           ... ...
       }
      
    • @Primary 默认Bean

      @Primary 注解的使用目标是被注入的 Bean:

       @Bean
       @Primary
       public Person master() {
           Person master = new Person();
           master.setName("master");
           return master;
       }
      

面试题:@Autowired注入的原理逻辑

先拿属性对应的类型,去 IOC 容器中找 Bean ,如果找到了一个,直接返回;

如果找到多个类型一样的 Bean , 把属性名拿过去,跟这些 Bean 的 id 逐个对比,如果有一个相同的,直接返回;

如果没有相同的 / 有多个相同的,则会抛出 NoUniqueBeanDefinitionException 异常。

  • 多个同类型Bean时,全部注入?

    注入一个用单个对象接收,注入一组对象就用集合来接收:

     @Component
     public class Dog {
         
         @Autowired
         private List<Person> persons;
    

@Resource

@Resource 也是用来属性注入的注解,与 @Autowired 的不同:

@Autowired 是按照类型注入;@Resource 是直接按照属性名 / Bean的名称注入相当于 @Autowired + @Qualifier

@Inject

@AutoWired类似:

     @Inject // 等同于@Autowired
     @Named("admin") // 等同于@Qualifier

面试题:依赖注入的注入方式

注入方式被注入成员是否可变是否依赖IOC框架的API使用场景
构造器注入不可变否(xml、编程式注入不依赖)不可变的固定注入
参数注入不可变否(高版本中注解配置类中的 @Bean 方法参数注入可不标注注解)注解配置类中 @Bean 方法注册 bean
属性注入不可变是(只能通过标注注解来侵入式注入)通常用于不可变的固定注入
setter注入可变否(xml、编程式注入不依赖)可选属性的注入

面试题:自动注入的注解对比

注解注入方式是否支持@Primary来源Bean不存在时处理
@Autowired根据类型注入SpringFramework原生注解可指定required=false来避免注入失败
@Resource根据名称注入JSR250规范容器中不存在指定Bean会抛出异常
@Inject根据类型注入JSR330规范 ( 需要导jar包 )容器中不存在指定Bean会抛出异常

@Qualifier :如果被标注的成员/方法在根据类型注入时发现有多个相同类型的 Bean ,则会根据该注解声明的 name 寻找特定的 bean

@Primary :如果有多个相同类型的 Bean 同时注册到 IOC 容器中,使用 “根据类型注入” 的注解时会注入标注 @Primary 注解的 bean

9.2 复杂类型注入

  • xml复杂注入

    • 数组
    • List / Set
    • Map
    • Properties
  • 注解复杂注入

    和num8中的SpEL一样,未跟着敲

10、依赖注入-回调注入/延迟注入

10.1 回调注入

回调的根源都是 @Aware 接口,下面两个都是其子接口:

  • ApplicationContextAware

    !!示例错误

  • BeanNameAware

    如果当前的 bean 需要依赖它本身的 name ,使用 @Autowired 就不好使了,这个时候就得使用 BeanNameAware 接口来辅助注入当前 bean 的 name 了。

    !!未理解

10.2 延迟注入

【面试题】依赖注入的注入方式-扩展

注入方式被注入成员是否可变是否依赖IOC框架的API注入时机使用场景支持延迟注入
构造器注入不可变否(xml、编程式注入不依赖)对象创建时不可变的固定注入
参数注入不可变是(只能通过标注注解来侵入式注入)对象创建后通常用于不可变的固定注入
setter注入可变否(xml、编程式注入不依赖)对象创建后可选属性的注入

10.3 依赖注入面试集锦

  • 依赖注入的目的和优点?

    首先,依赖注入作为 IOC 的实现方式之一,目的就是解耦,我们不再需要直接去 new 那些依赖的类对象(直接依赖会导致对象的创建机制、初始化过程难以统一控制);而且,如果组件存在多级依赖,依赖注入可以将这些依赖的关系简化,开发者只需要定义好谁依赖谁即可。

    除此之外,依赖注入的另一个特点是依赖对象的可配置:通过 xml 或者注解声明,可以指定和调整组件注入的对象,借助 Java 的多态特性,可以不需要大批量的修改就完成依赖注入的对象替换(面向接口编程与依赖注入配合近乎完美)。

  • 谁把什么注入给谁了?

  • 依赖注入具体是如何注入的?

  • 使用setter注入还是构造器注入?

11、Bean类型与作用域

11.1 Bean类型

Bean一般有两种设计:普通 Bean 、工厂 Bean 。之前都是普通Bean,下面介绍工厂Bean

  • FactoryBean

如果Bean 的创建需要指定一些策略,或者依赖特殊的场景来分别创建,也或者一个对象的创建过程太复杂,使用 xml 或者注解声明也比较复杂。这种情况下,可以借助 FactoryBean接口 来使用工厂方法创建对象。

FactoryBean 本身是一个接口,是一个创建对象的工厂。如果 Bean 实现了 FactoryBean 接口,则它将不会处理实际的业务逻辑,而是由Bean内创建的对象来处理。

接口实现类需要覆写 getObjectgetObjectType 两个方法。接口另有默认方法 default boolean isSingleton(),接口实现类默认是单实例的Bean。

  • FactoryBean创建Bean的时机

    • ApplicationContext 初始化 Bean 的时机默认是容器加载时就已经创建;
    • FactoryBean 本身的加载是伴随 IOC 容器的初始化一起的。而FactoryBean 创建 Bean 的机制是延迟生产,通过getBean()方法调用时才创建。
  • 取 FactoryBean 的本体

    一般情况下,都是类似于之前的演示,取的是 FactoryBean 生产的Bean;某些情况下会需要取 FactoryBean 本体,方法:

     //需要在 Bean 的 id 前面加 “&” 符号, 不然取的还是生产的Bean
     System.out.println(ctx.getBean("&toyFactory"));
    
  • 面试题:BeanFactory与FactoryBean的区别?

    • BeanFactory :SpringFramework 中实现 IOC 的最底层容器(此处的回答可以从两种角度出发:从类的继承结构上看,它是最顶级的接口,也就是最顶层的容器实现;从类的组合结构上看,它则是最深层次的容器,ApplicationContext 在最底层组合了 BeanFactory
    • FactoryBean :创建对象的工厂 Bean ,可以使用它来直接创建一些初始化流程比较复杂的对象

11.2 Bean的作用域

SpringFramework 中内置了 6 种作用域(5.x 版本):

作用域类型概述
singleton一个 IOC 容器中只有一个【默认值】
prototype每次获取创建一个
request一次请求创建一个(仅Web应用可用)
session一个会话创建一个(仅Web应用可用)
application一个 Web 应用创建一个(仅Web应用可用)
websocket一个 WebSocket 会话创建一个(仅Web应用可用)

singleton(单实例-缺省)

SpringFramework 中默认所有的 Bean 都是单实例的,即:一个 IOC 容器中只有一个

img

prototype(原型)

每次对原型 Bean 提出请求时,都会创建一个新的 Bean 实例。

”提出请求“ ,包括任何依赖查找、依赖注入的动作,都算做一次 ”提出请求“ 。

img

12、Bean的实例化方法

前一章介绍了 普通Bean实例化FactoryBean实例化,下面介绍 静态工厂实例化实例工厂实例化

12.3 静态工厂实例化

有静态工厂如下,内置静态方法 getCar

 public class CarStaticFactory {   
     public static Car getCar() {       
         return new Car();
     }
 }
  • 静态工厂的使用多借助 xml 方式:
 <bean id="car1" class="com.linkedbear.spring.bean.c_instantiate.bean.Car"/>
 ​
 <bean id="car2" class="com.linkedbear.spring.bean.c_instantiate.bean.CarStaticFactory" factory-method="getCar"/>

可以看出来,上面的注册方式是普通的 Bean 注册方式; 下面的方式会直接引用静态工厂,并声明要创建对象的工厂方法 factory-method 即可。SpringFramework 会依照这个 xml 的方式,解析出规则并调用静态工厂的方法来创建实际的 Bean 对象。

上面两个bean实际都是Car类型的,而CarStaticFactory本身不会被注册到IOC容器中。 例如打印如下内容,会报NoSuchBeanDefinitionException 异常:

 System.out.println(ctx.getBean(CarInstanceFactory.class));
  • 编程式使用静态工厂

只能使用 注解配置类+编程式 来使用静态工厂

     @Bean
     public Car car2() {
         return CarStaticFactory.getCar();
     }

12.4 实例工厂实例化

  • xml方式:

在工厂类中将 static 字段去掉,xml如下:

 <bean id="carInstanceFatory" class="num12_static_instance_Factory.bean.CarInstanceFactory"/>
 <bean id="car3" factory-bean="carInstanceFatory" factory-method="getCar"/>

此时实例工厂通过注册到 IOC 容器中了。

  • 编程式方式:
     @Bean
     public Car car3(CarInstanceFactory carInstanceFactory) {
         return carInstanceFactory.getCar();
     }

13、Bean生命周期-初始化/销毁

13.1 生命周期的意义

img

  • 一个对象从被创建,到被垃圾回收,可以宏观的划分为 5 个阶段:

    • 创建 / 实例化阶段:此时会调用类的构造方法,产生一个新的对象
    • 初始化阶段:此时对象已经创建好,但还没有被正式使用,可能这里面需要做一些额外的操作(如预初始化数据库的连接池)
    • 运行使用期:此时对象已经完全初始化好,程序正常运行,对象被使用
    • 销毁阶段:此时对象准备被销毁,已不再使用,需要预先的把自身占用的资源等处理好(如关闭、释放数据库连接)
    • 回收阶段:此时对象已经完全没有被引用了,被垃圾回收器回收

把控好生命周期的步骤,可以在恰当的时机处理一些恰当的逻辑。

  • SpringFramework 如何能让我们干预 Bean 的初始化和销毁呢?

    回想一下 ServletServlet 里面有两个方法,分别叫 initdestroy

    咱之前在使用 Servlet 开发时,有自己调过这两个方法吗?肯定没有吧。

    但是这两个方法有真实的被调用过吗?肯定有。这两个方法都是被 Web 容器( Tomcat 等)调用的,用来初始化和销毁 Servlet 的。这种方法的设计思想其实就是 “回调机制” ,它 不是自己设计的,而是由父类 / 接口定义好的,由第三者(框架、容器等)来调用。回调机制跟前面学的那些 Aware 接口的回调注入,在核心思想上其实是一样的。

先体会一种最容易理解的生命周期阶段:初始化和销毁方法单实例 Bean 在 SpringFramework 中常见的 3 种生命周期,见下面三章:

13.2 init-method/destroy-method

适用场景:手动声明注册的Bean

  • 先创建类,写 init、destroy 方法

    • 注意!这些配置的初始化和销毁方法必须具有以下特征:

      • 方法访问权限无限制要求( SpringFramework 底层会反射调用的)
      • 方法无参数(如果真的设置了参数,SpringFramework 也不知道传什么进去)
      • 方法无返回值(返回给 SpringFramework 也没有意义)
      • 可以抛出异常(异常不由自己处理,交予 SpringFramework 可以打断 Bean 的初始化 / 销毁步骤)
  • 再创建配置文件(xml文件) / 配置类

    • 若采用配置类,则使用标注: @Bean(initMethod = "init", destroyMethod = "destroy")
    • 若采用xml文件,则在<bean class=“” 后增加: init-method="init" destroy-method="destroy"
  • 编写启动类

    只需注意接收类型不再使用ApplicationContext,而是实现类本身。因为ApplicationContext 本身没有 close 方法。

有配置类和启动类如下:

 public class InitMethodConfiguration {
     @Bean(initMethod = "init", destroyMethod = "destroy")
     public Dog dog(){
         Dog dog = new Dog();
         dog.setName("wangwang");
         return dog;
     }
 }
 ​
 public class InitMethodApplication {
     public static void main(String[] args) {
         System.out.println("准备初始化IOC容器。。。");
         AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(InitMethodConfiguration.class);
         System.out.println("IOC容器初始化完成。。。");
 ​
         System.out.println("准备销毁IOC容器。。。");
         ctx.close();
         System.out.println("IOC容器销毁完成。。。");
     }
 }

控制台输出:

准备初始化IOC容器。。。 Cat 构造方法执行了。。。 setName 方法执行了。。。 wangwang被初始化了。。。 IOC容器初始化完成。。。

准备销毁IOC容器。。。 wangwang被销毁了。。。 IOC容器销毁完成。。。

可以看出:Bean 的生命周期中,是先对属性赋值,后执行 initMethod 标记的方法

13.3 JSR250规范

适用场景1:使用模式注解的Bean,即使用@Component的类

JSR250 规范中除了有 @Resource 这样的自动注入注解,还有负责生命周期的注解,包括 @PostConstruct@PreDestroy 两个注解,分别对应 init-methoddestroy-method

这两个注解,直接标注在 Bean 内相应的方法上即可。

适用场景2:不使用 @Component 注解时,JSR250规范可与init-method共存

如果不使用 @Component 注解来注册 Bean 而转用 <bean> / @Bean 的方式,那 @PostConstruct@PreDestroy 注解是可以与 init-method / destroy-method 共存的。

并且 JSR250 规范的执行优先级高于 init / destroy

13.4 InitializingBean与DisposableBean

标题中两项为接口,他们都是在 Bean 的初始化和销毁阶段要回调的。

使用方法是使类实现 InitializingBean, DisposableBean,并在类内覆写 afterPropertiesSetdestroy 方法。

同样的,他们既适用于使用 @Component注解 的场景;又可以在不使用 @Component注解 时,与其他两种生命周期共存。

当共存时,执行顺序为: @PostConstructInitializingBean接口init-method

@PreDestroyDisposableBean接口destroy-method

13.5 原型Bean的生命周期

对于原型 Bean 的生命周期,使用的方式跟上面是完全一致的,只是它的触发时机就不像单实例 Bean 那样了。

  • 单实例 Bean 的生命周期 与 IOC 容器相同。容器初始化,单实例 Bean 也跟着初始化(当然不绝对,后面会介绍延迟 Bean );容器销毁,单实例 Bean 也跟着销毁。

  • 原型 Bean 由于每次都是取的时候才产生一个,所以它的生命周期与 IOC 容器无关。

    • 原型 Bean 的创建不随 IOC 的初始化而创建;

    • 原型 Bean 的初始化动作与单实例Bean完全一致;

    • 原型 Bean 在销毁时不处理 destroy-method 标注的方法。

      • 调用 ctx.getBeanFactroy().destroyBean(pen); 发现控制台只打印了@PreDestroy 注解和 DisposableBean 接口的执行内容,没有触发 destroy-mothod 的执行。

13.6 三种控制Bean生命周期方式-总结

init-method & destroy-method@PostConstruct & @PreDestroyInitializingBean & DisposableBean
执行顺序最后最先中间
组件耦合度无侵入(只在 <bean>@Bean 中使用)与 JSR 规范耦合与 SpringFramework 耦合
容器支持xml 、注解原生支持注解原生支持,xml需开启注解驱动xml 、注解原生支持
单实例Bean
原型Bean只支持 init-method