说说 Spring 框架中的依赖注入方式

Spring 框架支持三种依赖注入方法:属性注入、构造函数注入以及工厂方法注入。下面,我们将一一予以说明。

1 属性注入

属性注入即通过 setXxx() 方法注入 Bean 的属性值或者依赖对象,由于属性注入方式具有可选择性和灵活性高的优点,因此属性注入是实际项目中最常采用的注入方式 。

1.1 实例

属性注入要求 Bean 提供一个默认的构造函数,并为需要注入的属性提供对应的 Setter 方法 。Spring 先调用 Bean 的默认构造函数实例化 Bean 对象,然后通过反射来调用 Setter 方法注入属性值 。 下面我们举一个例子:

public class Book {

    /**
     * 书名
     */
    private String name;

    /**
     * 定价
     */
    private double price;

    /**
     * 字数
     */
    private int wordNum;

    public void setName(String name) {
        this.name = name;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public void setWordNum(int wordNum) {
        this.wordNum = wordNum;
    }
   ...
}

注意:默认构造函数是不带参数的构造函数 。Java 语言规定,如果类中没有定义任何构造函数,则 JVM 会自动为其生成一个默认的构造函数 。 反之,如果类中显式定义了构造函数,那么 JVM 就不会为其生成默认的构造函数 。

属性注入配置:

<bean id="book" class="net.deniro.spring4.bean.Book">
    <property name="name">
        <value>面纱</value>
    </property>
    <property name="price">
        <value>25.5</value>
    </property>
    <property name="wordNum">
        <value>80000</value>
    </property>
</bean>

注意: Spring 只会检查 Bean 中是否有对应的 Setter 方法,至于 Bean 中是否有对应的属性变量则不做要求,因为它底层是采用 Java 的反射方法来设置值的呀O(∩_∩)O哈哈~

单元测试:

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Book book= (Book) context.getBean("book");
Assert.assertEquals(book.getName(),"面纱");

1.2 Java Bean 属性命名规范

Java Bean 属性命名规范是 xxx 的属性,它对应着的是 setXxx() 方法。

一般情况下,Java 的属性变量名都以小写字母起头。但也存在特殊情况 , 比如一些具有特定意义的大写英文缩略词(如 JSON、AJAX等)。 Java Bean 也允许大写字母起头的属性变量名 , 不过必须满足 " 变量的前两个字母要么全部大写 , 要么全部小写 " 的要求 。如 : IDCard 的属性名就是合法的,但 iDCard 属性名则是非法的,虽然 Spring 框架仍然可以正确注入,但不推荐。

所以,为了简便起见,对于以大写英文缩略词出现的专业术语,一律将其调整为小写形式,如 id 等,这样可以保证命名的统一性。

2 构造函数注入

构造函数注入保证一些必要的属性在 Bean 实例化时就得到设置,这样 Bean 在实例化后就可以使用啦O(∩_∩)O哈哈~

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

2.1 按类型匹配入参

我们为 “书” 这个类,添加一个带参的构造函数:

public Book(String name, double price) {
    this.name = name;
    this.price = price;
}

配置:

<bean id="book2" class="net.deniro.spring4.bean.Book">
    <constructor-arg type="java.lang.String">
        <value>人生的枷锁</value>
    </constructor-arg>
    <constructor-arg type="double">
        <value>35</value>
    </constructor-arg>
</bean>

<constructor-arg> 的元素中有一个 type 属性,它表示构造函数中参数的类型,这是 spring 用来判断配置项与构造函数入参对应关系的 “桥梁”。

注意:Spring 的配置采用了与元素标签顺序有关的策略,所以上面的多个 <constructor-arg> 位置并会对配置效果产生影响。

2.2 按索引匹配入参

如果 Book 类定义的构造函数具有多个类型相同入参,那么就需要依赖配置顺序咯,如果想定义的更加灵活,那么就可以使用 “ 按索引匹配入参”。

注意: Spring 底层是采用 Java 反射能力来实现依赖注入的,但 Java 反射无法获知构造函数的参数名,所以只能通过入参类型与索引信息来间接地确定构造函数的配置项与入参之间的对应关系。

假设 Book 类中新增了 “出版社” 属性:

/**
 * 出版社
 */
private String press;

