Spring | 控制反转与依赖注入

Spring | 控制反转与依赖注入

注入 Injection

基本概念

注入(Injection),简单来讲就是 Spring 通过工厂和配置文件,为所创建对象的成员变量赋值。通过程序中编码的方式(使用对象的 set 方法 )虽然可以为成员变量赋值,但是这样做的缺点依然是存在耦合,通过配置文件,以注解的方式赋值,可以做到降低耦合度。

如何注入

  1. 类要为相关的成员变量提供对应的 setget 方法
public class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  1. 配置文件添加相关的标签
    在要注入的对象对应的 <bean></bean> 标签体内添加 <property 标签,并指定其 name 属性对应该类的成员变量名,在 <property 标签内添加 <value> 标签,为变量赋值
<bean id="person" class="cool.yzt.domain.Person">
    <property name="age">
        <value>25</value>
    </property>

    <property name="name">
        <value>张三</value>
    </property>
</bean>

测试注入效果

ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Person p = ctx.getBean("person",Person.class);
System.out.println(p.getName());
System.out.println(p.getAge());

控制台输出

张三
25

简单分析 Spring 注入的原理

实际上,以上的注入方法的实现,底层是 Spring 框架调用对象属性对应的 set 方法完成的,Spring 工厂创建对象后,会根据配置文件中 <bean> 标签体内的其他标签解析对象的属性名,再通过反射调用相关的 set 方法,完成注入。所以这种注入方法也成为 set 注入。

Set 注入

如上所述,Set 注入实际上就是 Spring 工厂通过调用对象的 set 方法为对象的属性赋值,对于不同类型的属性有不同的注入方法,基本形式都是在 <bean> 标签体中嵌套 <property> 标签,对于特殊类型的属性,还需要嵌套其他标签

注入 Java 内置类型

  1. 8 种基本类型和 String 类型:直接嵌套 <value> 标签即可
<property name="age">
    <value>25</value>
</property>
  1. 数组和 List 集合:嵌套 <list> 标签,<list> 中再嵌套 <value>
    例如,在 Person 类中添加 private String[] email 属性并添加相应的 setget 方法
<property name="email">
    <list>
        <value>zhangsan@qq.com</value>
        <value>zhangsan@163.com</value>
        <value>zhangsan@gmail.com</value>
    </list>
</property>
  1. Set 集合:嵌套 <set> 标签,<set> 中再嵌套 <value>
    例如,在 Person 类中添加 private Set<String> tel 属性并添加相应的 setget 方法
<property name="tel">
    <set>
        <value>123</value>
        <value>456</value>
        <value>123</value>
    </set>
</property>

因为 Set 集合不可以存储重复元素,所以测试打印

Person p = ctx.getBean("person",Person.class);
String[] emails = p.getEmail();
Set<String> tels = p.getTel();
for(String s : tels) {
    System.out.println(s);
}

只有两个元素

123
456
  1. Map 集合:注意到 Java 中 Map 底层实现中会有一个内部类 EntryEntry 中包含存储的键值对,所以要注入 Map 类型的数据,需要 <property> 中嵌套 <map>,再嵌套 <entry> 标签表示一个键值对
    例如,在 Person 类中添加 private Map<String,String> address 属性并添加相应的 setget 方法
<property name="address">
    <map>
        <entry>
            <key><value>province</value></key>
            <value>湖南</value>
        </entry>

        <entry>
            <key><value>city</value></key>
            <value>长沙</value>
        </entry>

    </map>
</property>

IDEA 建议使用更简洁的写法

<property name="address">
    <map>
        <entry key="province" value="湖南"/>
        <entry key="city" value="长沙" />
    </map>
</property>

以上两种方法都可以正确的注入 Map 类型的值

  1. Properties 集合:Properties 是一种特殊的 Map 集合,其键和值只能存储 String 类型的值
    例如,在 Person 类中添加 private Properties account 属性并添加相应的 setget 方法
