Spring之装配Bean

目录

1  依赖注入的3种方式

1.1 构造器注入

1.2 使用setter注入

1.3 接口注入

2 装配Bean概述

3 通过XML配置装配Bean

3.1 装配简易值

3.2 装配集合

3.3 命名空间装配

4 通过注解装配Bean

4.1 使用@Component装配Bean

4.2 自动装配——@Autowired

4.3 自动装配的歧义性(@Primary和@Qualifier)

4.3.1 注解@Primary

4.3.2 注解@Qualifier

4.4 装在带有参数的构造方法类

4.5 使用@Bean装配Bean

4.6 注解自定义Bean的初始化和销毁方法

5 装配的混合使用

6 使用Profile

6.1 使用注解@Profile配置

6.2 使用XML定义Profile

6.3 启动Profile

7 加载属性(properties)文件

7.1 使用注解方式加载属性文件

7.2 使用XML方式加载属性文件

8 条件化装配Bean

9 Bean的作用域

10 使用Spring表达式(Spring EL)

10.1 Spring EL相关的类

10.2 Bean的属性和方法

10.3 使用类的静态常量和方法

10.4 Spring EL运算


上文介绍了Spring的简介以及IoC概念和IoC容器,详情请看《Spring之简介及其IoC的概念》。本文讲解如何将Bean注入到Spring IoC容器中。


1  依赖注入的3种方式

在实际环境中实现IoC容器的方式主要分为两大类,一类是依赖查找,依赖查找是通过资源定位,把对应的资源查找回来;另一类则是依赖注入,而Spring主要使用的是依赖注入。一般而言,依赖注入可以分为3种方式:

  • 构造器注入。
  • setter注入。
  • 接口注入。

构造器注入和setter注入是主要的方式,而接口注入是从别的地方注入的方式,比如在Web工程中,配置的数据源往往是通过服务器(比如Tomcat)来配置的,这个时候可以用JNDI的形式通过接口将它注入Spring IoC容器中来。下面对它们进行详细讲解。

1.1 构造器注入

构造器注入依赖于构造方法实现,而构造方法可以是有参数的或者是无参数的。在大部分的情况下,我们都是通过类的构造方法来创建类对象,Spring也可以采用反射的方式,通过使用构造方法来完成注入,这就是构造器注入的原理。

为了让Spring完成对应的构造注入,我们有必要来描述具体的类、构造方法并设置对应的参数,这样Spring就会通过对应的信息用反射的形式创建对象,比如之前我们多次谈到的角色类,现修改为下面的形式:

public class Role {

    private Long   id;
    private String roleName;
    private String note;

