Spring-依赖注入

  • 概述
  • 属性注入
    • 属性注入实例
      • 代码演示
    • JavaBean关于属性命名的特殊规范
  • 构造函数注入
    • 按类型匹配入参
    • 按索引匹配入参
    • 联合使用类型和索引匹配入参
    • 通过自身反射类型匹配入参
    • 循环依赖问题
  • 工厂方法注入
  • 选择注入方式的考量

概述

Spring支持两种依赖注入的方式

  • 属性注入
  • 构造函数注入

此外Spring还支持工厂方法注入。 这篇博文我们将了解到不同注入方式的具体配置方法。


属性注入

属性注入指的是通过setXxx()方法注入Bean的属性值或者依赖对象。

由于属性注入方式具有可选择性和灵活性高的有点,因此属性注入是实际应用中最常用的注入方式。


属性注入实例

属性注入的要求

  1. 提供一个默认的构造函数
  2. 为需要注入的属性提供对应的Setter方法

Spring先调用Bean的默认构造函数实例化Bean对象,然后通过反射调用Setter方法注入属性值。

代码演示

Spring-依赖注入_第1张图片

POJO对象

package com.xgj.ioc.inject.set;

public class Plane {

    private String brand;
    private String color;
    private int speed;

    /**
     * 在没有其他显示构造函数的情况下,默认构造函数可省略,有则必须声明
     */
    public Plane(){

    }

    public Plane(String brand,String color,int speed){
        this.brand = brand;
        this.color = color;
        this.speed = speed;
    }


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

    public void setColor(String color) {
        this.color = color;
    }

    public void setSpeed(int speed) {
        this.speed = speed;
    }

    public void introduce() {
        System.out.println("Plane brand:" + brand + ",color:" + color
                + ",speed," + speed);
    }
}

Bean配置文件


<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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="plane" class="com.xgj.ioc.inject.set.Plane">
        <property name="brand">
            <value>A380value>
        property>

        <property name="color">
            <value>redvalue>
        property>

        <property name="speed">
            <value>700value>
        property>
    bean>
beans>

测试类:

package com.xgj.ioc.inject.set;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SetTest {

    public static void main(String[] args) {
        // 加载配置文件,实例化bean
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/set/plane.xml");
        // 从容器中获取bean
        Plane plane = ctx.getBean("plane",Plane.class);
        // 调用方法
        plane.introduce();
    }

}

结果:
Spring-依赖注入_第2张图片

解读:

上面的代码配置了一个Bean,并为该Bean的3个属性提供了属性值, 具体来说Bean的每一个属性对应一个property标签,name为属性的名称(上述配置较为繁琐,后续介绍p的用法),在Bean实现类中拥有与其对应的Setter方法: 比如 brand对应setBrand()。

有一点需要注意: spring只会检查Bean中是否有对应的Setter方法,至于Bean中是否有对应的属性成员变更则不做要求。
举个例子:
配置文件中的属性配置项仅要求Plane中拥有setBrand()方法,但Plane类中不一定要拥有brand成员变量。

比如

private String color;
private int speed;
// 仅拥有setBrand方法,但是类中没有brand成员变量
public void setBrand(String brand) {
        ......
    }

但一般情况下,还是按照约定俗成的方式在Bean中提供同名的属性变量


注意:
默认构造函数是不带参数的构造函数。 Java语言规定,如果类中没有定义任何构造函数,JVM会自动为其生成一个默认的构造函数;反之,如果类中显式的定义了构造函数,JVM则不会为其生成默认的构造函数。如果类中显式的声明了其他构造函数,如果未提供一个默认的构造函数,则属性注入时,会抛出异常。

举例:
我们在Plane中增加一个显式构造函数,去掉默认的构造函数

public Plane(String brand,String color,int speed){
        this.brand = brand;
        this.color = color;
        this.speed = speed;
    }

运行测试类,

Failed to instantiate [com.xgj.ioc.inject.set.Plane]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.xgj.ioc.inject.set.Plane.()

Spring-依赖注入_第3张图片


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

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