<property name="account">
    <props>
        <prop key="uid">zhangsan123</prop>
        <prop key="password">1234567</prop>
    </props>
</property>

注入自定义类型

一个类中的属性类型是自定义的类型,这是很常见的需求,例如 UserServiceImpl 类中需要定义一个 UserDAOImpl 属性

方式一:使用 <bean> 标签

  1. 为该自定义类型的成员变量定义 setget方法
  2. 在配置文件 <property> 标签中嵌套使用 <bean> 进行注入
<bean id="userService" class="cool.yzt.service.UserServiceImpl">
    <property name="userDAO">
        <bean class="cool.yzt.dao.UserDAOImpl"/>
    </property>
</bean>

这种方式进行注入虽然可行,但是存在两个问题

  • 配置文件代码冗余,因为对于 cool.yzt.dao.UserDAOImpl 类的 <bean> 标签会出现两次,一次是自己单独声明,一次是出现在 userService 的注入过程中
  • cool.yzt.dao.UserDAOImpl 类的对象实际创建了两个,一个 <bean> 对应一个,实际上这是不必要的,会造成 JVM 内存资源的浪费

方式二:使用 <ref> 标签
这种方式类似于引用,内存中只会存在一个 cool.yzt.dao.UserDAOImpl 类的对象

<bean id="userService" class="cool.yzt.service.UserServiceImpl">
    <property name="userDAO">
        <ref bean="userDAO"/>
    </property>
</bean>

<bean id="userDAO" class="cool.yzt.dao.UserDAOImpl"/>

Set 注入简化写法

基于属性的简化写法

  1. 注入基本类型和 String 类型
<bean id="person" class="cool.yzt.domain.Person">
    <property name="age" value="25"/>
    <property name="name" value="张三"/>
</bean>
  1. 注入自定义类型
<bean id="userService" class="cool.yzt.service.UserServiceImpl">
    <property name="userDAO" ref="userDAO"/>
</bean>

基于 p 命名空间的简化写法

这是一种更加简洁的写法,直接在 <bean> 标签中注入,使用关键字 p 表示 <property> 标签

  1. 注入基本类型和 String 类型
<bean id="person" class="cool.yzt.domain.Person" p:age="25" p:name="张三"/>
  1. 注入自定义类型
<bean id="userService" class="cool.yzt.service.UserServiceImpl" p:userDAO-ref="userDAO"/>

构造注入

与 Set 注入类似,构造注入是指 Spring 工厂调用对象的构造方法,为对象的成员变量(属性)赋值

使用构造注入

  1. 为类添加有参构造方法
public class Person {
    private int age;
    private String name;
  
    public Person(){}

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }
}
  1. 配置文件使用 <constructor-arg> 标签
<bean id="person" class="cool.yzt.domain.Person">
  <constructor-arg>
      <value>26</value>
  </constructor-arg>
  <constructor-arg>
      <value>李四</value>
  </constructor-arg>
</bean>

或者简写为

<constructor-arg value="26"/>
<constructor-arg value="李四"/>

构造方法重载

对于构造方法有重载的情况

  1. 如果重载的构造方法的参数个数不同:通过控制 <constructor-arg> 标签的个数进行区分
  2. 如果重载的构造方法参数个数相同,而参数类型不同,可以通过 <constructor-arg>type 属性进行区分:<constructor-arg type="">

控制反转和依赖注入

控制反转(Inverse of Control,IOC)

控制,可以理解为对对象的创建以及对象属性赋值的控制权,控制反转就是把这种控制权从代码中转移到 Spring 工厂和配置文件中完成,可以降低代码的耦合,其底层实现就是工厂设计模式。

依赖注入(Dependency Injection)

之前说过,注入就是通过 Spring 工厂及配置文件,为对象(即 Bean、组件)的成员变量赋值,当一个类中需要另外一个类时,就意味着以来,例如 UserService 依赖 UserDAO,一旦出现这种依赖,就可以通过 Spring 和配置文件进行注入,好处依然是解耦合。

参考

孙哥说Spring5