    public Role(String roleName, String note) {
        this.roleName = roleName;
        this.note = note;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

这个时候是没有办法利用无参数的构造方法来创建对象的,为了使Spring能够正确创建这个对象,可以像下面代码这样去做:

        <bean id="role1" class="com.hys.spring.example2.pojo.Role">
		<constructor-arg index="0" value="初级工程师" />
		<constructor-arg index="1" value="基层开发人员" />
	</bean>

constructor-arg元素用于定义类构造方法的参数,其中index用于定义参数的位置,而value则是设置值,通过这样的定义,Spring便知道Role(String,String)这样的构造方法来创建对象了。

这样注入还是比较简单的,但是缺点也很明显,由于这里的参数比较少,所以可读性还是不错的,但是如果参数很多,那么这种构造方法就比较复杂了,这个时候应该考虑setter注入。

1.2 使用setter注入

setter注入是Spring中最主流的注入方式,它利用Java Bean规范所定义的setter方法来完成注入,灵活且可读性高。它消除了使用构造器注入时出现多个参数的可能性,首先可以把构造方法声明为无参数的,然后使用setter注入为其设置对应的值,其实也是通过Java反射技术得以实现的。这里往上述Role类中加入一个没有参数的构造方法,然后做下述代码的配置:

        <bean id="role2" class="com.hys.spring.example2.pojo.Role">
		<property name="roleName" value="高级工程师" />
		<property name="note" value="架构师" />
	</bean>

这样Spring就会通过反射调用没有参数的构造方法生成对象,同时通过反射对应的setter注入配置的值了。这种方式是Spring最为主要的方式,在实际工作中使用广泛。

1.3 接口注入

有些时候资源并非来自于自身系统,而是来自于外界,比如数据库连接资源完全可以在Tomcat下配置,然后通过JNDI的形式来获取它,这样数据库连接资源是属于开发工程外的资源,这个时候我们可以采用接口注入的形式来获取它,比如在Tomcat中可以配置数据源,又如在Eclipse中配置了Tomcat后,可以打开服务器的context.xml文件,在这个文件元素context中加入自己的一个资源,代码如下:

        <?xml version="1.0" encoding="UTF-8"?>
	<Context>
		<Resource name="jdbc/ssm" auth="Container"
			type="javax.sql.DataSource" driverClassName="com.mysql.jdbc.Driver"
			url="jdbc:mysql://localhost:3306/ssm?zeroDateBehavior=convertToNull"
			username="root" password="root" />
	</Context>

如果Tomcat的Web工程使用了Spring,那么可以通过Spring的机制,用JNDI获取Tomcat启动的数据库连接池,代码如下:

        <bean id="dataSource"
		class="org.springframework.jndi.JndiObjectFactoryBean">
		<property name="jndiName">
			<value>java:comp/env/jdbc/ssm</value>
		</property>
	</bean>

这样就可以在Spring的IoC容器中获得Tomcat所管理的数据库连接池了,这就是一种接口注入的形式。


2 装配Bean概述

在大部分场景下,我们都会使用ApplicationContext的具体实现类,因为对应的Spring IoC容器功能相对强大。而在Spring中提供了3种方法进行配置:

  • 在XML中显示配置。
  • 在Java的接口和类中实现配置。
  • 隐式Bean的发现机制和自动装配原则。

在现实的工作中,这3种方式都会被用到,并且在学习和工作中常常混合使用,所以本文会对这3种方式进行详细的讨论。只是读者需要明确3种方式的优先级,也就是我们应该怎么选择、使用哪种方式来把Bean发布到Spring IoC容器中。以下是一些建议:

  1. 基于约定优于配置的原则,最优先的应该是通过隐式Bean的发现机制和自动装配的原则。这样的好处是减少程序开发者的决定权,简单又不失灵活。
  2. 在没有办法使用自动装配原则的情况下应该优先考虑Java接口和类中实现配置,这样的好处是避免XML配置的泛滥,也更为容易。这种场景典型的例子是一个父类有多个子类,比如学生类有两个子类:男学生类和女学生类,通过IoC容器初始化一个学生类,容器将无法知道使用哪个子类去初始化,这个时候可以使用Java的注解配置去指定。
  3. 在上述方法都无法使用的情况下,那么只能选择XML来配置Spring IoC容器。由于现实工作中常常用到第三方的类库,有些类并不是我们开发的,我们无法修改里面的代码,这个时候就通过XML的方式配置使用了。

通俗来讲,当配置的类是你自身正在开发的工程,那么应该考虑Java配置为主,而Java配置又分为自动装配和Bean名称配置。在没有歧义的基础上,优先使用自动装配,这样就可以减少大量的XML配置。如果所需配置的类并不是你的工程开发的,那么建议使用XML的方式。


3 通过XML配置装配Bean

使用XML装配Bean需要定义对应的XML,这里需要引入对应的XML模式(XSD)文件,这些文件会定义配置Spring Bean的一些元素,如下:

<?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 http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
	<!-- Spring Bean配置代码 -->
</beans>

在上述代码中引入了一个beans的定义,它是一个根元素,而XSD文件也被引入了,这样它所定义的元素将可以定义对应的Spring Bean。

3.1 装配简易值

这里先讨论那些简易的装配方法,先来一个最简单的装配,代码如下:

        <bean id="role2" class="com.hys.spring.example2.pojo.Role">
		<property name="id" value="1" />
		<property name="roleName" value="高级工程师" />
		<property name="note" value="架构师" />
	</bean>

这是一个简易的配置,简单解释一下:

  • id属性是Spring找到的这个Bean的编号,不过id不是一个必需的属性,如果没有声明它,那么Spring将会采用“全限定名#{number}”的格式生成编号。如果只声明一个这样的类,而没有声明id="role2",那么Spring为其生成的编号就是"com.hys.spring.example2.pojo.Role#0"。当它第二次声明没有id属性的Bean时,编号就是"com.hys.spring.example2.pojo.Role#1",但是一般我们都会选择自己定义id,因为自动生成的id会比较繁琐。
  • class显然是一个类全限定名。
  • property元素是定义类的属性,其中name属性定义的是属性名称,而value是其值。

这样的定义很简单,但是有时候需要注入一些自定义的类,比如之前的果汁制造器例子,它需要原料信息和饮品店共同完成,于是可能要先定义原料的信息,然后在制造器中引用原料,代码如下:

        <bean id="source" class="com.hys.spring.example1.pojo.Source">
		<property name="fruit" value="橙汁"></property>
		<property name="sugar" value="少糖"></property>
		<property name="size" value="大杯"></property>
	</bean>

	<bean id="juiceMaker2"
		class="com.hys.spring.example1.pojo.JuiceMaker2" init-method="init"
		destroy-method="myDestroy">
		<property name="beverageShop" value="贡茶"></property>
		<property name="source" ref="source"></property>
	</bean>

这里先定义了一个id为source的Bean,然后在制造器中通过ref属性来引用对应的Bean,而source正是之前定义的Bean的id,这样就可以相互引用了。

上面只是一些很简单的使用方法,有时候会将对应集合类注入,下一节将讨论它们。

3.2 装配集合

有些时候要做一些复杂的装配工作,比如Set、Map、List和Properties等。为了介绍它们,先定义个Bean,代码如下:

public class ComplexAssembly {

    private Long                id;
    private List<String>        list;
    private Map<String, String> map;
    private Properties          props;
    private Set<String>         set;
    private String[]            array;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public List<String> getList() {
        return list;
    }

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

    public Map<String, String> getMap() {
        return map;
    }

    public void setMap(Map<String, String> map) {
        this.map = map;
    }

    public Properties getProps() {
        return props;
    }

    public void setProps(Properties props) {
        this.props = props;
    }

    public Set<String> getSet() {
        return set;
    }

    public void setSet(Set<String> set) {
        this.set = set;
    }

    public String[] getArray() {
        return array;
    }

    public void setArray(String[] array) {
        this.array = array;
    }
}

这个Bean没有任何的业务含义,只是为了介绍如何装配这些常用的集合类,为此可以如同下述代码这样装配这些属性:

        <bean id="complexAssemply"
		class="com.hys.spring.example2.pojo.ComplexAssembly">
		<property name="id" value="1" />
		<property name="list">
			<list>
				<value>value-list-1</value>
				<value>value-list-2</value>
				<value>value-list-3</value>
			</list>
		</property>
		<property name="map">
			<map>
				<entry key="key1" value="value-key-1" />
				<entry key="key2" value="value-key-2" />
				<entry key="key1" value="value-key-3" />
			</map>
		</property>
		<property name="props">
			<props>
				<prop key="prop1">value-prop-1</prop>
				<prop key="prop2">value-prop-2</prop>
				<prop key="prop3">value-prop-3</prop>
			</props>
		</property>
		<property name="set">
			<set>
				<value>value-set-1</value>
				<value>value-set-2</value>
				<value>value-set-3</value>
			</set>
		</property>
		<property name="array">
			<array>
				<value>value-array-1</value>
				<value>value-array-2</value>
				<value>value-array-3</value>
			</array>
		</property>
	</bean>

当然这里的装配主要集中在比较简单的String类型上,其主要的目的是告诉大家如何装配一些简易的数据到集合中。

  • List属性为对应的<list>元素进行装配,然后通过多个<value>元素设值。
  • Map属性为对应的<map>元素进行装配,然后通过多个<entry>元素设值,只是entry包含一个键(key)和一个值(value)的设置。
  • Properties属性为对应的<props>元素进行装配,通过多个<prop>元素设置,只是prop元素有一个必填属性key,然后可以设置值。
  • Set属性为对应的<set>元素进行装配,然后通过多个<value>元素设值。
  • 对于数组而言,可以使用<array>设置值,然后通过多个<value>元素设值。

从上面可以看到对字符串的各个集合的装载,但是有些时候可能需要更为复杂的装载,比如一个List可以是一个系列类的对象,又如一个Map集合类,键可以是一个类对象,而值也要是一个类对象,这些也是Java中常常可以看到的。为此先建两个POJO,代码如下:

public class Role {

    private Long   id;
    private String roleName;
    private String note;

    public Role() {}

    public Role(Long id, String roleName, String note) {
        this.id = id;
        this.roleName = roleName;
        this.note = note;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}
public class User {

    private Long   id;
    private String userName;
    private String note;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

为了测试上面的用户和角色,再来建一个稍微复杂的POJO,装配用户和角色类,代码如下:

public class UserRoleAssembly {

    private Long            id;
    private List<Role>      list;
    private Map<Role, User> map;
    private Set<Role>       set;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public List<Role> getList() {
        return list;
    }

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

    public Map<Role, User> getMap() {
        return map;
    }

    public void setMap(Map<Role, User> map) {
        this.map = map;
    }

    public Set<Role> getSet() {
        return set;
    }

    public void setSet(Set<Role> set) {
        this.set = set;
    }
}

这里可以看到,对于List、Map、和Set等集合类使用的是类对象,不过不必担心,Spring IoC容器提供了对应的配置方法,代码如下:

        <bean id="role1" class="com.hys.spring.example2.pojo.Role">
		<property name="id" value="1" />
		<property name="roleName" value="role_name_1" />
		<property name="note" value="role_note_1" />
	</bean>

	<bean id="role2" class="com.hys.spring.example2.pojo.Role">
		<property name="id" value="2" />
		<property name="roleName" value="role_name_2" />
		<property name="note" value="role_note_2" />
	</bean>

	<bean id="user1" class="com.hys.spring.example2.pojo.User">
		<property name="id" value="1" />
		<property name="userName" value="user_name_1" />
		<property name="note" value="user_note_1" />
	</bean>

	<bean id="user2" class="com.hys.spring.example2.pojo.User">
		<property name="id" value="2" />
		<property name="userName" value="user_name_2" />
		<property name="note" value="user_note_2" />
	</bean>

	<bean id="userRoleAssembly"
		class="com.hys.spring.example2.pojo.UserRoleAssembly">
		<property name="id" value="1" />
		<property name="list">
			<list>
				<ref bean="role1" />
				<ref bean="role2" />
			</list>
		</property>
		<property name="map">
			<map>
				<entry key-ref="role1" value-ref="user1" />
				<entry key-ref="role2" value-ref="user2" />
			</map>
		</property>
		<property name="set">
			<set>
				<ref bean="role1" />
				<ref bean="role2" />
			</set>
		</property>
	</bean>

这里先定义了两个角色Bean(role1和role2)和两个用户Bean(user1和user2),它们和之前的定义并没有什么不同,只是后面的定义略微不一样而已。其中:

  • List属性使用<list>元素定义注入,使用多个<ref>元素的bean属性去引用之前定义好的Bean。
  • Map属性使用<map>元素定义注入,使用多个<entry>元素的key-ref属性去引用之前定义好的Bean作为键,而用value-ref属性去引用之前定义好的Bean作为值。
  • Set属性使用<set>元素定义注入,使用多个<ref>元素的bean属性去引用之前定义好的Bean。

至此,我们学习了如何装配简单值和集合值,只是在Spring中还能通过命名空间去定义Bean。

3.3 命名空间装配

除上述的配置之外,Spring还提供了对应的命名空间的定义,只是在使用命名空间的时候要先引入对应的命名空间和XML模式(XSD)文件。比如,继续使用之前关于角色类的定义,我们可以通过如下述代码所示的定义,使用命名空间的方法,将角色类实例注册在Spring IoC容器中:

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:c="http://www.springframework.org/schema/c"
	xmlns:p="http://www.springframework.org/schema/p"
	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-4.0.xsd">

	<bean id="role1" class="com.hys.spring.example2.pojo.Role"
		c:_0="1" c:_1="role_name_1" c:_2="role_note_1" />

	<bean id="role2" class="com.hys.spring.example2.pojo.Role"
		p:id="2" p:roleName="role_name_2" p:note="role_note_2" />

</beans>

这里讨论一下这段代码:

  • 注意第2行和第3行的代码,它们定义了XML的命名空间,这样才能在内容里面使用c和p这样的前缀定义。
  • id为role1的角色定义,c:_0代表构造方法的第1个参数,c:_1代表的是第2个,c:_2代表的是第3个,依此类推。
  • id为role2的角色定义,p代表引用属性,其中p:id="2"以2为值,使用setId方法设置,roleName、note属性也是一样的道理。

以上就是简单的设值方式,而现实中也可能需要为属性设值,正如上述代码的UserRoleAssembly类,可以借助引入XML文档(XSD)文件的方法,把UserRoleAssembly类实例注册给Spring IoC容器,代码如下:

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:c="http://www.springframework.org/schema/c"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:util="http://www.springframework.org/schema/util"
	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-4.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

	<bean id="role1" class="com.hys.spring.example2.pojo.Role"
		c:_0="1" c:_1="role_name_1" c:_2="role_note_1" />

	<bean id="role2" class="com.hys.spring.example2.pojo.Role"
		p:id="2" p:roleName="role_name_2" p:note="role_note_2" />

	<bean id="user1" class="com.hys.spring.example2.pojo.User"
		p:id="1" p:userName="user_name_1" p:note="user_note_1" />

	<bean id="user2" class="com.hys.spring.example2.pojo.User"
		p:id="2" p:userName="user_name_2" p:note="user_note_2" />

	<util:list id="list">
		<ref bean="role1" />
		<ref bean="role2" />
	</util:list>

	<util:map id="map">
		<entry key-ref="role1" value-ref="user1" />
		<entry key-ref="role2" value-ref="user2" />
	</util:map>

	<util:set id="set">
		<ref bean="role1" />
		<ref bean="role2" />
	</util:set>

	<bean id="userRoleAssembly"
		class="com.hys.spring.example2.pojo.UserRoleAssembly" p:id="1"
		p:list-ref="list" p:map-ref="map" p:set-ref="set" />

</beans>

注意引入的命名空间和XSD文件,然后定义了两个角色类对象(role1和role2)和两个用户类对象(user1和user2)。通过命名空间util来定义List、Map和Set对象,这些在装配集合的时候也论述过,跟着就是定义id为userRoleAssembly的Bean,这里的list-ref代表采用List属性,但是其值引用上下文定义好的Bean,这里显然就是util命名空间定义的List,同理Map和Set也是如此。

从上面可知,在使用XML定义的时候,无论使用原始的配置,还是使用命名空间定义都是允许的。


4 通过注解装配Bean

通过上面的学习,读者已经知道如何使用XML的方式来装配Bean,但是更多的时候已经不再推荐使用XML的方式来装配Bean,更多的时候会考虑使用注解(annotation)的方式来装配Bean。使用注解的方式可以减少XML的配置,注解功能更为强大,它既能实现XML的功能,也提供了自动装配的功能,采用了自动装配后,程序员所需要做的决断就少了,更加有利于对程序的开发,这就是“约定优于配置”的开发原则。

在Spring中,它提供了两种方式来让Spring IoC容器发现Bean。

  • 组件扫描:通过定义资源的方式,让Spring IoC容器扫描对应的包,从而把Bean装配进来。 
  • 自动装配:通过注解定义,使得一些依赖关系可以通过注解完成。

通过扫描和自动装配,大部分的工程都可以用Java配置完成,而不是XML,这样可以有效减少配置和引入大量XML,它解决了在Spring 3之前的版本需要大量的XML配置的问题,这些问题曾被许多开发者诟病,所以目前注解已经成为Spring开发的主流。但是这并不意味着XML配置被淘汰了,不使用XML也存在着一定的弊端,比如系统存在多个公共的配置文件(比如多个properties和XML文件),如果写在注解里,那么那些公共资源的配置就会比较分散了,这样不利于统一的管理,又或者一些类来自于第三方,而不是我们系统开发的配置文件,这时利用XML的方式来完成会更加明确一些,因此目前企业所流行的方式是:以注解为主,以XML为辅。

4.1 使用@Component装配Bean

首先定义一下POJO,代码如下:

package com.hys.spring.example3.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component(value = "role")
public class Role {

    @Value("1")
    private Long   id;
    @Value("role_name_1")
    private String roleName;
    @Value("role_note_1")
    private String note;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}
  • 注解@Component代表Spring IoC会把这个类扫描生成Bean实例,而其中的value属性代表这个类在Spring中的id,这就相当于XML方式定义的Bean的id,也可以简写成@Component("role"),甚至直接写成@Component,对于不写的,Spring IoC容器就默认类名,但是以首字母小写的形式作为id,为其生成对象,配置到容器中。
  • 注解@Value代表的是值的注入,这里只是简单注入一些值,其中id是一个long类型,注入的时候Spring会为其转化类型。

现在有了这个类,但是还不能进行测试,因为Spring IoC并不知道需要去哪里扫描对象,这个时候可以使用一个Java Config类来去告诉它,代码如下:

package com.hys.spring.example3.pojo;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class PojoConfig {
}

这个类十分简单,几乎没有逻辑,但是要注意:

  • 包名和Role类代码保持一致。
  • @ComponentScan代表进行扫描,默认是扫描当前包的路径,POJO的包名和它保持一致才能扫描,否则是没有的。

有了上述代码,就可以通过Spring定义好的Spring IoC容器的实现类——AnnotationConfigApplicationContext来生成IoC容器了。它十分简单,代码如下:

package com.hys.spring.example3.test;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;

import com.hys.spring.example3.pojo.PojoConfig;
import com.hys.spring.example3.pojo.Role;

public class Test {

    public static void main(String[] args) {
        AbstractApplicationContext context = new AnnotationConfigApplicationContext(PojoConfig.class);
        Role role = context.getBean(Role.class);
        System.out.println(role.getId());
    }
}

这里可以看到使用了AnnotationConfigApplicationContext类来初始化Spring IoC容器,它的配置项是上述的PojoConfig类。这样Spring IoC就会根据注解的配置去解析对应的资源,来生成IoC容器了。

由此可以看到两个明显的弊端:其一,对于@ComponentScan注解,它只是扫描所在包的Java类,但是更多的时候真正需要的是可以扫描所指定的类;其二,上面只注入了一些简单的值,而没有注入对象,同样在现实的开发中可以注入对象是十分重要的,也是常见的场景。这里先解决第1个问题,第2个问题后面会讲解。

@ComponentScan存在着两个配置项:第1个是basePackages,它是由base和package两个单词组成的,而package还使用了复数,意味着它可以配置一个Java包的数组,Spring会根据它的配置扫描对应的包和子包,将配置好的Bean装配进来;第2个是basePackageClasses,它由base、package和class三个单词组成的,采用复数,意味着它可以配置多个类,Spring会根据配置的类所在的包,为包和子包进行扫描装配对应配置的Bean。

为了更好地验证@ComponentScan的两个配置项,首先定义一个接口RoleService,代码如下:

package com.hys.spring.example3.service;

import com.hys.spring.example3.pojo.Role;

public interface RoleService {

    public void printRoleInfo(Role role);
}

使用接口来编写一些操作类是Spring所推荐的,它可以将定义和实现相分离,这样就更为灵活了。对于一个接口而言,这里开发了一个实现类,代码如下:

package com.hys.spring.example3.impl;

import org.springframework.stereotype.Component;

import com.hys.spring.example3.pojo.Role;
import com.hys.spring.example3.service.RoleService;

@Component
public class RoleServiceImpl implements RoleService {

    @Override
    public void printRoleInfo(Role role) {
        System.out.println("id=" + role.getId());
        System.out.println("roleName=" + role.getRoleName());
        System.out.println("note=" + role.getNote());
    }
}

这里的@Component表明它是一个Spring所需要的Bean,而且也实现了对应的RoleService接口所定义的printRoleInfo方法。为了装配RoleServiceImpl和上述的Role的两个Bean,需要给@ComponentScan注解加上对应的配置,代码如下:

package com.hys.spring.example3.pojo;

import org.springframework.context.annotation.ComponentScan;

import com.hys.spring.example3.impl.RoleServiceImpl;

@ComponentScan(basePackageClasses = { Role.class, RoleServiceImpl.class })
//@ComponentScan(basePackages = { "com.hys.spring.example3.pojo", "com.hys.spring.example3.impl" })
//@ComponentScan(basePackages = { "com.hys.spring.example3.pojo", "com.hys.spring.example3.impl" }, basePackageClasses = { Role.class, RoleServiceImpl.class })
public class ApplicationConfig {
}
  • 这是对扫描包的定义,可以采用任意一个@ComponentScan来定义,也可以取消代码中的注释。
  • 如果采用多个@ComponentScan来定义对应的包,但是每定义一个@ComponentScan,Spring就会为所定义的类生成一个新的对象,也就是所配置的Bean将会生成多个实例,这往往不是我们需要的。
  • 对于已定义了basePackages和basePackageClasses的@ComponentScan,Spring会进行专门的区分,也就是说在同一个@ComponentScan中即使重复定义相同的包或者存在其子包定义,也不会造成因同一个Bean的多次扫描,而导致一次配置生成多个对象。

对于basePackages和basePackageClasses的选择问题,basePackages的可读性会更好一些,因此在项目中会优先选择使用它,但是在需要大量重构的工程中,尽量不要使用basePackages定义,因为很多时候重构修改包名需要反复地配置,而IDE不会给你任何的提示,而采用basePackageClasses,当你对包移动的时候,IDE会报错提示,并且可以轻松处理这些错误。

采用下述代码来测试上述两个配置:

        AbstractApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        Role role = context.getBean(Role.class);
        RoleService roleService = context.getBean(RoleService.class);
        roleService.printRoleInfo(role);
        context.close();

运行结果:

八月 14, 2018 10:58:40 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@c2e1f26: startup date [Tue Aug 14 22:58:40 CST 2018]; root of context hierarchy
id=1
roleName=role_name_1
note=role_note_1
八月 14, 2018 10:58:40 下午 org.springframework.context.support.AbstractApplicationContext doClose
信息: Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@c2e1f26: startup date [Tue Aug 14 22:58:40 CST 2018]; root of context hierarchy

4.2 自动装配——@Autowired

之前一节中提到的两个问题之一就是注解没有注入对象。关于这个问题,在注解中略微有点复杂,在大部分的情况下建议使用自动装配,因为这样可以减小配置的复杂度,所以这里先介绍自动装配。

通过学习Spring IoC容器,我们知道Spring是先完成Bean的定义和生成,然后寻找需要注入的资源。也就是当Spring生成所有的Bean后,如果发现这个注解,它就会在Bean中查找,然后找到对应的类型,将其注入进来,这样就完成依赖注入了。所谓自动装配技术是一种由Spring自己发现对应的Bean,自动完成装配工作的方式,它会应用到一个十分常用的注解@Autowired上,这个时候Spring会根据类型去寻找定义的Bean然后将其注入,这里需要留意按类型(Role)的方式。

下面开始测试自动装配,代码如下:

package com.hys.spring.example3.service;

public interface RoleService2 {

    public void printRoleInfo();
}

这个接口采用了Spring推荐的接口方式,这样可以更为灵活,因为我们将定义和实现分离,接下来是其实现类,代码如下:

package com.hys.spring.example3.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.hys.spring.example3.pojo.Role;
import com.hys.spring.example3.service.RoleService2;

@Component("RoleService2")
public class RoleServiceImpl2 implements RoleService2 {

    @Autowired
    private Role role = null;

    @Override
    public void printRoleInfo() {
        System.out.println("id=" + role.getId());
        System.out.println("roleName=" + role.getRoleName());
        System.out.println("note=" + role.getNote());
    }
}

这里的@Autowired注解,表示在Spring IoC定位所有的Bean后,这个字段需要按类型注入,这样IoC容器就会寻找资源,然后将其注入。比如上述的Role和RoleServiceImpl2两个Bean,假设将其定义,那么Spring IoC容器会为它们先生成对应的实例,然后依据@Autowired注解,按照类型找到定义的实例,将其注入。

IoC容器有时候会寻找失败,在默认的情况下寻找失败它就会抛出异常,也就是说默认情况下,Spring IoC容器会认为一定要找到对应的Bean来注入这个字段,有些时候这并不是一个真实的需要,比如日志,有时候我们会觉得这是可有可无的,这个时候可以通过@Autowired的配置项required来改变它,比如@Autowired(required=false)。

正如之前所谈到的在默认情况下是必须注入成功的,所以这里的required的默认值为true。当把配置修改为了false时,就告诉Spring IoC容器,假如在已经定义好的Bean中找不到对应的类型,允许不注入,这样也就没有了异常抛出,只是这样这个字段可能为空,读者要自行校验,以避免发生空指针异常。在大部分的情况下,都不需要这样修改。

@Autowired除可以配置在属性之外,还允许方法配置,常见的Bean的setter方法也可以使用它来完成注入,比如类似下述的代码:

@Component("RoleService2")
public class RoleServiceImpl2 implements RoleService2 {

    private Role role = null;

    //do something...

    @Autowired
    public void setRole(Role role) {
        this.role = role;
    }
}

在大部分的配置中都推荐使用@Autowired注解,这是Spring IoC自动装配完成的,使得配置大幅度减少,满足约定优于配置的原则,增强程序的健壮性。但是在有些时候是不能进行自动装配的,关于这个问题,下节我们会进行讨论。

4.3 自动装配的歧义性(@Primary和@Qualifier)

在上节中,我们谈到了@Autowired注解,它可以完成一些自动装配的功能,并且使用方式十分简单,但是有时候这样的方式并不能使用。这一切的根源来自于按类型的方式,按照Spring的建议,在大部分情况下会使用接口编程,但是定义一个接口,并不一定只有与之对应的一个实现类。换句话说,一个接口可以有多个实现类,比如上述所说的RoleService,有了一个实现类RoleServiceImpl,但是还可以为其定义一个新的实现类RoleServiceImpl3,代码如下:

package com.hys.spring.example3.impl;

import org.springframework.stereotype.Component;

import com.hys.spring.example3.pojo.Role;
import com.hys.spring.example3.service.RoleService;

@Component("roleService3")
public class RoleServiceImpl3 implements RoleService {

    @Override
    public void printRoleInfo(Role role) {
        System.out.println("id=" + role.getId());
        System.out.println("roleName=" + role.getRoleName());
        System.out.println("note=" + role.getNote());
    }
}

再新建一个RoleController类,它有一个字段是RoleService类型,代码如下:

package com.hys.spring.example3.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.hys.spring.example3.pojo.Role;
import com.hys.spring.example3.service.RoleService;

@Component
public class RoleController {

    @Autowired
    private RoleService roleService = null;

    public void printRole(Role role) {
        roleService.printRoleInfo(role);
    }
}

这里的字段roleService是一个RoleService接口类型。RoleService有两个实现类,分别是RoleServiceImpl和RoleServiceImpl3,这个时候Spring IoC容器就会犯糊涂了,它无法判断把哪个对象注入进来,于是就会抛出异常,这样@Autowired注入就失败了。异常信息如下:

八月 15, 2018 10:28:25 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@c2e1f26: startup date [Wed Aug 15 22:28:25 CST 2018]; root of context hierarchy
八月 15, 2018 10:28:26 下午 org.springframework.context.support.AbstractApplicationContext refresh
警告: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'roleController': Unsatisfied dependency expressed through field 'roleService'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.hys.spring.example3.service.RoleService' available: expected single matching bean but found 2: roleServiceImpl,roleService3
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'roleController': Unsatisfied dependency expressed through field 'roleService'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.hys.spring.example3.service.RoleService' available: expected single matching bean but found 2: roleServiceImpl,roleService3
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:586)
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:91)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:372)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1341)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:572)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:495)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:759)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:869)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550)
	at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:88)
	at com.hys.spring.example3.test.Test.main(Test.java:14)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.hys.spring.example3.service.RoleService' available: expected single matching bean but found 2: roleServiceImpl,roleService3
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:215)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1113)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1062)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:583)
	... 14 more

通过上面的分析,可以知道产生这样的状况是因为它采用的是按类型来注入对象,而在Java中接口可以有多个实现类,同样的抽象类也可以有多个实例化的类,这样就会造成通过类型(by type)获取Bean的不唯一,从而导致Spring IoC类似于按类型的方法无法获得唯一的实例化类。我们可以回想到Spring IoC最底层容器接口——BeanFactory的定义,它存在一个通过类型获取Bean的方法:

public abstract <T> T getBean(Class<T> paramClass)
    throws BeansException;

通过RoleService.class作为参数就无法判断使用哪个类实例进行返回,这便是自动装配的歧义性。

为了消除歧义性,Spring提供了两个注解@Primary和@Qualifier,这是两个不同的注解,其消除歧义性的理念不太一样,下面让我们学习它们。

4.3.1 注解@Primary

注解@Primary代表首要的,当Spring IoC通过一个接口或者抽象类注入对象的时候,由于存在多个实现类或者具体类,就会犯糊涂,不知道采用哪个类注入为好。注解@Primary则是告诉Spring IoC容器,请优先使用该类注入。例如可以在下述代码中加入注解@Primary,如下所示:

package com.hys.spring.example3.impl;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import com.hys.spring.example3.pojo.Role;
import com.hys.spring.example3.service.RoleService;

