依赖注入

传统的Java开发中,程序员负责在代码中用new创建对象以及维护对象之间的关系。使用Spring框架后,Spring可以自动进行对象创建和对象关系的维护,也就是所谓的依赖注入。

image-20210310104042454

从Spring的总体模块组成来看,依赖注入是Spring核心容器模块所做的事情。

要想让Spring自动完成依赖注入,有三种配置方法:

  1. 自动化配置 在需要使用的类中用@Autowired进行配置
  2. JavaConfig 在专门的Config类中配置
  3. XML配置 在专门的XML文件中进行配置。也是Spring最开始的配置方法

方案一 自动化配置

例子类图:

image-20210310104853326

CDPlayer可以播放CD的一个例子。

如果要实现自动化配置,需要两步:

  • 告诉Spring需要创建哪些对象。通过组件扫描的方式告诉Spring。
  • 自动装配:构建对象和对象之间的关系。

组件扫描

在需要创建对象的类上方加上@Component注解。

还需要告诉Spring,去哪些类或哪些包下去扫描这样的类。这一步通过配置类(通过@Configuration标明配置类)来实现。在配置类上通过@ComponentScan告诉Spring要在当前配置类所在的包和子包下扫描有@Component的类。

需要注意的是,凡是涉及到自动注入的类,都需要@Component进行声明,交给Spring容器进行管理,否则Spring还是无法进行对象关系的管理

自动装配

三种自动装配的方式。

构造方法注入。
  1. @Autowired指定一个构造方法时,这个组件被Spring实例化时就会调用该构造方法。如果构造方法的参数中需要传入其他对象,Spring便会从容器中找一个符合声明类型的对象传入。
  2. Autowired指定的构造方法至少有一个参数,并且这个参数指定的类型需要在容器中能够找到,否则会报错。
  3. 如果不用@Autowired指定,则Spring会使用默认构造函数进行对象构造;这种情况下假如不存在默认构造函数,Spring也会报错。
  4. 不能同时给多个构造函数声明Autowired,否则Spring不知道调用哪个构造方法。
属性注入。
  1. @Autowired指定一个Set方法,Spring可以自动调用这个方法,传入参数中指定类型的Bean
  2. 本质上,对于任何一个非构造函数的方法都可以加上@Autowired,只不过如果参数中有不存在于容器中的对象,就会报错。
  3. Spring依照代码编写前后顺序,依次调用带有Autowired标记的方法。
@Component
public class CDPlayer implements MediaPlayer {
    private CompactDisc cd;
    private Power power;

    // 调用顺序 2 验证了多个参数也可以注入
    @Autowired
    public void setCd(CompactDisc cd,Power power) {
        this.cd = cd;
        this.power=power;
    }

    // 调用顺序 3 重复注入,可以覆盖之前的注入
    @Autowired
    public void lalala(CompactDisc cd){
        this.cd=new CompactDisc() {
            @Override
            public void play() {
                System.out.println("hacked");
            }
        };
    }

    // 调用顺序:1 构造函数首先被调用 cd 为容器内CompactDisc的一个bean对象
    @Autowired
    public CDPlayer(CompactDisc cd) {
        this.cd = cd;
    }

    public void play() {
        power.on();
        cd.play();
    }
}
// 输出:
//Power on
//hacked

其实构造函数和Set方法(属性注入)的注入方法,Spring只负责传入参数,至于方法内部怎么做Spring是管不着的。对于方法上的Autowired,Spring只会自动调用这个方法,传入相应的参数而已

使用习惯上,如果两个对象关系密切就使用构造函数注入,否则使用属性注入。

直接在成员变量上加@Autowired

这种情况,Spring直接给这个成员变量初始化指向容器内对象。

其他注意点

ComponentScan注解可以带参数,指定要搜的包名、类名。如@ComponentScan(basePackages={“aaa”,”bbb”}) 或者 @ComponentScan({CDPlayer.class})。也可以通过@ComponentScan(basePackageClasses = MyInter.class)来指定检查哪个类所在的包。使用一个空的接口Maker Interface可以避免后期修改。

在测试类中,通过@ContextConfiguration(class= CDPlayerConfig.class)可以指定当前类使用的Spring上下文,然后就可以注入和使用Spring容器中的对象了。

@Autowired(required=false)声明时,即使容器内并没有要装配的对象也不会报错(但是引用可能为null)