 public Book(String name, String press) {
    this.name = name;
    this.press = press;
}

配置文件:

<bean id="book3" class="net.deniro.spring4.bean.Book">
    <constructor-arg index="0">
        <value>人生的枷锁</value>
    </constructor-arg>
    <constructor-arg index="1">
        <value>上海译文出版社</value>
    </constructor-arg>
</bean>

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

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

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


public Book(String name, String press, double price) {
    this.name = name;
    this.press = press;
    this.price = price;
}

public Book(String name, String press, int wordNum) {
    this.name = name;
    this.press = press;
    this.wordNum = wordNum;
}

这里的 Book 拥有两个重载的构造函数,它们都有三个入参,这时就需要联合使用类型匹配入参与索引匹配入参来确定关系:

<bean id="book4" class="net.deniro.spring4.bean.Book">
    <constructor-arg index="0" >
        <value>人生的枷锁</value>
    </constructor-arg>
    <constructor-arg index="1" >
        <value>上海译文出版社</value>
    </constructor-arg>
    <constructor-arg index="2" type="double">
        <value>22.5</value>
    </constructor-arg>
</bean>

真正引起歧义的地方在于第 3 个入参,所以我们在此仅需明确指定第 3 个入参的类型就可以取消歧义咯O(∩_∩)O哈哈~

注意:对于由于参数数目相同而类型不同所引起的潜在配置歧义问题, Spring 容器可以正确启动但不会给出报错信息,它将随机采用一个匹配的构造函数来实例化 Bean ,而被选择的构造函数可能并不是我们所希望的 。 因此,在配置时必须小心谨慎,以避免发生意外。

建议显式地指定 index 与 type,因为这样可以明确指定 Bean 中的一个构造函数。

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

如果 Bean 构造函数的入参类型不是基础数据类型,而且入参类型各异,那么可以通过 Java 反射机制获取构造函数的入参类型。

假设在 “书” 中加入了作者实体类:

public Book(String name, Author author) {
    this.name = name;
    this.author = author;
}

/**
 * 作者
 */
private Author author;

Author.java:

public class Author {

    public Author(String name) {
        this.name = name;
    }

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Author{" +
                "name='" + name + '\'' +
                '}';
    }
}

配置文件如下:

<bean id="author" class="net.deniro.spring4.bean.Author">
    <constructor-arg value="毛姆"/>
</bean>
<bean id="book5" class="net.deniro.spring4.bean.Book">
    <constructor-arg>
        <value>人生的枷锁</value>
    </constructor-arg>
    <constructor-arg>
        <ref bean="author"/>
    </constructor-arg>
</bean>

2.5 循环依赖

实例化构造函数配置的 Bean 的前提条件是:这个 Bean 构造函数的入参对象必须已准备就绪(已实例化)。如果存在两个 Bean,它们都使用构造函数的注入方式,并且它们的入参互相引用的话,就会发生循环依赖的问题。

假设 Book 中拥有 Author:

public class Book {

    public Book(String name, Author author) {
        this.name = name;
        this.author = author;
    }

    /**
     * 作者
     */
    private Author author;
...
}

而 Author 中拥有 Book:

public class Author {

    public Author(String name, Book book) {
        this.name = name;
        this.book = book;
    }

    private Book book;
...
}

配置文件中,Book 与 Author 都采用构造函数的依赖注入方式:

<bean id="author10" class="net.deniro.spring4.bean.Author">
    <constructor-arg index="0" value="毛姆"/>
    <constructor-arg index="1" ref="book10"/>

</bean>
<bean id="book10" class="net.deniro.spring4.bean.Book">
    <constructor-arg index="0" value="人生的枷锁"/>
    <constructor-arg index="1" ref="author10"/>
</bean>

这时,启动 Spring 容器,就会抛出以下错误:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'author10': Requested bean is currently in creation: Is there an unresolvable circular reference?
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:347)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:351)
    ... 62 more

因为存在循环依赖的问题。

我们可以把它们的注入方式都改为属性注入方式,就可以解析上述的问题啦O(∩_∩)O哈哈~

3 工厂方法注入

工厂方法是被经常使用到的设计模式,它也是控制反转与单实例设计思想的主要实现方式。它已经成为 Spring 框架的底层基础设施的一部分。

3.1 非静态工厂方法

非静态,即必须实例化工厂类之后,才能调用其方法。

public class BookFactory {

    public Book create() {
        return new Book("面纱", "重庆出版社");
    }
}

工厂类负责创建目标类实例,它对外屏蔽了目标类的实例化细节,返回的类型一般是接口或抽象类的形式。

<bean id="bookFactory" class="net.deniro.spring4.bean.BookFactory"/>
<!-- factory-bean:指定工厂类;factory-method:指定工厂方法-->
<bean id="book11" factory-bean="bookFactory" factory-method="create"/>

3.2 静态工厂方法

很多工厂方法是以静态的方式实现的,因为更易使用,直接调用方法就可以啦。

<!--class:指定工厂类; factory-method:指定静态工厂方法 -->
<bean id="book12" class="net.deniro.spring4.bean.BookFactory" factory-method="createBook"/>

4 如何选择

支持构造函数注入的理由有这些:

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

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

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

建议一般情况下,使用属性注入;必要情况下,使用构造函数注入;只有在不得已的情况下,才使用工厂方法注入。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容