Spring基础:依赖注入

前言

在平常的开发过程中通常使用的属性初始化方法有构造函数初始化和 setXxx 方法初始化,接下来让我们看看 Spring 容器属性的初始化过程。

正题

在开始文章前,有几个问题需要思考一下:

  • Spring 支持的依赖注入有哪几种
  • 如何选择适合自己的注入方式
1. Spring 支持的依赖注入有哪几种

Spring支持两种依赖注入方式,分别是属性注入和构造函数注入。除此之外,Spring还支持工厂方法注入方式。

1.1 属性注入

属性注入指通过 setXxx() 方法注入Bean的属性值或依赖对象。由于属性注入方式具有可选择性和灵活性高的优点,因此属性注入是实际应用中最常采用的注入方式。属性注入要求 Bean 提供一个默认的构造函数,并为需要注入的属性提供对应的 Setter 方法。Spring 先调用 Bean 的默认构造函数实例化 Bean 对象,然后通过反射的方式调用Setter方法注入属性值。

public class Car {
    private int maxSpeed;
    private String brand;
    private double price;

    public void setMaxSpeed(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}
在Spring配置文件中对Car进行属性注入的配置片段:
<bean id="car" class="com.smart.domain.Car">
    <property name="maxSpeed"><value>100</value></property>
    <property name="brand"><value>红旗CA72</value></property>
    <property name="price"><value>200000.00</value></property>
</bean>

上述代码配置了一个 Bean,并为该 Bean 的3个属性提供了属性值。具体来说,Bean 的每一个属性对应一个<property>标签,name 为属性的名称,在 Bean 实现类中拥有与其对应的 Setter 方法。需要指出的是,Spring 只会检查 Bean 中是否有对应的 Setter 方法,至于 Bean 中是否有对应的属性成员变量则不做要求

默认构造函数是不带参的构造函数。Java语言规定,如果类中没有定义任何构造函数,则JVM会自动为其生成一个默认的构造函数;反之,如果类中显示定义了构造函数,则JVM不会为其生成默认的构造函数。所以假设Car类中显示定义了一个带参的构造函数,如pubic Car(String brand),则需要同时提供一个默认的构造函数public Car(),否则使用属性注入时将抛出异常。

No default constructor found; nested exception is java.lang.NoSuchMethodException: com.smart.domain.Car.<init>()

JavaBean关于属性命名的特殊规范

Spring配置文件中<property>元素所指定的属性名和Bean实现类的Setter方法满足Sun JavaBean的属性命名规范:xxx的属性对应setXxx()方法。

一般情况下,Java的属性变量名都以小写字母开头,如maxSpeed,brand等,但也存在特殊的情况。考虑到一些特定意义的大写英文缩略词(如USA、XML等),JavaBean也允许以大写字母开头的属性变量名,不过必须满足“变量的前两个字母要么全部大写,要么全部小写”的要求。这个并不广为人知的JavaBean规范条款引发了从多让人困惑的配置问题。

1.2 构造函数注入

构造函数注入是除属性注入外的另一种常用的注入方式,它保证一些必要的属性在Bean实例化时得到设置,确保Bean在实例化后就可以使用。

1.2.1 按类型匹配入参

如果任何可用的Car对象都必须提供brand和price的值,若使用属性注入方式,则只能人为地在配置时提供保证而无法在语法级提供保证,这时通过构造函数注入就可以很好地满足这一需求。使用构造函数注入的前提是Bean必须提供带参的构造函数。

public class Car {
    ......
    public Car(){}

    public Car(String brand, double price) {
        this.brand = brand;
        this.price = price;
    }
}

       构造函数注入的配置方式和属性注入的配置方式有所不同,下面在Spring配置文件中使用构造函数注入的配置方式装配这个car Bean:

<bean id="car" class="com.smart.domain.Car">
    <constructor-arg type="java.lang.String">①
        <value>红旗CA72</value>
    </constructor-arg>
    <constructor-arg type="double">②
        <value>200000</value>
    </constructor-arg>
</bean>

在<constructor-arg>的元素中有一个type属性,它为Spring提供了判断配置项和构造函数入参对应关系的“信息”。细心的读者可能会提出以下疑问:配置文件中<bean>元素的<constructor-arg>声明顺序难道不能用于确定构造函数入参的顺序吗?在只有一个构造函数的情况下当然是可以的,但如果在Car中定义了多个具有相同入参的构造函数,这种顺序标识就失效了。此外,Spring的配置文件采用和元素标签顺序无关的策略,这种策略可以在一定程度上保证配置信息的确定性,避免一些似是而非的问题。因此,①和②处的<constructor-arg>位置并不会对最终的配置效果产生影响。

1.2.2 按索引匹配入参

我们知道Java语言通过入参的类型及顺序区分不同的重载方法。但是如果Car构造函数有两个类型相同的入参,那么仅通过type就无法确定对应关系了,这时需要通过入参索引的方式进项确定。

public class Car {
    ......
    public Car(){}