方案二 JavaConfig配置类

配置类

通过配置类(配置类需要用@Configuration注解进行声明),用@Bean来声明需要在容器中创建的对象。声明的需要是一个方法,返回值就是一个需要交给Spring容器管理的对象。Spring容器会自动调用这些方法,并得到返回值做为Bean对象在容器中进行管理。

在调用这些方法的时候,如果需要传入参数,Spring便会自动试图从容器中找一个合适的对象引用传入。但也可以不用参数,而是直接在方法体中调用其他@Bean方法,实现注入。例如:

@Configuration
public class CDPlayerConfig {

    @Bean
    public CompactDisc compactDisc() {
//        return new SgtPeppers();
        SgtPeppers sgtPeppers = new SgtPeppers();
        sgtPeppers.play();
        return sgtPeppers;
    }

    // 传入参数
//    @Bean
//    public CDPlayer cdPlayer(CompactDisc cd) {
//        return new CDPlayer(cd);
//    }
    
    // 直接调用@Bean方法
    @Bean 
    public CDPlayer cdPlayer() {
        //这里的方法调用被Spring拦截,并传入已经存在于容器内的对象
        return new CDPlayer(compactDisc()); 
    }
    
    // 就算多次调用函数,容器内也只有一个CD实例
    @Bean 
    public CDPlayer cdPlayer2() {
        //这里的方法调用被Spring拦截,并传入已经存在于容器内的对象
        return new CDPlayer(compactDisc()); 
    }

}

在需要使用容器中的对象的位置,还用@Autowired注解进行声明,Spring便会传入一个容器内的对象。

Bean对象的名字

需要创建成Bean对象的类可以实现一个BeanNameAware接口中的方法 setBeanName。Spring在容器中实例化这个对象后,会回调这个方法,将当前对象的名字传入。

在配置类中可以通过注解参数指定Bean对象的名字,如@Bean(name=”newPlayer”) 。默认是取方法的名字。

顺便一提,上一种方法(自动化配置)中,容器内Bean对象的默认名字是类名(第一个字母改为小写)。如果需要指定名字,使用诸如@Component(“newPlayer”)的注解就可以起名。

业务代码和配置代码分离

通过配置类配置Bean的好处是,业务代码中不需要加入什么Spring的内容,也就是Spring对业务代码的侵入性大大降低了。为了减小耦合、建立清晰的项目结构,通常把配置类放在单独的地方,和业务代码隔开。

XML文件配置

是Spring最先出现的一种容器对象配置方法,可以在XML中用一定格式指定配置,就可以在其他地方用xml文件构建上下文(Spring容器)。

虽然是Spring的起家方案,但是因为陈旧、不利于编译器帮忙进行错误检查,已经很少有人使用。现在使用最多的是第一种方案,然后就配置类的第二种方案。

xml文件头需要使用特定的头部,例如:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:c="http://www.springframework.org/schema/c"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <!-- 中间是bean定义 -->
    
</beans>

Tips: 测试类上使用 @ContextCofiguration 声明上下文时,如果不加参数,默认会找同名的xml配置文件配置上下文。

构造方法配置

constructor-arg 指定构造方法参数,如:

<bean id="compactDisc" class="soundsystem.SgtPeppers" />
      
<bean id="cdPlayer" class="soundsystem.CDPlayer">
  <constructor-arg ref="compactDisc" />
</bean>

传入参数的类型不仅有引用,也可以之间传入值:

<bean id="compactDisc" class="soundsystem.BlankDisc">
  <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
  <constructor-arg value="The Beatles" />
</bean>

甚至可以直接传入list,在xml用list元素声明即可:

<bean id="compactDisc" class="soundsystem.collections.BlankDisc">
  <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
  <constructor-arg value="The Beatles" />
  <constructor-arg>
    <list>
      <value>Sgt. Pepper's Lonely Hearts Club Band</value>
      <value>With a Little Help from My Friends</value>
      <value>Lucy in the Sky with Diamonds</value>
        <!-- 省略内容 -->
    </list>
  </constructor-arg>
</bean>

另外还有set元素,可以保证元素不重复。

c命名空间

在头部增加命名空间的定义,就可以在bean的定义中使用c命名空间进行属性声明。命名空间定义例如下面第4行所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:c="http://www.springframework.org/schema/c"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>

