跳至主要內容

Javassm - Spring2 (Bean注册与配置 + 依赖注入)

codejavassmSpring约 3854 字大约 13 分钟

IoC理论基础 2

Bean注册与配置

详细了解一下如何向Spring注册Bean以及Bean的相关配置。

实际上我们的配置文件可以有很多个,并且这些配置文件是可以相互导入的:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
    <import resource="test.xml"/>
</beans>

但是为了简单起见,我们还是从单配置文件开始讲起

配置并注册Bean

首先我们需要知道如何配置Bean并注册。

要配置一个Bean,只需要添加:

<bean/>

但是这样写的话,Spring无法得知我们要配置的Bean到底是哪一个类,所以说我们还得指定对应的类才可以:

<bean class="com.test.bean.Student"/>
alt text
alt text

可以看到类的旁边出现了Bean的图标,表示我们的Bean已经注册成功了,这样,我们就可以根据类型向容器索要Bean实例对象了:

public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    //getBean有多种形式,其中第一种就是根据类型获取对应的Bean
    //容器中只要注册了对应类的Bean或是对应类型子类的Bean,都可以获取到
    Student student = context.getBean(Student.class);
    student.hello();
}

表示,当我们尝试获取该类的bean来创建对象时,容器会去找注册的bean是否有对应的类或者其类的子类。

创建对象的类存在多个对应bean

不过在有些时候,Bean的获取可能会出现歧义,我们可以来分别注册两个子类的Bean:

public class ArtStudent extends Student{
    public void art(){
        System.out.println("我爱画画");
    }
}
public class SportStudent extends Student{
        public void sport(){
        System.out.println("我爱运动");
    }
}
<bean class="com.test.bean.ArtStudent"/>
<bean class="com.test.bean.SportStudent"/>

但是此时我们在创建Student类时,获取Bean时会找到有两个子类都满足:

Student student = context.getBean(Student.class);
student.hello();

运行时得到如下报错:

alt text
alt text

这里出现了一个Bean定义不唯一异常,很明显,因为我们需要的类型是Student,但是此时有两个Bean定义都满足这个类型,它们都是Student的子类,此时IoC容器不知道给我们返回哪一个Bean,所以就只能抛出异常了。

因此,如果我们需要一个Bean并且使用类型进行获取,那么必须要指明类型并且不能出现歧义:

ArtStudent student = context.getBean(ArtStudent.class);
student.art();

name属性

那要是两个Bean的类型都是一样的呢?

<bean class="com.test.bean.Student"/>
<bean class="com.test.bean.Student"/>

这种情况下,就无法使用Class来进行区分了,除了为Bean指定对应类型之外,我们也可以为Bean指定一个名称用于区分:

<bean name="art" class="com.test.bean.ArtStudent"/>
<bean name="sport" class="com.test.bean.SportStudent"/>

name属性就是为这个Bean设定一个独一无二的名称(id属性也可以,跟name功能相同,但是会检查命名是否规范,否则会显示黄标)

不同的Bean名字不能相同,否则报错:

<bean name="a" class="com.test.bean.Student"/>
<bean name="b" class="com.test.bean.Student"/>

这样,这两个Bean我们就可以区分出来了:

Student student = (Student) context.getBean("a");
student.hello();

虽然目前这两Bean定义都是一模一样的,也没什么区别,但是这确实是两个不同的Bean,只是类型一样而已,之后我们还可以为这两个Bean分别设置不同的其他属性。

alias别名

我们可以给Bean起名字,也可以起别名

<bean name="a" class="com.test.bean.Student"/>
<alias name="a" alias="test"/>

这样,我们使用别名也是可以拿到对应的Bean的:

Student student = (Student) context.getBean("test");
student.hello();

IoC容器对象生成原理

那么现在又有新的问题了,IoC容器创建的Bean是只有一个还是每次索要的时候都会给我们一个新的对象?

我们现在在主方法中连续获取两次Bean对象:

Student student1 = context.getBean(Student.class);
Student student2 = context.getBean(Student.class);
System.out.println(student1 == student2);   
//默认为单例模式,对象始终为同一个

我们发现,最后得到的结果为true

那么说明每次从IoC容器获取到的对象,始终都是同一个,默认情况下,通过IoC容器进行管理的Bean都是单例模式的,这个对象只会被创建一次。

如果我们希望每次拿到的对象都是一个新的,我们也可以将其作用域进行修改

这里一共有两种作用域,第一种是singleton,默认情况下就是这一种,当然还有prototype,表示为原型模式,这种模式每次得到的对象都是一个新的:

Student student1 = context.getBean(Student.class);  
//原型模式下,对象不再始终是同一个了
Student student2 = context.getBean(Student.class);
System.out.println(student1 == student2);

实际上,当Bean的作用域为单例模式时,那么它会在一开始(容器加载配置时)就被创建,我们之后拿到的都是这个对象。

而处于原型模式下,只有在获取时才会被创建,也就是说,单例模式下,Bean会被IoC容器存储,只要容器没有被销毁,那么此对象将一直存在,而原型模式才是相当于在要用的时候直接new了一个对象,并不会被保存。