    public Car(String brand, String corp, double price) {
        this.brand = brand;
        this.corp = corp;
        this.price = price;
    }
}

因为brand和Corp的入参类型都是String,所以Spring无法确定type为String的<constructor-arg>到底对应的是brand还是crop。但是通过显示指定参数的索引能够消除这种不确定性。

我们知道,在属性注入时,Spring按JavaBean规范找到匹配属性对应的Setter方法,并使用Java反射机制调用Setter方法完成属性注入。但Java反射机制并不会记住构造函数的入参名,因此我们无法通过制定的入参名进行构造函数注入的配置,只能通过入参类型和索引信息间接确定构造函数配置项和入参的对应关系。

<bean id="car" class="com.smart.domain.Car">
    <constructor-arg index="0" value="红旗CA72"/>
    <constructor-arg index="1" value="中国一汽"/>
    <constructor-arg index="2" value="200000"/>
</bean>

构造函数的第一个索引为0,第二个为1,以此类推,因此很容易知道对应的入参。

1.2.3 联合使用类型和索引匹配入参

有时需要type和index联合使用才能确定配置项和构造函数入参的对应关系:

    public Car(String brand, String corp, double price) {
        this.brand = brand;
        this.corp = corp;
        this.price = price;
    }

    public Car(String brand, String corp, int maxSpeed) {
        this.brand = brand;
        this.corp = corp;
        this.maxSpeed = maxSpeed;
    }

这里,Car拥有两个重载的构造函数,它们都有两个入参。按照上面的入参位置索引的配置方式针对这种情况又难以满足要求了,这时需要联合使用<constructor-arg>的type和index才能解决问题:

<bean id="car" class="com.smart.domain.Car">
    <constructor-arg index="0" type="java.lang.String" value="红旗CA72"/>
    <constructor-arg index="1" type="java.lang.String" value="中国一汽"/>
    <constructor-arg index="2" type="int" value="200000"/>
</bean>

对于上面代码中的两个构造函数,如果仅通过index进行配置,那么Spring将无法确定第三个入参配置项究竟是对应Int的maxSpeed还是doubleprice,所以在采用索引匹配配置时,真正引起歧义的地方是第三个入参,因此仅需要明确指定第三个入参的类型就可以消除歧义。

对于因参数数目相同而类型不同引起的潜在配置歧义问题,Spring容器可以正确启动且不会给出报错信息,它将随机采用一个匹配的构造函数实例Bean,而被选择的构造函数可能并不是用户所期待的那个。因此,必须特别谨慎,以避免潜在的错误。

1.2.4 循环依赖问题

Spring容器能对构造函数配置的Bean进行实例化有一个前提,即Bean构造函数入参引用的对象必须已经准备就绪。由于这个机制的限制,如果两个Bean都采用构造函数注入,而且都通过构造函数入参引用对方,就会发生类似于线程死锁的循环依赖问题。

public class Car {
    ......
    public Car(String brand, Boss boss) {
        this.brand = brand;
        this.boss = boss;
    }
}

public class Boss {
    ......
    public Boss(String name, Car car) {
        this.name = name;
        this.car = car;
    }
}

假设在Spring配置文件中按照以下构造函数注入方式进行配置:

<bean id="car" class="com.smart.domain.Car">
    <constructor-arg index="0" value="红旗CA72"/>
    <constructor-arg index="1" ref="boss"/>
</bean>

<bean id="boss" class="com.smart.domain.Boss">
    <constructor-arg index="0" value="John"/>
    <constructor-arg index="1" ref="car"/>
</bean>

错误信息:

Requested bean is currently in creation: Is there an unresolvable circular reference
当启动Spring IoC容器时,因为存在循环依赖问题,Spring容器将无法成功启动。如何解决这个问题呢?用户只需修改Bean的代码,将构造函数注入方式调整为属性注入方式就可以了。
1.3 工厂方法注入

工厂方法是在应用中被经常使用的设计模式,它也是控制反转和单实例设计思想的主要实现方法。由于Spring IoC容器以框架的方式提供工厂方法的功能,并以透明的方式开放给开发者,所以很少需要手工编写基于工厂方法的类。正是因为工厂方法已经成为底层设施的一部分,因此工厂方法对于实际编码的重要性就降低了。不过在一些遗留系统或第三方类库中,我们还会遇到工厂方法,这是可以使用Spring工厂方法注入的方式进行配置。

1.3.1 非静态工厂方法

有些工厂方法是非静态的,即必须实例化工厂后才能调用工厂方法。

public class CarFactory {
    //创建Car的工厂方法
    public Car CreateHongQiCar() {
        Car car = new Car();
        car.setBrand("红旗CA72");
        return car;
    }
}

工厂类负责创建一个或多个目标类实例,工厂类方法一般以接口或抽象类变量的形式返回目标类实例。工厂类对外屏蔽了目标类的实例化步骤,调用者甚至无须知道具体的目标类是什么。

<!-- ①工厂类Bean -->
<bean id="carFactoty" class="com.smart.ditype.CarFactory"/>

<!-- factory-bean指定①处的工厂类Bean;factory-method指定工厂类Bean创建该Bean的工厂方法 -->
<bean id="car5" factory-bean="carFactoty" factory-method="CreateHongQiCar"/>

由于CarFactory工厂类的工厂方法不是静态的,所以首先需要定义一个工厂类的Bean,然后通过factory-bean引用工厂类实例,最后通过factory-method指定对应的工厂类方法。

1.3.2 静态工厂方法

很多工厂类方法都是静态的,这意味着用户在无须创建工厂类实例的情况下就可以调用工厂类方法,因此,静态工厂方法比非静态工厂方法更易使用。下面对CarFactory进行改造,将其createHongQiCar()方法调整为静态的。

public class CarFactory {
    //工厂类静态方法
    public static Car CreateHongQiCar() {
        Car car = new Car();
        car.setBrand("红旗CA72");
        return car;
    }
}

当使用静态工厂类型的方法后,用户就无须在配置文件中定义工厂类的Bean,只需按以下方式进行配置即可:


直接在<bean>中通过class属性指定工厂类,然后再通过factory-method指定对应的工厂方法。

2.  如何选择适合自己的注入方式

Spring提供了3中可供选择的注入方式,在实际应用中,究竟应该选择哪种注入方式呢?对于这个问题,仁者见仁,智者见智,并没有统一的标准。下面是支持使用构造函数注入的理由:

