spring properties 注入 map spring注入pojo_c++

Spring系列之依赖注入

Spring 中所有的 Bean 都是通过容器来进行管理的。每个 POJO 都可以是一个 Spring Bean。容器会管理 Bean 的依赖关系,这种依赖关系有可能是 Bean 之间的,也有可能是 Bean 对配置数据的依赖。在使用 Spring 的时候,开发者需要做的就是让 Spring 容器知道这些依赖关系,然后剩下的事情交给 Spring 容器就行了。

spring properties 注入 map spring注入pojo_xml_02


Spring 容器在初始化的时候会做两件事,将配置中的所有 POJO 加载进容器生成 Bean 并且注入 Bean 之间的依赖关系。配置 Bean 之间的依赖关系就是我们所说的依赖注入,需要注意的是这个生成 Bean 和依赖注入之间并不存在严格的先后关系,具体下面再说。

Bean 的配置

如果我们想让 Spring 来管理 Bean,第一步就是要将这些 Bean 装入容器中。把 Bean 装入容器中的方式有三种:

  • 使用 xml 配置文件
  • 使用注解78
  • 使用 Java 代码

这三种方式完成的效果都一样,而且这三种方式可以混合使用。在下面我会主要使用 xml 的方式来作为例子来进行演示,因为 xml 配置文件相对比较直观,也会提供注解版本和 Java 代码版本的例子。

public class Gun implements Weapon{
    @Override
    public boolean attack() {
        return false;
    }
}

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

    <bean id="gun" name="gun1,gun2" class="cn.rayjun.springdemo.springcontainer.Gun">
    </bean>
</beans>

在上面的配置文件中,我们创建一个 xml 配置文件,其中 xml 根元素是 beans,然后里面有一个bean 元素,这就是一个 Spring Bean。上面配置文件的意思就是告诉容器,我有一个名字叫做Gun 的 Java 类,现在交给你管理了,就这么简单。

上面 XML Bean 的配置中有 id,name,class 等几个属性。其中 class 很简单,就是类的全限定名称,这个相当于告诉 Spring 容器要创建哪个类的实例。id 和 name 都是用来标识一个 Bean 的唯一性。每个 Bean 的 id 在容器中只能是唯一的,而 name 则可以有多个,每个 name 使用,、; 或者空格来隔开,id 的命名需要符合 XML 的命名规范,也就是不能使用特殊字符,但 name 则可以使用特殊字符来进行命名,如果没有定义 id,则会把 name 的第一个值定为 id,如果 id 和 name 都没有定义,则使用类全名加上数字编号来作为 id。获取 bean 的时候,可以通过如下的方式获取:

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Gun gun1 = (Gun) context.getBean("gun1");
Gun gun = (Gun) context.getBean("gun");

System.out.println(gun == gun1); // true

在实际使用 Spring 的过程中是不会使用上面的方式来使用 Bean,都会通过依赖注入的方式来获取。

使用注解如何配置呢,使用注解的配置如下:

@Component
public class Gun implements Weapon{
    @Override
    public boolean attack() {
        return false;
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
    <context:component-scan base-package="cn.rayjun.springdemo"/>
</beans>

上面的代码与完全使用 xml 配置的效果相同,你可能会想,这样更麻烦了,其实不是,在 xml 中添加 context:component-scan 之后,cn.rayjun.springdemo 包下所有被 @Component 注解的类都会自动添加到容器中。也就是 xml 中就不再需要 这个标签了,如果想把 xml 从代码中完全剔除掉上面的代码可以写成这样:

@Component
public class Gun implements Weapon{
    @Override
    public boolean attack() {
        return false;
    }
}

@Configuration
@ComponentScan(basePackages = "cn.rayjun.springdemo")
public class SoldierConfig {
}

除了 @Component 注解之外,还有 @Repository, @Service, @Controller 等注解,@Repository 主要用来标识数据层,@Service 主要用来标识 Service 层,@Controller 主要用来标识 Controller 层。这些注解其实并没有什么不同,这么做只是为了让代码的分层更加清晰,这些注解都可以使用 @Componenet 替代。@Repository注解的实现如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

实际上 @Repository 等注解就是使用 @Component 注解实现的。Spring 号称是无侵入性的,但是上面的代码却在 Gun 类上添加了 @Component 的注解,虽然不影响代码的功能,但还是稍微有点侵入性的,下面是终极的方案:

public class Gun implements Weapon{
    @Override
    public boolean attack() {
        return false;
    }
}

@Configuration
public class SoldierConfig {
    @Bean
    public Gun gun() {
        return new Gun();
    }
}

这里使用带 @Configuration 的配置类完全替代 XML,这样一来,代码中没有 XML,通过也做到了对代码真正的无侵入性。

上面是将代码放入到容器中的三种方法,这三种方法不是独立存在的,而是可以混用的。这三种配置各有各的好处,XML 让 Bean 之间的依赖很清晰,而且不用修改源码;注解可以基本消除掉配置文件,但是这样代码中就会充斥各种注解;Java 配置则可以完全替代 XML。

依赖注入

在上面我们说生成 Bean 和依赖注入不存在严格的先后关系,这是因为 DI有两种方式:构造方法参数注入 和 Setter 方法注入。如果是构造参数注入,那么在 Bean 生成对象的时候就需要将依赖注入,如果是 Setter 方法注入,则是在 Bean 对象生成之后才会注入。

下面来看看如何进行依赖注入,先来看 XML 版本:

// 构造方法注入
public class Soldier {
    private Weapon weapon;

    public Soldier(Weapon weapon) {
        this.weapon = weapon;
    }
}
<?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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="gun" class="cn.rayjun.springdemo.springcontainer.Gun">
    </bean>
    
    <bean id="solider" class="cn.rayjun.springdemo.springcontainer.Soldier">
        <constructor-arg name="weapon" ref="gun"/>
    </bean>
</beans>

使用构造方法注入时,需要在构造方法的参数上声明所需要的依赖,然后在 XML 配置中通过 将依赖注入。再来看看 Setter 方法注入:

// setter 方法注入
public class Soldier {
    private Weapon weapon;

    public void setWeapon(Weapon weapon) {
        this.weapon = weapon;
    }
}
<?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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="gun" class="cn.rayjun.springdemo.springcontainer.Gun">
    </bean>

    <bean id="solider" class="cn.rayjun.springdemo.springcontainer.Soldier">
        <property name="weapon" ref="gun"/>
    </bean>
</beans>

首先 Solider 中需要有待注入依赖的 Setter 方法,然后在 XML 配置中通过 来进行注入。那么构造参数注入和 Setter 方法注入怎么选呢?官方的推荐的做法是:如果依赖关系是强依赖时,使用构造参数注入,如果是可选依赖,则使用 setter 进行注入。强依赖可以理解为当前这个 Bean 正常运行所必须的依赖。

下面再来看看注解的是如何来进行依赖注入的,先看下面这段代码,下面的这段代码使用的是构造函数参数注入的方式:

@Component
public class Soldier {
    private Weapon weapon;
    @Autowired
    public Soldier(Weapon weapon) {
        this.weapon = weapon;
    }
}
@Component
public class Knife implements Weapon{
    public boolean attack() {
        return false;
    }
}
@Component
public class Gun implements Weapon{
    public boolean attack() {
        return false;
    }
}

除了使用构造函数参数注入之外,还可以使用 Setter 方法注入和成员变量注入,这些注入方式的效果都一样:

// Setter 方法注入
@Component
public class Soldier {
    private Weapon weapon;
    @Autowired
    public void setWeapon(Weapon weapon) {
        this.weapon = weapon;
    }
}
// 私有成员变量注入
@Component
public class Soldier {
    @Autowired
    private Weapon weapon;
}

在上面的代码中,我们使用的是 @Autowired 来进行注入,也可以使用 @Inject 来进行依赖注入。这两个注解是等价的,可以在代码中互相替换,但是推荐在整个项目中保持统一。如果项目的依赖比较复杂,那么代码中就会充斥着这些注解,这也是使用注解配置的问题,大量的这样的注解会让代码不好管。注解使用是最便利的,但也是最难管理的。

还有最后一种注入方式,使用 Java 代码进行注入:

@Configuration
public class SoldierConfig {

    @Bean
    public Gun newGun() {
        return new Gun();
    }

    @Bean
    public Knife newKnife() {
        return new Knife();
    }
    
    @Bean
    public Soldier newSoldier() {
        // 使用构造方法参数注入
        return new Soldier(newGun());
    }
    
    @Bean
    public Soldier newSoldier() {
        // 使用 Setter方法参数注入
        Soldier s = new Soldier();
        s.setWeapon(newGun());
        return s;
    }
}

@ComponentScan 注解用来配置需要扫描的包的范围,被 @Bean 注解的方法表示会得到一个返回类型的 Bean,然后可以直接通过调用相应方法为 Bean 注入依赖。使用 Java 配置的好处是即可以不使用 XML 配置文件,又不会对代码有侵入。上面的代码中都是类之间的依赖,如果依赖的是普通的字面量或者一些容器类型,也可以进行注入,注入的方式如下:

<bean id="soldier" class="cn.rayjun.springdemo.container.Soldier" >
    <property name="name" value="Tom"/>
    <property name="age" value="12" />
</bean>

配置的 value 会根据 Bean 中成员的类型自动进行转换。还可以注入容器类型的值:

<bean id="soldier" class="cn.rayjun.springdemo.container.Soldier" >
    <property name="name" value="Tom"/>
    <property name="age" value="12" />
    <property name="foods">
        <list>
            <value>apple</value>
            <value>orange</value>
            <value>banana</value>
        </list>
    </property>
</bean

依赖冲突

Soldier 中需要注入一个 Weapon 类型的 Bean,现在 Gun 和 Knife 都实现了 Weapon,采用上面的方式注入时,就会报 UnsatisfiedDependencyException 异常,因为容器无法决定要注入哪一个。解决的方法有三种:

  • @Primary 注解
  • @Qualifier 注解
  • 自定义注解

被 @Primary 注解的 Bean 会被优先注入,这样就不会报错:

@Component
public class Soldier {
    private Weapon weapon;
    @Autowired
    public Soldier(Weapon weapon) {
        this.weapon = weapon;
    }
}

@Component
public class Knife implements Weapon{
    public boolean attack() {
        return false;
    }
}

@Component
@Primary
public class Gun implements Weapon{
    public boolean attack() {
        return false;
    }
}

还可以使用 @Qualifier 来明确告诉容器使用的是哪个 Bean。

@Component
public class Soldier {
    private Weapon weapon;
    @Autowired
    public Soldier(@Qualifier("gun") Weapon weapon) {
        this.weapon = weapon;
    }
}

@Component
@Qualifier("knife")
public class Knife implements Weapon{
    public boolean attack() {
        return false;
    }
}

@Component
@Qualifier("gun")
public class Gun implements Weapon{
    public boolean attack() {
        return false;
    }
}

自定义注解稍微复杂点,后续再写文章来说明。

循环依赖

在一些情况下,会出现循环依赖,虽然这种情况比较少,但还是有可能会出现.

public class ClassA {

    private ClassB classB;

    public ClassA(ClassB classB) {
        this.classB = classB;
    }
}
public class ClassB {

    private ClassA classA;

    public ClassB(ClassA classA) {
        this.classA = classA;
    }
}

<bean id="classB" class="cn.rayjun.springdemo.springcontainer.cycle.ClassB">
    <constructor-arg name="classA" ref="classA"/>
</bean>

<bean id="classA" class="cn.rayjun.springdemo.springcontainer.cycle.ClassA">
    <constructor-arg name="classB" ref="classB"/>
</bean>

如果按照上面的配置,启动的时候会报 UnsatisfiedDependencyException 异常,这就是循环依赖,解决的办法也很简单,就是把构造方法注入改成 Setter 方法注入。

public class ClassA {
    private ClassB classB;

    public void setClassB(ClassB classB) {
        this.classB = classB;
    }
}
public class ClassB {

    private ClassA classA;

    public void setClassA(ClassA classA) {
        this.classA = classA;
    }
}
<bean id="classB" class="cn.rayjun.springdemo.springcontainer.cycle.ClassB">
    <property name="classA" ref="classA"/>
</bean>
<bean id="classA" class="cn.rayjun.springdemo.springcontainer.cycle.ClassA">
    <property name="classB" ref="classB"/>
</bean>

改成 Setter 方法之后就可以注入成功了,原因就是构造函数注入和 Setter 方法注入的时机不同,构造函数需要在生成对象的时候就注入,这是 ClassA 和 ClassB 就产生了鸡生蛋还是蛋生鸡的问题。而 Setter 方法注入这是在生成对象之后,那就可以成功注入了