一般情况下,Java的属性变量名都以小写字母开头,比如brand,speed等,但也有些特殊情况的存在。考虑到一些特定意义的大写英文缩略字母(比如USA、XML等),Javabean也允许以大写字母开头的属性变量名,不过必须满足 变量的前两个字母要么全部大写,要么全部小写。

比如brand,IDCode、IC等是合法的,而iC、iDCard等是非法的。

比如我们在Plane类中添加属性和setter方法

// 非法的属性变量名,但是Java并不会报错,因为它将iDCard看做普通的变量
    private String iDCard;

    // 改setter方法对应IDCard属性,而非iDCard属性
    public void setIDCard(String iDCard) {
        this.iDCard = iDCard;
    }

因为xxx的属性对应setXxx()方法. 所以是setIDCard().

bean配置文件

Spring-依赖注入_第4张图片

STS中校验都未通过,更加无法运行了。

总结:
以大写字母开头的变量总是显得比较另类,为了规避这种诡异的错误,用户可遵照以下编程的经验:比如QQ、MSN、ID等正常情况下以大写字母出现的专业术语,在Java中一律调整为小写形式,如qq、msn、id等,以保证命名的统一性(变量名称都小写字母开头),减少出错概率。


构造函数注入

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


按类型匹配入参

举个例子,假设任何使用Tank对象都必须提供brand和weight,若使用属性注入 方式,这只能人为在配置时候提供保证而无法再语法级提供保证,这时候构造函数注入就可以很好地满足这一个需求。

使用构造函数的前提是Bean必须提供带参的构造函数。

代码演示:

Spring-依赖注入_第5张图片

Pojo Bean实现类

package com.xgj.ioc.inject.construct;

public class Tank {

    private String brand;
    private double weight;

    public Tank(String brand, double weight) {
        super();
        this.brand = brand;
        this.weight = weight;
    }

    public void introduce() {
        System.out.println("Tank information: brand:" + brand + ",weight:"
                + weight + "KG");
    }

}

配置文件


<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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="tank" class="com.xgj.ioc.inject.construct.Tank">

        <constructor-arg type="java.lang.String">
            <value>T72value>
        constructor-arg>

        <constructor-arg type="double">
            <value>15000.00value>
        constructor-arg>

    bean>
beans>

测试类:

package com.xgj.ioc.inject.construct;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ConstructInjectTest {

    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/construct/tank.xml");

        Tank tank = ctx.getBean("tank", Tank.class);

        tank.introduce();
    }

}

运行结果:

这里写图片描述

解读:

在constructor-arg的元素中有个type属性,它为Spring提供了判断配置项和构造函数入参对应关系的信息。

一个疑问

  • 配置文件中constructor-arg声明顺序难道不能用于确定构造函数入参的顺序吗?—在只有一个构造函数的情况下当然是可以的,如果类中定义了多个具有相同入参的构造函数,这种顺序标识就失效了。

另外,Spring的配置文件采用和元素标签顺序无关的策略,一定程度上保证了配置信息的确定性,避免一些似是而非的问题。

    <constructor-arg type="java.lang.String">
            <value>T72value>
        constructor-arg>

        <constructor-arg type="double">
            <value>15000.00value>
        constructor-arg>

这两个参数的位置并不会影响对最终的配置效果产生影响。


按索引匹配入参

众所周知,Java语言通过入参的类型和顺序区分不同的重载方法。

如果Tank类中有两个相同类型的入参,仅仅通过type就无法确定的对应关系了。这是需要通过入参索引的方式进行确定。

Spring-依赖注入_第6张图片

POJO对象

package com.xgj.ioc.inject.construct.index;

public class Tank {

    private String brand;
    private double weight;
    private double speed;

    /**
     * 
     * @param brand
     * @param weight
     * @param speed
     * 第二个参数和第三个参数同为double类型
     */
    public Tank(String brand, double weight, double speed) {
        this.brand = brand;
        this.weight = weight;
        this.speed = speed;
    }

    public void introduce() {
        System.out.println("Tank information: brand:" + brand + ",weight:"
                + weight + "KG,speed:" + speed + "km/h");
    }

}

第二个参数和第三个参数同为double类型,所以Spring无法确定type为double到底对应constructor-arg中的哪个,但是通过显示指定参数的索引能消除这种不确定性。