@Component("roleService3")
@Primary
public class RoleServiceImpl3 implements RoleService {

    //do something...
}

这里的@Primary注解告诉Spring IoC容器,如果存在多个RoleService类型,无法判断注入哪个的时候,优先将RoleServiceImpl3的实例注入,这样就可以消除歧义性。同样的,或许你可以想到将@Primary注解也加入到RoleServiceImpl中,这样就存在两个首选的RoleService接口的实例了,但是在Spring IoC容器中这样定义是允许的,只是在注入的时候将抛出异常。异常信息如下:

八月 15, 2018 11:20:56 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@c2e1f26: startup date [Wed Aug 15 23:20:56 CST 2018]; root of context hierarchy
八月 15, 2018 11:20:56 下午 org.springframework.context.support.AbstractApplicationContext refresh
警告: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'roleController': Unsatisfied dependency expressed through field 'roleService'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.hys.spring.example3.service.RoleService' available: more than one 'primary' bean found among candidates: [roleServiceImpl, roleService3]
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'roleController': Unsatisfied dependency expressed through field 'roleService'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.hys.spring.example3.service.RoleService' available: more than one 'primary' bean found among candidates: [roleServiceImpl, roleService3]
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:586)
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:91)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:372)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1341)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:572)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:495)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:759)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:869)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550)
	at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:88)
	at com.hys.spring.example3.test.Test.main(Test.java:13)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.hys.spring.example3.service.RoleService' available: more than one 'primary' bean found among candidates: [roleServiceImpl, roleService3]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.determinePrimaryCandidate(DefaultListableBeanFactory.java:1381)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.determineAutowireCandidate(DefaultListableBeanFactory.java:1341)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1110)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1062)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:583)
	... 14 more

但是无论如何@Primary只能解决首要性的问题,而不能解决选择性的问题,简而言之,它不能选择使用接口具体的实现类来注入。

4.3.2 注解@Qualifier

正如上面所谈及的歧义性,一个重要的原因是Spring在寻找依赖注入的时候采用按类型注入引起的。除了按类型查找Bean,Spring IoC容器最底层的接口BeanFactory,也定义了按名称查找的方法,如果采用名称查找的方法,而不是采用按类型查找的方法,那么不就可以消除歧义性了吗?答案是肯定的,而注解@Qualifier就是这样的一个注解。

回到上述所说的代码,如果把RoleServiceImpl3定义了别名roleService3,那么只需要把RoleController按照下面的方式修改就可以注入这个实现类了,代码如下:

package com.hys.spring.example3.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import com.hys.spring.example3.pojo.Role;
import com.hys.spring.example3.service.RoleService;

@Component
public class RoleController {

    @Autowired
    @Qualifier("roleService3")
    private RoleService roleService = null;

    public void printRole(Role role) {
        roleService.printRoleInfo(role);
    }
}

这个时候IoC容器就不会再按照类型的方式注入,而是按照名称的方式注入,这样既能注入成功,也不存在歧义性。IoC容器的底层接口——BeanFactory,它所定义的方法如下:

public abstract Object getBean(String paramString)
    throws BeansException;

使用@Qualifier注解后就可以使用这个方法通过名称从IoC容器中获取对象进行注入。

4.4 装在带有参数的构造方法类

角色类的构造方法都是没带参数的,而事实上在某些时候构造方法是带参数的,对于一些带有参数的构造方法,也允许我们通过注解进行注入。比如有时候RoleController的构造方法代码如下:

package com.hys.spring.example3.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import com.hys.spring.example3.pojo.Role;
import com.hys.spring.example3.service.RoleService;

@Component
public class RoleController {

    @Autowired
    @Qualifier("roleService3")
    private RoleService roleService = null;

    public RoleController(RoleService roleService) {
        this.roleService = roleService;
    }

    //do something...
}

关于XML构建的方式,在1.1节中谈过,使用注解的方式应该如何注入呢?我们可以使用@Autowired或者@Qualifier进行注入,换句话说,这两个注解还能支持到参数。比如将上述代码中的构造方法修改为下述代码的样子,就可以完成构造方法的注入了:

    public RoleController(@Qualifier("roleService3") RoleService roleService) {
        this.roleService = roleService;
    }

4.5 使用@Bean装配Bean

以上都是通过@Component装配Bean,但是@Component只能注解在类上,不能注解到方法上。对于Java而言,大部分的开发都需要引入第三方的包(jar文件),而且往往并没有这些包的源码,这时候将无法为这些包的类加入@Component注解,让它们变为开发环境的Bean。你可以使用新类扩展(extends)其包内的类,然后在新类上使用@Component,但是这样又显得不伦不类。