在使用时,引用参数传递可以这样使用:

<bean id="cdPlayer" class="soundsystem.CDPlayer"
      c:_0-ref="compactDisc" />

_0-ref 指的是传入的第一个参数,_1-ref 指的是第二个参数,以此类推。如果只有一个参数甚至可以不加数字。

如果是值传递:

<bean id="compactDisc" class="soundsystem.BlankDisc"
      c:_0="Sgt. Pepper's Lonely Hearts Club Band" 
      c:_1="The Beatles" />

属性配置

可以通过set方法进行配置。Spring寻找set方法就是将属性首字母大写然后前面加set,然后将后面的参数传入。例如:

<bean id="cdPlayer" class="soundsystem.properties.CDPlayer">
  <property name="compactDisc" ref="compactDisc" />
</bean>

在这个配置中,Spring自动找到类的 setCompactDisc 方法,然后传入名为 compactDisc 的Bean引用,将该对象的类进行配置。

Tips: xml配置方法对代码没有侵入性,减少对框架的依赖

同样也可以进行属性值的注入,以及list和set的注入:

  <bean id="compactDisc"
        class="soundsystem.properties.BlankDisc">
    <property name="title" value="Sgt. Pepper's Lonely Hearts Club Band" />
    <property name="artist" value="The Beatles" />
    <property name="tracks">
      <list>
        <value>Sgt. Pepper's Lonely Hearts Club Band</value>
        <value>With a Little Help from My Friends</value>
        <value>Lucy in the Sky with Diamonds</value>
          <!-- 省略 -->
      </list>
    </property>
  </bean>

p命名空间

在头部增加p命名空间的声明(第4行):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
</beans>

但是p命名空间不能解决list赋值的问题,所有如果想直接赋值列表,还是要使用property标签。

实在想用可以用util先声明列表后使用。

如果想对第三方库进行装配,由于不能改变库代码,所以只能使用配置类或配置xml的方式配置

混合配置

三种方法可以混合使用。

配置类可以import其他配置类和xml配置文件。只需使用最上层的跟配置就可以用到所有import到的配置。例如:

@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath:cd-config.xml")
public class SoundSystemConfig {

}

也可以使用xml文件做为根,在xml中可以创建配置类的Bean。看到Bean是配置类,Spring就会自动调用配置类中的@Bean方法创建Bean对象。

<bean class="soundsystem.CDConfig" />

其他功能

Profile

可以指定某些Bean只在特定的环境下才会创建。

@Profile(“dev”)

@Profile(“prod”)

决定用哪个Bean要靠环境指定。可以通过激活的方式指定环境:

  • 默认spring.profiles.default=”dev”,所以默认会使用dev的Bean。
  • 在java运行参数中可以通过 -D的方式修改spring.profiles.active=”sys”,就可以使用prod的Bean。-D参数还可以起到不同环境读不同配置文件的作用,例如 java -jar -Dspring.profiles.active=sys 就可以让Spring运行时读取application-sys.properties而不是application.properties
  • @ActiveProfiles(“dev”) 通过注解激活。

Conditional

有条件地创建Bean。通过@Conditional(**.class)指定一个实现了Condition接口的类。类实现boolean matches接口后就可以进行判断,根据条件决定要不要创建Bean。

例如类中可以去读取系统环境变量,实现有某个环境变量时创建,但没有时不创建的效果。

Scope

可以指定Bean的四种作用域:

  • Singleton 单例 默认
  • Prototype 多例
  • Session 一次会话包含多次请求响应。一次会话创建一个
  • Request 一次请求。每次请求创建一个。

Spring将会话/请求作用域的Bean注入到单例Bean的时候,会通过代理对象实现。实际运行时,Spring可以根据上下文区分后面具体的会话/请求作用域Bean是哪个。

自动装配的歧义性

实现某个接口的Bean可能有多个,导致Spring自动注入的时候不知道该选哪个,产生报错。

@Primary可以指定创建某个Bean

@Qualifier(“mycd”)指定限定符。在注入时也用带有相同限定符的@Qualifier(“mycd”)去指定,Spring就能对应起来了。

还可以自己定义注解,但是自定义的注解定义中也是通过Qualifier注解实现的。

不加任何修饰的注解的Bean,Spring默认会去创建。