配置文件


<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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="tank" class="com.xgj.ioc.inject.construct.index.Tank">

        <constructor-arg index="0" value="T72" />
        <constructor-arg index="1" value="20000" />
        <constructor-arg index="2" value="300" />

    bean>

beans>

构造函数的一个参数索引为0,第二个为1,以此类推

测试类

package com.xgj.ioc.inject.construct.index;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ConstructInjectTest {

    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/construct/index/tank_index_match.xml");

        Tank tank = ctx.getBean("tank", Tank.class);

        tank.introduce();
    }

}

运行结果:

Spring-依赖注入_第7张图片


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

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

Spring-依赖注入_第8张图片

POJO 对象

package com.xgj.ioc.inject.construct.joint;

public class Tank {

    private String brand;
    private double weight;
    private double speed;
    // 载人数量
    private int manned;


    /**
     * 
     * @param brand
     * @param weight
     * @param speed
     * 第二个参数和第三个参数同为double类型
     */
    public Tank(String brand, double weight, double speed) {
        this.brand = brand;
        this.weight = weight;
        this.speed = speed;
    }

    /**
     * 
     * @param brand
     * @param weight
     * @param manned
     */
    public Tank(String brand, double weight,int manned){
        this.brand =brand;
        this.weight = weight;
        this.manned = manned;
    }

    public void introduce() {
        System.out.println("Tank information: brand:" + brand + ",weight:"
                + weight + "KG,speed:" + speed + "km/h");
    }

    public void introduce2() {
        System.out.println("Tank information: brand:" + brand + ",weight:"
                + weight + "KG,manned:" + manned + "/person");
    }


}

配置文件


<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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="tank" class="com.xgj.ioc.inject.construct.joint.Tank">

        <constructor-arg type="java.lang.String" index="0" value="T72" />
        <constructor-arg type="double" index="1" value="25000" />
        <constructor-arg type="int" index="2" value="3" />

    bean>

beans>

如果仅仅根据index来配置,Spring无法知道第三个入参配置的类型究竟是int还是double ,因此需要明确指出第三个入参的类型以消除歧义。
事实上,constructor-arg中前两个的type属性可以去掉、

当然了,也可以直接用type来判断。

测试类

package com.xgj.ioc.inject.construct.joint;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ConstructInjectTest {

    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/construct/joint/tank_joint_match.xml");

        Tank tank = ctx.getBean("tank", Tank.class);

        tank.introduce2();
    }

}

运行结果:
Spring-依赖注入_第9张图片


通过自身反射类型匹配入参

如果Bean构造函数入参的类型是可辩别的(非基础数据类型且入参类型各不相同),由于Java反射机制可以获取构造函数的入参类型,即使构造函数注入的配置不提供类型和索引的信息,Spring依然可以正确的完成构造函数的注入工作。

例子:

Spring-依赖注入_第10张图片

POJO类-Tank

package com.xgj.ioc.inject.construct.reflect;

public class Tank {

    public void attack() {
        System.out.println("tank begins to attack");
    }

}

POJO类-Plane

package com.xgj.ioc.inject.construct.reflect;

public class Plane {

    public void attack(){
        System.out.println("plane begins to attack");
    }

}

配置文件


<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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="plane" class="com.xgj.ioc.inject.construct.reflect.Plane"/>
    <bean id="tank" class="com.xgj.ioc.inject.construct.reflect.Tank"/>

    <bean id="commander" class="com.xgj.ioc.inject.construct.reflect.Commander">
    
        <constructor-arg value="XGJ"/>
        <constructor-arg ref="plane"/>
        <constructor-arg ref="tank"/>
    bean>

beans>

解析:

由于plane tank name入参的类型都是可辨别的,所以无需再构造函数注入的配置时指定constructor-arg的type和index,因此可以采用如上的简易配置方式

测试类

package com.xgj.ioc.inject.construct.reflect;

public class Commander {

    private Plane plane;
    private Tank tank;
    private String name;

    public Commander(Plane plane, Tank tank, String name) {
        super();
        this.plane = plane;
        this.tank = tank;
        this.name = name;
    }


    public void direct(){
        System.out.println("Commamder name:" + name);
        System.out.println("Commnader begins to direct the army");
        plane.attack();
        tank.attack();

    }

}
package com.xgj.ioc.inject.construct.reflect;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ConstructInjectTest {

    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/construct/reflect/beans.xml");

        Commander commander = ctx.getBean("commander", Commander.class);

        commander.direct();
    }

}

运行结果

Spring-依赖注入_第11张图片


循环依赖问题

Spring容器能够对构造函数配置的Bean进行实例化有一个前提:Bean构造函数入参所引用的对象必须已经准备就绪。

鉴于这个机制,如果两个Bean都采用构造函数注入,并且都通过构造函数入参引用对方,就会发生类属于线程死锁的的循环依赖问题。

举个例子说明一下(飞行员和飞机):

Spring-依赖注入_第12张图片

package com.xgj.ioc.inject.construct.loop;

public class Pilot {

    private String pilotNname;
    private Plane plane;

    public Pilot(String pilotNname, Plane plane) {
        super();
        this.pilotNname = pilotNname;
        this.plane = plane;
    }

    public void drivePlane() {

        plane.fly();
    }

}
package com.xgj.ioc.inject.construct.loop;

public class Plane {

    private Pilot pilot;
    private String planeBrand;

    public Plane(Pilot pilot, String planeBrand) {
        super();
        this.pilot = pilot;
        this.planeBrand = planeBrand;
    }

    public void fly() {
        System.out.println("Plane :" + planeBrand + " is reday to fly");
    }
}

<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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="pilot" class="com.xgj.ioc.inject.construct.loop.Pilot">
        <constructor-arg ref="plane"/>
        <constructor-arg value="F35"/>
    bean>

    <bean id="plane" class="com.xgj.ioc.inject.construct.loop.Plane">
        <constructor-arg ref="pilot"/>
        <constructor-arg value="XGJ"/>
    bean>
beans>
package com.xgj.ioc.inject.construct.loop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ConstructInjectTest {

    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/construct/loop/beans.xml");

        Pilot pilot = ctx.getBean("pilot", Pilot.class);

        pilot.drivePlane();
    }

}

运行结果:

Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'pilot' defined in class path resource [com/xgj/ioc/inject/construct/loop/beans.xml]: Cannot resolve reference to bean 'plane' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'plane' defined in class path resource [com/xgj/ioc/inject/construct/loop/beans.xml]: Cannot resolve reference to bean 'pilot' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'pilot': Requested bean is currently in creation: Is there an unresolvable circular reference?
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:359)
.......
.......
.......
org.springframework.context.support.ClassPathXmlApplicationContext.(ClassPathXmlApplicationContext.java:83)
    at com.xgj.ioc.inject.construct.loop.ConstructInjectTest.main(ConstructInjectTest.java:10)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'plane' defined in class path resource [com/xgj/ioc/inject/construct/loop/beans.xml]: Cannot resolve reference to bean 'pilot' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'pilot': Requested bean is currently in creation: Is there an unresolvable circular reference?
    at .......
    .......
    ... 29 more

如何解决呢? 构造函数注入修改为 属性注入即可。

如下所示:

只需要修改beans.xml即可


<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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="pilot" class="com.xgj.ioc.inject.construct.loopsolve.Pilot">
        <property name="pilotNname" value="XGJ">property>
        <property name="plane" ref="plane">property>
    bean>
    <bean id="plane" class="com.xgj.ioc.inject.construct.loopsolve.Plane">
        <property name="planeBrand" value="F35">property>
        <property name="pilot" ref="pilot">property>
    bean>

beans>

运行结果

Spring-依赖注入_第13张图片


工厂方法注入

分为非静态工厂方法和静态工厂方法。

对于一个全新开发的应用来说,我们不推荐使用工厂方法的注入方式。因为工厂方法需要额外的类和代码,这些功能和业务并无关系。


选择注入方式的考量

仁者见仁 智者见智,并无定论。在合适的场景下使用合适的注入方式。

你可能感兴趣的:(【Spring-IOC】,Spring-IOC手札)