这个时候Spring给予一个注解@Bean,它可以注解到方法之上,并且将方法返回的对象作为Spring的Bean,存放在IoC容器中。比如我们需要使用DBCP数据源,这个时候要引入关于它的包,然后可以通过如下代码来装配数据源的Bean:

    @Bean(name = "dataSource")
    public DataSource getDataSource() {
        Properties props = new Properties();
        props.setProperty("driver", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql://localhost:3306/ssm");
        props.setProperty("username", "root");
        props.setProperty("password", "root");
        DataSource dataSource = null;
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

这样就能够装配一个Bean,当Spring IoC容器扫描它的时候,就会为其生成对应的Bean。这里还配置了@Bean的name选项为dataSource,这就意味着Spring生成该Bean的时候就会使用dataSource作为其BeanName。和其他Bean一样,它也可以通过@Autowired或者@Qualifier等注解注入别的Bean中。

4.6 注解自定义Bean的初始化和销毁方法

上篇文章介绍了Spring Bean的生命周期,参考当时的生命周期图就可以知道,对于Bean的初始化可以通过实现Spring所定义的一些关于生命周期的接口来实现,这样BeanFactory或者其他高级容器ApplicationContext就可以调用这些接口所定义的方法了,这点和使用XML是一样的。但是我们还没有讨论如何在注解中实现自定义的初始化方法和销毁方法。其实很简单,主要是运用注解@Bean的配置项,注解@Bean不能使用在类的标注上,它主要是用在方法上,@Bean的配置项中包含4个配置项:

  • name:是一个字符串数组,允许配置多个BeanName。
  • autowire:标志是否是一个引用的Bean对象,默认值是Autowire.NO。
  • initMethod:自定义初始化方法。
  • destroyMethod:自定义销毁方法。

基于上述介绍,自定义的初始化方法是配置initMethod,而销毁方法则是destroyMethod,下面使用一个方法来创建上文中的juiceMaker2实例,并指明它的初始化方法和销毁方法:

    @Bean(name = "juiceMaker2", initMethod = "init", destroyMethod = "myDestory")
    public JuiceMaker2 initJuiceMaker2() {
        JuiceMaker2 juiceMaker2 = new JuiceMaker2();
        juiceMaker2.setBeverageShop("贡茶");
        Source source = new Source();
        source.setFruit("橙子");
        source.setSize("大杯");
        source.setSugar("少糖");
        juiceMaker2.setSource(source);
        return juiceMaker2;
    }

这样一个Spring Bean就可以注册到Spring IoC容器中了,也可以使用自动装配的方法将它装配到其他Bean中。


5 装配的混合使用

上面介绍了最基本的装配Bean的方法,在现实中,使用XML或者注解各有道理,建议在自己的工程中所开发的类尽量使用注解方式,因为使用它并不困难,甚至可以说更为简单,而对于引入第三方或者服务的类,尽量使用XML方式,这样的好处是可以尽量对第三方包或者服务的细节减少理解,也更加清晰和明朗。

如上述DBCP的注解注入就有些弊端:开发者需要了解第三方包的使用规则,而对于XML进行改写就简单了许多。现在通过使用XML来实现,代码如下:

        <bean id="dataSource"
		class="org.apache.commons.dbcp.BasicDataSource">
		<property name="driverClassName"
			value="com.mysql.jdbc.Driver" />
		<property name="url" value="jdbc:mysql://localhost:3306/ssm" />
		<property name="username" value="root" />
		<property name="password" value="root" />
	</bean>

显然我们并不需要去了解第三方包的更多细节,也不需要过多的Java代码,尤其是不用try...catch...finally...语句去处理它们,相对于@Bean的注入会更好一些,也更为简单,所以对于第三方的包或者其他外部的接口,建议还是使用XML的方式进行装载。

Spring同时支持这两种形式的装配,所以可以自由选择,只是无论采用XML还是注解方式的装配都是将Bean装配到Spring IoC容器中,这样就可以通过Spring IoC容器来管理各类资源了。

以数据库池的配置来举例,首先DBCP数据库连接池是通过第三方来定义的,我们没有办法给第三方加入注解,但是可以选择通过XML给出,这里可以继续使用上述代码的配置,假设它配置XML文件——spring-data.xml,我们需要通过引入它达到注解的体系当中。

使用注解@ImportResource,引入spring-data.xml所定义的内容,代码如下:

package com.hys.spring.example3.pojo;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportResource;

@ComponentScan(basePackages = { "com.hys.spring.example3" })
@ImportResource({ "classpath:spring-data.xml" })
public class ApplicationConfig {
}

@ImportResource中配置的内容是一个数组,也就是可以配置多个XML配置文件,这样就可以引入多个XML所定义的Bean了。

有时候所有的配置都放在一个ApplicationConfig类里面会造成配置复杂,因此就希望有多个类似于ApplicationConfig配置类,比如ApplicationConfig2、ApplicationConfig3等。Spring也提供了注解@Import的方式注入这些配置类,代码如下:

package com.hys.spring.example3.pojo;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

@ComponentScan(basePackages = { "com.hys.spring.example3" })
@Import({ ApplicationConfig2.class, ApplicationConfig3.class })
public class ApplicationConfig {
}

通过这样的形式加载了多个配置文件。

有多个XML文件,而你希望通过其中的一个XML文件来引入其他的XML文件,假设目前有了spring-bean.xml,需要引入spring-data.xml,那么可以在spring-bean.xml使用import元素来加载它,如下所示:

<import resource="spring-data.xml" />

也许你希望使用XML加载Java配置类,但是目前Spring是不能支持的,不过Spring可以支持通过XML的配置扫描注解的包,只需要通过<context:component-scan>定义扫描的包就可以了,如下所示:

<context:component-scan
		base-package="com.hys.spring.example3" />

6 使用Profile

在软件开发的过程中,敏捷开发模式很常见,也就是每次都提交一个小阶段的测试。那么可能是开发人员使用一套环境,而测试人员使用另一套环境,而这两套系统的数据库是不一样的,毕竟测试人员也需要花费很多的时间来构建测试数据,可不想老是被开发人员修改那些测试数据,这样就有了在不同的环境中进行切换的需求了。Spring也会对这样的场景进行支持,在Spring中我们可以定义Bean的Profile。

6.1 使用注解@Profile配置

先来看看使用注解@Profile是如何配置的,比如下面的例子,配置两个数据库连接池,一个用于开发(dev),一个用于测试(test),代码如下:

@Component
public class ProfileDataSource {

    @Bean(name = "devDataSource")
    @Profile("dev")
    public DataSource getDevDataSource() {
        Properties props = new Properties();
        props.setProperty("driver", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql://localhost:3306/ssm");
        props.setProperty("username", "root");
        props.setProperty("password", "root");
        DataSource dataSource = null;
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

    @Bean(name = "testDataSource")
    @Profile("test")
    public DataSource getTestDataSource() {
        Properties props = new Properties();
        props.setProperty("driver", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql://localhost:3306/test");
        props.setProperty("username", "root");
        props.setProperty("password", "root");
        DataSource dataSource = null;
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }
}

这里定义了两个Bean,分别定义了@Profile,一个是dev,一个是test,同样,使用XML也可以进行定义。

6.2 使用XML定义Profile

正如前面所论述的那样,有时候我们希望使用XML来配置数据源,因为它可以减少一些Java代码的使用。这个时候使用XML配置数据源也是没有问题的,代码如下:

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

	<beans profile="dev">
		<bean id="dataSource"
			class="org.apache.commons.dbcp.BasicDataSource">
			<property name="driverClassName"
				value="com.mysql.jdbc.Driver" />
			<property name="url" value="jdbc:mysql://localhost:3306/ssm" />
			<property name="username" value="root" />
			<property name="password" value="root" />
		</bean>
	</beans>

	<beans profile="test">
		<bean id="dataSource"
			class="org.apache.commons.dbcp.BasicDataSource">
			<property name="driverClassName"
				value="com.mysql.jdbc.Driver" />
			<property name="url"
				value="jdbc:mysql://localhost:3306/test" />
			<property name="username" value="root" />
			<property name="password" value="root" />
		</bean>
	</beans>

</beans>

这样也能够使用Profile。

6.3 启动Profile

当启动Java配置或者XML配置Profile时,可以发现这两个Bean并不会被加载到Spring IoC容器中,需要自行激活Profile。激活Profile的方法有5种。

  • 在使用Spring MVC的情况下可以配置Web上下文参数,或者DispatchServlet参数。
  • 作为JNDI条目。
  • 配置环境变量。
  • 配置JVM启动参数。
  • 在集成测试环境中使用@ActiveProfiles。

具体启动方法这里不再进行介绍,感兴趣的读者可以自行查阅相关知识。


7 加载属性(properties)文件

在开发的过程中,配置文件往往就是那些属性(properties)文件,比如使用properties文件配置数据库文件,又如database-config.properties,其内容如下述代码所示:

jdbc.database.driver=com.mysql.jdbc.Driver
jdbc.database.url=jdbc:mysql://localhost:3306/ssm
jdbc.database.username=root
jdbc.database.password=root

使用属性文件可以有效地减少硬编码,很多时候修改环境只需要修改配置文件就可以了,这样能够有效提高运维人员的操作便利性,所以使用properties文件是十分常见的场景。在Spring中也可以通过注解或者XML的方式进行加载属性文件,下面的两个小节,将展示它们的使用方法。

7.1 使用注解方式加载属性文件

首先Spring提供了注解@PropertySource来加载属性文件,它的使用比较简单,不过在此之前需要先来了解它的配置项:

  • name:字符串,配置这次属性配置的名称。
  • value:字符串数组,可以配置多个属性文件。
  • ignoreResourceNotFound:boolean值,默认为false,其含义为如果找不到对应的属性文件是否进行忽略处理,由于默认值为false,所以在默认的情况下找不到对应的配置文件会抛出异常。
  • encoding:编码,默认为""。

注意,如果只有@PropertySource的加载,Spring只会把对应文件加载起来。因此可以在Spring环境中使用它们,比如先重新定义Java配置类,代码如下:

package com.hys.spring.example3.pojo;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource(value = { "com/hys/spring/example3/config/database-config.properties" }, ignoreResourceNotFound = true)
public class ApplicationConfig {
}

@PropertySource的配置,首先加载了database-config.properties文件,然后定义选项为ignoreResourceNotFound=true,也就是找不到该文件就会忽略掉它。如果这个值为false,且找不到对应的文件,那么Spring就会抛出异常,停止工作,用下述代码对其进行测试:

        AbstractApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        String url = context.getEnvironment().getProperty("jdbc.database.url");
        System.out.println(url);
        context.close();

通过环境来获取对应的配置属性,但是如果仅仅这样,在Spring中是没有解析属性占位符的能力,Spring推荐使用一个属性文件解析类进行处理,它就是PropertySourcesPlaceholderConfigurer,使用它就意味着允许Spring解析对应的属性文件,并通过占位符来引用对应的配置。下面通过创建DBCP数据源来演示它。

首先修改上述的Java配置文件,代码如下所示:

package com.hys.spring.example3.pojo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;

@Configuration
@ComponentScan(basePackages = { "com.hys.spring.example3" })
@PropertySource(value = { "com/hys/spring/example3/config/database-config.properties" }, ignoreResourceNotFound = false)
public class ApplicationConfig {

    @Bean
    public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

上面的代码中定义了一个PropertySourcesPlaceholderConfigurer类的Bean,它的作用是为了让Spring能够解析属性占位符,比如这里既然属性文件已经定义了关于数据库连接所需要的配置,那么还需要知道如何来引用已经定义好的配置,这里可以使用注解@Value和占位符,代码如下:

@Component
public class DataSourceBean {

    @Value("${jdbc.database.driver}")
    private String driver   = null;
    @Value("${jdbc.database.url}")
    private String url      = null;
    @Value("${jdbc.database.username}")
    private String username = null;
    @Value("${jdbc.database.password}")
    private String password = null;

    @Bean(name = "dataSource")
    public DataSource getTestDataSource() {
        Properties props = new Properties();
        props.setProperty("driver", driver);
        props.setProperty("url", url);
        props.setProperty("username", username);
        props.setProperty("password", password);
        DataSource dataSource = null;
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }
}

注意代码中的@Value,我们使用了占位符${jdbc.database.driver}来引用加载进来的属性,这样就可以在Bean中通过注入形式获取文件的配置了。

7.2 使用XML方式加载属性文件

上节讨论了如何通过注解方式来加载属性文件,有时候也可以使用XML方式进行加载属性文件。如下所示:

                <bean
			class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
			<!-- 字符串数组,可配置多个属性文件 -->
			<property name="locations">
				<array>
					<value>classpath:database-config.properties</value>
					<value>classpath:log4j.properties</value>
				</array>
			</property>
			<property name="ignoreResourceNotFound" value="true" />
		</bean>

8 条件化装配Bean

在某些条件下不需要来装配Bean。比如当没有上述的database-config.properties属性配置时,就不要去创建数据源,这个时候,我们就需要通过条件化来判断。Spring提供了注解@Conditional来配置,通过它可以配置一个或者多个类,只是这些类都需要实现接口Condition(org.springframework.context.annotation.Condition),为了演示它,先来修改关于DBCP数据源的Bean,代码如下:

    @Bean(name = "dataSource")
    @Conditional({ DataSourceCondition.class })
    public DataSource getDataSource(@Value("${jdbc.database.driver}") String driver, @Value("${jdbc.database.url}") String url, @Value("${jdbc.database.username}") String username,
            @Value("${jdbc.database.password}") String password) {
        Properties props = new Properties();
        props.setProperty("driver", driver);
        props.setProperty("url", url);
        props.setProperty("username", username);
        props.setProperty("password", password);
        DataSource dataSource = null;
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

这里代码通过@Value往参数里注入了对应属性文件的配置,但是我们没有办法确定这些数据源连接池的属性是否在属性文件中已经配置完整,如果是不充足的属性配置,则会引起创建失败,为此要判断属性文件的配置是否充足才能继续创建Bean。通过@Conditional来引入了一个类——DataSourceCondition,由它来进行判断。先看看这个类,代码如下:

package com.hys.spring.example3.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class DataSourceCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //获取上下文环境
        Environment env = context.getEnvironment();
        //判断是否存在关于数据源的基础配置
        return env.containsProperty("jdbc.database.driver") && env.containsProperty("jdbc.database.url") && env.containsProperty("jdbc.database.username")
                && env.containsProperty("jdbc.database.password");
    }
}

这里要求DataSourceCondition实现接口Condition的matches方法,该方法有两个参数,一个是ConditionContext,通过它可以获得Spring的运行环境,一个是AnnotatedTypeMetadata,通过它可以获得关于该Bean的注解信息。代码中先获取了运行上下文环境,然后判断在环境中属性文件是否配置了数据库的相关参数,如果配置了,则返回为true,那么Spring会去创建对应的Bean,否则是不会创建的。


9 Bean的作用域

在默认的情况下,Spring IoC容器只会对一个Bean创建一个实例,比如下面的测试:

        AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        RoleService roleService1 = (RoleService) ctx.getBean("roleService3");
        RoleService roleService2 = (RoleService) ctx.getBean("roleService3");
        System.out.println(roleService1 == roleService2);

运行结果:

八月 18, 2018 10:47:44 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@c2e1f26: startup date [Sat Aug 18 22:47:44 CST 2018]; root of context hierarchy
true

这里我们通过类型两次从Spring IoC容器中取出Bean,然后通过==比较,这是一个位比较。话句话说,就是比较roleService1和roleService2是否为同一个对象,经过测试它们是同一个对象,话句话说,在默认的情况下,Spring IoC容器只会为配置的Bean生成一个实例,而不是多个。

有时候我们希望能够通过Spring IoC容器中获取多个实例,比如Struts2(现在它的使用已经比较少了)中的Action(Struts2的控制层类),它往往绑定了从页面请求过来的订单。如果它也是一个实例,那么订单从头到尾就只有一个,而不是多个,这样就不能满足互联网的并发要求了。为了解决这个问题,有时候我们希望Action是多个实例,每当我们请求的时候就产生一个独立的对象,而不是默认的一个,这样多个实例就可以在不同的线程运行了,就没有并发问题了。关于这些是由Spring的作用域所决定的。

Spring提供了4种作用域,它会根据情况来决定是否生成新的对象。

  • 单例(singleton):它是默认的选项,在整个应用中,Spring只为其生成一个Bean的实例。
  • 原型(prototype):当每次注入,或者通过Spring IoC容器获取Bean时,Spring都会为它创建一个新的实例。
  • 会话(session):在Web应用中使用,就是在会话过程中Spring只创建一个实例。
  • 请求(request):在Web应用中使用的,就是在一次请求中Spring会创建一个实例,但是不同的请求会创建不同的实例。

从4种作用域可以看出,对于Struts2的Action而言,使用请求会合理一些。在4种作用域中会话和请求只能在Web应用中使用,先来测试原型,修改RoleServiceImpl3类,代码如下:

@Component("roleService3")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class RoleServiceImpl3 implements RoleService {

    //do something...
}

这里使用了注解@Scope,并且声明为原型,修改完之后,再次测试代码,于是可以得到下面的结果:

八月 18, 2018 10:49:38 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@c2e1f26: startup date [Sat Aug 18 22:49:38 CST 2018]; root of context hierarchy
false

从测试结果可以看到两个对象并非同一个对象,因为我们将其声明为了原型,每当我们从Spring IoC容器中获取对象,它就会生成一个新的实例,这样两次获取就获得了不同的对象,于是比较就返回为false了。


10 使用Spring表达式(Spring EL)

Spring还提供了更灵活的注入方式,那就是Spring表达式,实际上Spring EL远比以上注入方式强大,我们需要学习它。

Spring EL拥有很多功能:

  • 使用Bean的id来引用Bean。
  • 调用指定对象的方法和访问对象的属性。
  • 进行运算。
  • 提供正则表达式进行匹配。
  • 集合配置。

这些都是Spring表达式的内容,使用Spring表达式可以获得比使用Properties文件更为强大的装配功能,只是有时候为了方便测试可以使用Spring EL定义的解析类进行测试,为此我们先来认识它们。

10.1 Spring EL相关的类

简要介绍Spring EL的相关类,以便我们进行测试和理解。首先是ExpressionParser接口,它是一个表达式的解析接口,既然是一个接口,那么它就不具备任何具体的功能,显然Spring会提供更多的实现类,如下图所示:

举例说明Spring EL的使用,代码如下:

        //表达式解析器
        ExpressionParser parser = new SpelExpressionParser();
        //设置表达式
        Expression exp = parser.parseExpression("'hello world'");
        String str = (String) exp.getValue();
        System.out.println(str);
        //通过EL方法普通方法
        exp = parser.parseExpression("'hello world'.charAt(0)");
        char ch = (char) exp.getValue();
        System.out.println(ch);
        //通过EL访问getter方法
        exp = parser.parseExpression("'hello world'.bytes");
        byte[] bytes = (byte[]) exp.getValue();
        System.out.println(bytes);
        //通过EL访问属性,相当于"hello world".getBytes().length
        exp = parser.parseExpression("'hello world'.bytes.length");
        int length = (int) exp.getValue();
        System.out.println(length);
        exp = parser.parseExpression("new String('abc')");
        String abc = (String) exp.getValue();
        System.out.println(abc);

运行结果:

hello world
h
[B@380fb434
11
abc

通过表达式可以创建对象,调用对象的方法获取属性。用变量来解析表达式,因为使用变量会使表达式更加灵活,比如针对Spring IoC容器进行解析,我们可以从中获得我们配置的属性。为了更好地满足用户的需要,Spring EL还支持变量的解析,只是使用变量解析的时候常常用到一个接口——EvaluationContext,它可以有效解析表达式中的变量。它也有一个实现类——StandardEvaluationContext,下面针对角色类和List进行举例,代码如下:

        //创建角色对象
        Role role = new Role(1L, "role_name", "role_note");
        exp = parser.parseExpression("note");
        //相当于从role中获取备注信息
        String note = (String) exp.getValue(role);
        System.out.println(note);
        //变量环境类,并且将角色对象role作为其根节点
        EvaluationContext ctx = new StandardEvaluationContext(role);
        //变量环境类操作根节点
        parser.parseExpression("note").setValue(ctx, "new_note");
        //获取备注,这里的String.class指明,我们希望返回的是一个字符串
        note = parser.parseExpression("note").getValue(ctx, String.class);
        System.out.println(note);
        //调用getRoleName方法
        String roleName = parser.parseExpression("getRoleName()").getValue(ctx, String.class);
        System.out.println(roleName);
        //新增环境变量
        List<String> list = new ArrayList<>();
        list.add("value1");
        list.add("value2");
        //给环境变量增加变量
        ctx.setVariable("list", list);
        //通过表达式来读/写环境变量的值
        parser.parseExpression("#list[1]").setValue(ctx, "update_value2");
        System.out.println(parser.parseExpression("#list[1]").getValue(ctx));

运行结果:

role_note
new_note
role_name
update_value2

EvaluationContext使用了它的实现类StandardEvaluationContext,进行了实例化,在构造方法中将角色对象传递给它了,那么估值内容就会基于这个类进行解析。所以后面表达式的setValue和getValue方法都把这个估值内容传递进去,这样就能够读/写根节点的内容了,并且通过getRoleName()的例子,还可以知道它甚至能够支持方法的调用。为了更加灵活,估值内容还支持了其他变量的新增和其他操作,正如代码中创建了一个List,并且把List用估值内容的setVariable方法设置,其键为“list”,这样就允许我们在表达式里面通过#list来引用它,而给出的下标1,则是代表引用List的第二个元素(list是以下标0标识第一个元素的)。

上面介绍了Spring具有对表达式的解析功能,Spring EL最重要的功能就是对Bean属性进行注入,让我们以注解的方式为主来学习它们。

10.2 Bean的属性和方法

使用注解的方式需要用到注解@Value,在属性文件的读取中使用的是“$”,而在Spring EL中则使用“#”。下面以角色类为例进行讨论,我们可以这样初始化它的属性,代码如下:

package com.hys.spring.example3.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component("role")
public class Role {

    //赋值long型
    @Value("#{1}")
    private Long   id;
    //字符串赋值
    @Value("#{'role_name_1'}")
    private String roleName;
    //字符串赋值
    @Value("#{'role_note_1'}")
    private String note;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

这样就可以定义一个BeanName为role的角色类了,同时给予它所有的属性赋值,这个时候可以通过另外一个Bean来引用它的属性或者调用它的方法,比如新建一个类——ElBean作为测试,代码如下:

package com.hys.spring.example3.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component("elBean")
public class ElBean {

    //通过beanName获取bean,然后注入
    @Value("#{role}")
    private Role   role;
    //获取bean的属性id
    @Value("#{role.id}")
    private Long   id;
    //获取bean的getNote方法,获取角色名称
    @Value("#{role.getNote().toString()}")
    private String note;

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

我们可以通过BeanName进行注入,也可以通过OGNL获取其属性或者调用其方法来注入其他的Bean中。注意,表达式“#{role.getNote().toString()}”的注入,因为getNote可能返回为null,这样toString()就会抛出异常了。为了处理这个问题,可以这样写“#{role.getNote()?.toString()}”,这个表达式中问号的含义是先判断是否返回为非null,如果不是则不再调用toString方法(这在一些模板语言例如FreeMarker中的用法是类似的)。

10.3 使用类的静态常量和方法

有时候我们可能希望使用一些静态方法和常量,比如圆周率Π,而在Java中就是Math类的PI常量了,需要注入它十分简单,在ElBean中如同下面一样操作就可以了:

    @Value("#{T(Math).PI}")
    private double pi;

同样,有时候使用Math类的静态方法来生成随机数(0到1之间的随机双精度数字),这个时候就需要使用它的random方法了,比如:

    @Value("#{T(Math).random()}")
    private double random;

这样就可以通过调用类的静态方法加载对应的数据了。

10.4 Spring EL运算

上面讨论了如何获取值,除此之外Spring EL还可以进行运算,比如在ElBean上增加一个数字num,其值默认要求是角色编号(id)+1,那么我们就可以写成:

    @Value("#{role.id+1}")
    private int    num;

有时候“+”运算符也可以运用在字符串的连接上,比如下面的这个字段,把角色对象中的属性roleName和note相连:

    @Value("#{role.roleName+role.note}")
    private String str;

这样就能够得到一个角色名称和备注相连接的字符串。

比较两个值是否相等,比如角色id是否为1,角色名称是否为“role_name_001”。数字和字符串都可以使用“eq”或者“==”进行相等比较。除此之外,还有大于、小于等数学运算,比如:

    @Value("#{role.id==1}")
    private boolean equalNum;
    @Value("#{role.note eq 'note_1'}")
    private boolean equalString;
    @Value("#{role.id>2}")
    private boolean greater;
    @Value("#{role.id<2}")
    private boolean less;

在Java中,也许你会怀念三目运算,比如,如果角色编号大于1,那么取值5,否则取值1,那么在Java中可以写成:

int max = (role.getId() > 1 ? 5 : 1);

如果角色的备注为空,我们给它一个默认的初始值“note”,使用Java则写成:

String defaultString = (role.getNote() == null ? "hello" : role.getNote());

下面让我们通过String EL来实现上述的功能:

    @Value("#{role.id>1?5:1}")
    private int     max;
    @Value("#{role.note?:'hello'}")
    private String  defaultString;

实际上Spring EL的功能远不止这些,上面只介绍了一些最基础、最常用的功能,熟练运用它还需要读者们多动手实践。

参考资料:[1]杨开振 周吉文 梁华辉 谭茂华.Java EE 互联网轻量级框架整合开发:SSM框架(Spring MVC+Spring+MyBatis)和Redis实现.北京:电子工业出版社,2017.7

猜你喜欢

转载自blog.csdn.net/weixin_30342639/article/details/81814656