懒加载 lazy-init

当然,如果我们希望单例模式下的Bean不用再一开始就加载,而是一样等到需要时再加载(加载后依然会被容器存储,之后一直使用这个对象了,不会再创建新的)我们也可以开启懒加载:

<bean class="com.test.bean.Student" lazy-init="true"/>

开启懒加载后,只有在真正第一次使用时才会创建对象。

规定加载顺序 depends-on

因为单例模式下Bean是由IoC容器加载,但是加载顺序我们并不清楚,如果我们需要维护Bean的加载顺序(比如某个Bean必须要在另一个Bean之前创建)那么我们可以使用depends-on来设定前置加载Bean,这样被依赖的Bean一定会在之前加载,比如Teacher应该在Student之前加载:

<bean name="teacher" class="com.test.bean.Teacher"/>
<bean name="student" class="com.test.bean.Student" depends-on="teacher"/>

这样就可以保证Bean的加载顺序了。

依赖注入

依赖注入(Dependency Injection, DI)是一种设计模式,也是Spring框架的核心概念之一。

现在我们已经了解了如何注册和使用一个Bean,但是这样还远远不够,我们使用容器的目标之一是消除类之间的强关联.

其实现的主要功能是,IoC容器在创建对象时,会根据我们提供的信息作为对象的属性来提前注入到对象中

比如现在有一个教师接口:

public interface Teacher {
    void teach();
}

具体的实现有两个:

public class ArtTeacher implements Teacher{
    @Override
    public void teach() {
        System.out.println("我是美术老师,我教你画画!");
    }
}
public class ProgramTeacher implements Teacher{
    @Override
    public void teach() {
        System.out.println("我是编程老师,我教你学Golang!");
    }
}

我们的学生一开始有一个老师教他,比如美术老师:

public class Student {
    private Teacher teacher = new ArtTeacher();   
    //在以前,如果我们需要制定哪个老师教我们,直接new创建对应的对象就可以了
    public void study(){
        teacher.teach();
    }
}

但是我们发现,如果美术老师不教了,现在来了一个其他的老师教学生,那么就需要去修改Student类的定义:

public class Student {
    private Teacher teacher = new ProgramTeacher();
    ...
}

可以想象一下,如果现在冒出来各种各样的类都需要这样去用Teacher,那么一旦Teacher的实现发生变化,会导致我们挨个对之前用到Teacher的类进行修改,这就很难受了。

而有了依赖注入之后,Student中的Teacher成员变量,可以由IoC容器来选择一个合适的Teacher对象进行赋值

也就是说,IoC容器在创建对象时,需要将我们预先给定的属性注入到对象中

property 标签

我们可以使用property标签来实现,将bean标签展开:

<bean name="mathTeacher" class="com.test.entity.MathTeacher"/>
<bean name="artTeacher" class="com.test.entity.ArtTeacher"/>
<bean class="com.test.entity.Student">
    <property name="teacher" ref="artTeacher"/>
</bean>
  • name属性表示该注册的bean对应的对象的指定属性(name值)
  • ref属性表示赋给这个属性的值来自另一个对应的bean注册对象中
  • value属性表示赋给这个属性的值为value对应的值

同时我们还需要修改一下Student类,依赖注入要求对应的属性必须有一个set方法:

public class Student {
    private Teacher teacher;
    //要使用依赖注入,我们必须提供一个set方法(无论成员变量的访问权限是什么)命名规则依然是驼峰命名法
    public void setTeacher(Teacher teacher) {
        this.teacher = teacher;
    }
    ...
}
alt text
alt text

使用property来指定需要注入的值是一个Bean,这里我们选择ProgramTeacher,那么在使用时,Student类中的得到的就是这个Bean的对象了:

Student student = context.getBean(Student.class);
student.study();
alt text
alt text

可以看到,现在我们的Java代码中,没有出现任何的具体实现类信息(ArtTeacher、ProgramTeacher都没出现)取而代之的是那一堆xml配置

这样,就算我们切换老师的实现为另一个类,也不用去调整代码,只需要变动一下Bean的类型就可以:

<!--  只需要修改这里的class即可,现在改为ArtTeacher  -->
<bean name="mathTeacher" class="com.test.entity.MathTeacher"/>
<bean name="artTeacher" class="com.test.entity.ArtTeacher"/>
<bean class="com.test.entity.Student">
    <property name="teacher" ref="mathTeacher"/>
</bean>

这样,这个Bean的class就变成了新的类型,并且我们不需要再去调整其他位置的代码

通过依赖注入,是不是开始逐渐感受到Spring为我们带来的便利了?

当然,依赖注入并不一定要注入其他的Bean,也可以是一个简单的值:

<bean name="student" class="com.test.bean.Student">
    <property name="name" value="卢本伟"/>
</bean>

直接使用value可以直接传入一个具体值。

构造注入 constructor-arg 标签

实际上,在很多情况下,类中的某些参数是在构造方法中就已经完成初始化,而不是创建之后,比如:

public class Student {
    private final Teacher teacher;   
    //构造方法中完成,所以说是一个final变量