  • 构造函数可以保证一些重要的属性在Bean实例化时就设置好,避免因为一些重要属性没有提供而导致一个无用Bean实例的情况。
  • 不需要为每个属性提供Setter方法,减少了类的方法个数。
  • 可以更好的封装变量,不需要为每个属性指定Setter方法,避免外部错误的调用。

更多的开发者可能倾向于使用属性注入方式,她们反对构造函数注入的理由如下:

  • 如果一个类的属性众多,那么构造函数的签名将变成一个庞然大物,可读性很差。
  • 灵活性不强,在有些属性是可选的情况下,如果通过构造函数注入,也需要为可选的参数提供一个null值。
  • 如果有多个构造函数,则需要考虑配置文件和具体构造函数匹配歧义的问题,配置上相对复杂。
  • 构造函数不利于类的继承和扩展,因为子类需要引用父类复杂的构造函数。
  • 构造函数注入有时会造成循环依赖的问题。

其实构造函数注入和属性注入各有自己的应用场景,Spring并没有强制用户使用哪一种方式,用户完全可以根据个人偏好做出选择,在某些情况下使用构造函数注入,而在另一些情况下使用属性注入。对于一个全新开发的应用来说,我们不推荐使用工厂方法的注入方式,因为工厂方法需要额外的类和代码,这些功能和业务是没有关系的,既然Spring容器已经以一种更优雅的方式实现了传统工厂模式所有功能,那么我们大可不必再去做这项重复性的工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值