    public Student(Teacher teacher){  
        //Teacher属性是在构造方法中完成的初始化
        this.teacher = teacher;
    }
    ...
}

很明显,是因为我们修改了构造方法,IoC容器默认只会调用无参构造,所以,我们需要指明一个可以用的构造方法

我们展开bean标签,添加一个constructor-arg标签:

<bean name="mathTeacher" class="com.test.entity.MathTeacher"/>
<bean name="artTeacher" class="com.test.entity.ArtTeacher"/>
<bean class="com.test.entity.Student">
    <constructor-arg name="teacher" ref="artTeacher"/>
</bean>

这里的constructor-arg就是构造方法的一个参数,这个参数可以写很多个,会自动匹配符合里面参数数量的构造方法,这里匹配的就是我们刚刚编写的需要一个参数的构造方法。

通过这种方式,我们也能实现依赖注入,只不过现在我们将依赖注入的时机提前到了对象构造时

多种构造函数情况

现在我们的Student类中是这样定义的:

public class Student {
    private final String name;
    public Student(String name){
        System.out.println("我是一号构造方法");
        this.name = name;
    }

    public Student(int age){
        System.out.println("我是二号构造方法");
        this.name = String.valueOf(age);
    }
}

此时我们希望使用的是二号构造方法,那么怎么才能指定呢?

有2种方式,我们可以给标签添加类型:

<constructor-arg value="1" type="int"/>

也可以指定为对应的参数名称:

<constructor-arg value="1" name="age"/>

如果是参数数量不同,就根据参数数量来区分就行

反正只要能够保证我们指定的参数匹配到目标构造方法即可。

集合类型情况

现在我们的类中出现了一个比较特殊的类型,它是一个集合类型:

public class Student {
    private List<String> list;

    public void setList(List<String> list) {
        this.list = list;
    }
}

对于这种集合类型,有着特殊的支持:

<bean name="student" class="com.test.bean.Student">
    <!--  对于集合类型,我们可以直接使用标签编辑集合的默认值  -->
    <property name="list">
        <list>
            <value>AAA</value>
            <value>BBB</value>
            <value>CCC</value>
        </list>
    </property>
</bean>

不仅仅是List,Map、Set这类常用集合类包括数组在内,都是支持这样编写的,比如Map类型,我们也可以使用entry来注入:

<bean name="student" class="com.test.bean.Student">
    <property name="map">
        <map>
            <entry key="语文" value="100.0"/>
            <entry key="数学" value="80.0"/>
            <entry key="英语" value="92.5"/>
        </map>
    </property>
</bean>

至此,我们就已经完成了两种依赖注入的学习:

  • Setter依赖注入:通过成员属性对应的set方法完成注入。
  • 构造方法依赖注入:通过构造方法完成注入。

自动装配 autowire 属性

在之前,如果我们需要使用依赖注入的话,我们需要对property参数进行配置:

<bean name="student" class="com.test.bean.Student">
    <property name="teacher" ref="teacher"/>
</bean>

但是有些时候为了方便,我们也可以开启自动装配

自动装配就是让IoC容器自己去寻找需要填入的值,我们只需要将set方法提供好就可以了,这里需要添加autowire属性:

<bean name="student" class="com.test.bean.Student" autowire="byType"/>

byName + byType

autowire属性最普通的有两个值

一个是byName,还有一个是byType

顾名思义,一个是根据类型(即对应的类或者其子类)去寻找合适的Bean自动装配

还有一个是根据名字(根据setxxx中的xxx)去找,这样我们就不需要显式指定property了。

此时set方法旁边会出现一个自动装配图标,效果和上面是一样的。

constructor

对于使用构造方法完成的依赖注入,也支持自动装配,我们只需要将autowire修改为:

<bean name="student" class="com.test.bean.Student" autowire="constructor"/>

这样,我们只需要提供一个对应参数的构造方法就可以了(这种情况默认也是byType寻找的)

这样同样可以完成自动注入

候选名单 autowire-candidate

自动化的东西虽然省事,但是太过机械,有些时候,自动装配可能会遇到一些问题,比如出现了下面的情况:

alt text
alt text

此时,由于autowire的规则为byType,存在两个候选Bean,但是我们其实希望ProgramTeacher这个Bean在任何情况下都不参与到自动装配中,此时我们就可以将它的自动装配候选关闭:

<bean name="teacher" class="com.test.bean.ArtTeacher"/>
<bean name="teacher2" class="com.test.bean.ProgramTeacher" autowire-candidate="false"/>
<bean name="student" class="com.test.bean.Student" autowire="byType"/>

autowire-candidate设定false时,这个Bean将不再作为自动装配的候选Bean,此时自动装配候选就只剩下一个唯一的Bean了,报错消失,程序可以正常运行。

优先选择 primary

除了这种方式,我们也可以设定primary属性,表示这个Bean作为主要的Bean,当出现歧义时,也会优先选择:

<bean name="teacher" class="com.test.bean.ArtTeacher" primary="true"/>
<bean name="teacher2" class="com.test.bean.ProgramTeacher"/>
<bean name="student" class="com.test.bean.Student" autowire="byType"/>
上次编辑于: