Spring工厂类介绍

在前面使用了Spring的ApplicationContext,并通过它的getBean方法获取Spring的配置文件applicationContext.xml,然后得到Bean的class对象。

从图中可以知道我们使用的是ApplicationContext接口,而使用的具体实现类是ClassPathXmlApplicationContext用来加载类路径下面的配置文件;注意到左边还有一个FileSystemXmlApplicationContext实现类,用于加载文件系统中的文件,言外之意就是当你的配置文件不在当前的项目工程内,此时就可以使用FileSystemXmlApplicationContext实现类去获取配置文件,接着去获取Bean对象。在老旧的Spring中使用的是BeanFactory接口,正如你所看到的ApplicationContext接口是它的子类自然功能就相对来说较为丰富。再有一点就是两者在生成Bean实例的时间是不同的。对于BeanFactory接口来说是工厂实例化结束后,在调用getBean方法的时才会去创建该类的实例;而ApplicationContext接口则是一开始加载配置文件的时候,就会将配置文件中所有通过单例模式创建的对象都给实例化,一般都会使用后者:

1
2
3
4
5
6
7
8
9
10
11
/**
* Spring开发模式:读取磁盘系统中的配置文件
**/
@Test
public void testThree(){
ApplicationContext applicationContext = new FileSystemXmlApplicationContext("D:\\Maven\\applicationContext.xml");
UserServiceImpl userServiceimpl = (UserServiceImpl) applicationContext.getBean("userService");
//调用对象的方法
System.out.println(userServiceimpl.getName());
userServiceimpl.sayHello();
}

使用XML来管理Bean

在Spring中管理Bean有两种方式:XML方式和注解方式,先介绍XML方式。在Spring中实例化Bean的方式有三种:1、使用类构造器实例化(默认无参数);2、使用静态工厂方法实例化(简单工厂模式);3、使用实例工厂方法实例化(工厂方法模式)

使用XML来管理Bean—-使用类构造器实例化(默认无参数)

在java包中新建一个com.envy.demo的文件夹,接着新建一个Bean1.java文件:

1
2
3
4
5
6
7
8
public class Bean1 {
/**
* 使用无参数的类构造器实例化Bean
*/
public Bean1(){
System.out.println("使用无参数的类构造器实例化Bean");
}
}

接着在applicationContext.xml文件中新增一个Bean:

1
2
3
<!-- Bean实例化的三种方式 -->
<!-- 第一种,使用无参数的类构造器实例化Bean -->
<bean id="bean1" class="com.envy.demo.Bean1"></bean>

最后新建一个SpringBeanTest.java的测试文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SpringBeanTest {
/**
*
* Bean实例化的三种方式
**/
@Test
public void testOne(){
/**
* 使用无参数的构造函数实例化Bean
* **/

//获得Spring工厂
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean1 bean1 = (Bean1)applicationContext.getBean("bean1");
}

运行testOne方法,发现控制台输出:

1
使用无参数的类构造器实例化Bean

使用XML来管理Bean—-使用静态工厂方法实例化(简单工厂模式)

在java包中新建一个com.envy.demo的文件夹,接着新建一个Bean2.java文件:

1
2
3
public class Bean2{

}

接着在该包中新建Bean2Factory.java文件,用于作为静态工厂实例化Bean2:

1
2
3
4
5
6
public class Bean2Factory {
public static Bean2 createBean2(){
System.out.println("使用静态工厂实例化Bean");
return new Bean2();
}
}

然后在applicationContext.xml文件中新增一个Bean:

1
2
3
<!-- Bean实例化的三种方式 -->
<!-- 第二种,使用静态工厂实例化Bean -->
<bean id="bean2" class="com.envy.demo.Bean2Factory" factory-method="createBean2"></bean>

最后在SpringBeanTest.java测试文件中添加testTwo方法:

1
2
3
4
5
6
7
8
9
@Test
public void testTwo(){
/**
* 使用静态工厂实例化Bean
* **/
//获得Spring工厂
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean2 bean2 = (Bean2)applicationContext.getBean("bean2");
}

运行testTwo方法,发现控制台输出:

1
2
使用无参数的类构造器实例化Bean
使用静态工厂实例化Bean

你会发现之前的testOne方法也被执行了,这个是正常现象,因为该工厂ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");只要加载配置文件就会将该配置文件中所有的类都给实例化。

如果你不想看到testOne方法被执行输出,可以将applicationContext.xml文件中的<bean id="bean1" class="com.envy.demo.Bean1"></bean>给注释掉即可。

使用XML来管理Bean—-使用实例工厂方法实例化(工厂方法模式)

在java包中新建一个com.envy.demo的文件夹,接着新建一个Bean3.java文件:

1
2
3
public class Bean3{

}

接着在该包中新建Bean3Factory.java文件,用于作为实例工厂实例化Bean3:

1
2
3
4
5
6
7
8
9
public class Bean3Factory {
/**
* Bean3的实例工厂
**/
public Bean3 createBean3(){
System.out.println("使用实例工厂实例化Bean");
return new Bean3();
}
}

然后在applicationContext.xml文件中新增一个Bean:

1
2
3
4
 <!-- Bean实例化的三种方式 -->
<!-- 第三种,使用实例工厂实例化Bean -->
<bean id="bean3Factory" class="com.envy.demo.Bean3Factory"></bean>
<bean id="bean3" factory-bean="bean3Factory" factory-method="createBean3"></bean>

实例方法和静态方法的区别是前者的调用必须依赖于对象,而后者可以通过类名来调用。因此此处的bean3必须要依靠bean3Factory对象,所以先获取bean3Factory对象,接着把bean3Factory对象作为factory-bean来生成bean3对象。

最后在SpringBeanTest.java测试文件中添加testThree方法:

1
2
3
4
5
6
7
8
9
@Test
public void testThree(){
/**
* 使用实例工厂实例化Bean
* **/
//获得Spring工厂
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean3 bean3 = (Bean3) applicationContext.getBean("bean3");
}

运行testThree方法,发现控制台输出:

1
使用实例工厂实例化Bean

通常情况下我们会使用第一种,也就是默认的无参数的构造函数方式来实例化Bean,除非你的类的构造非常复杂才会使用后面三种。

Bean的配置

一般而言在Bean在使用最多的就是id和name,通常装配一个Bean时指定一个id属性作为Bean的名称,注意id属性在IOC容器中必须是唯一的;name和id属性的区别不大,当id中出现一些特殊字符时,则必须使用name属性,Id属性中不能包含特殊字符。

class属性用于设置一个类的完全路径名称,主要作用是IOC容器生成类的实例。

Bean的作用域

Bean的作用域是通过scope属性来设置的,其中scope的值有以下几种:

前面两个较为常用,后面两个不常用,一般用于web项目。scope属性默认值是singleton,也就是单例模式。你可以测试一下,先创建一个Bean4.java文件:

1
2
public class Bean4 {
}

然后在applicationContext.xml文件中新增一个Bean:

1
2
<!-- 测试Bean的作用域 -->
<bean id="bean4" class="com.envy.demo.Bean4"></bean>

最后在SpringBeanTest.java测试文件中添加testFour方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testFour(){
/**
* 测试Bean的作用域
* **/
//获得Spring工厂
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean4 bean41 = (Bean4) applicationContext.getBean("bean4");
Bean4 bean42 = (Bean4) applicationContext.getBean("bean4");

System.out.println(bean41);
System.out.println(bean42);
}

运行testFour方法,发现控制台输出:

1
2
com.envy.demo.Bean4@51c8530f
com.envy.demo.Bean4@51c8530f

说明这两个对象是同一个对象,也就是scope属性默认值为singleton。

接着添加scope="prototype"配置,然后再次运行testFour方法,发现控制台输出:

1
2
com.envy.demo.Bean4@51c8530f
com.envy.demo.Bean4@7403c468

说明这是两个不同的对象,每次调用getBean方法都会生成一个新的实例。

Bean的生命周期配置

Spring初始化bean或销毁bean时,有时需要进行一些处理工作,因此spring可以在创建和销毁bean的时候调用Bean的两个生命周期方法:init-methoddestory-method

1
<bean id="test" class="com.envy.demo.Test" init-method="init" destroy-method="destory"></bean>

当bean被加载到容器的时候调用init方法;当bean从容器中删除的时候调用destory方法(注意destory方法必须是当scope=singleton时才有效,因为当你不是单例的时候,destory方法不知道你具体需要删除哪个实例)。前面说过scope默认是singleton,因此可以不写scope即可。

你可以测试一下,先创建一个Bean5.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Bean5 {
public Bean5(){
System.out.println("Bean5被实例化了");
}

public void initBean5(){
System.out.println("Bean5被初始化了");
}

public void destroyBean5(){
System.out.println("Bean5被销毁了");
}
}

然后在applicationContext.xml文件中新增一个Bean:

1
2
<!-- 测试Bean的生命周期 -->
<bean id="bean5" class="com.envy.demo.Bean5" init-method="initBean5" destroy-method="destroyBean5"></bean>

最后在SpringBeanTest.java测试文件中添加testFive方法:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testFive(){
/**
* 测试Bean的生命周期
* **/
//获得Spring工厂
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean5 bean5 = (Bean5) applicationContext.getBean("bean5");
System.out.println(bean5);
((ClassPathXmlApplicationContext) applicationContext).close();
}

运行testFive方法,发现控制台输出:

1
2
3
4
Bean5被实例化了
Bean5被初始化了
com.envy.demo.Bean5@51c8530f
Bean5被销毁了

Bean的生命周期完整过程

接下来通过之前的Bean5来介绍Bean的整个完整的生命周期过程。
第一步:Bean对象实例化。前面代码已经完成。
第二步:封装Bean对象属性。打开Bean5.java文件,修改代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Bean5 {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
System.out.println("第二步:封装name属性");
this.name = name;
}

public Bean5(){
System.out.println("第一步:Bean5被实例化了");
}

public void initBean5(){
System.out.println("Bean5被初始化了");
}

public void destroyBean5(){
System.out.println("Bean5被销毁了");
}
}

既然封装了某个属性,那么需要去applicationContext.xml文件中新增property:

1
2
3
<bean id="bean5" class="com.envy.demo.Bean5" init-method="initBean5" destroy-method="destroyBean5">
<property name="name" value="这是测试完整的Bean生命周期"></property>
</bean>

第三步:如果Bean实现BeanNameAware,那么必须重写setBeanName方法
第四步:如果Bean实现BeanFactoryAware,则需重写setBeanFactory方法(或者实现ApplicationContextAware,并重写上下文对象setApplicationContext方法,两者二选一)

可以看出其实第三步和第四步是让我们这个Bean来了解自己在Spring容器中的信息,如名字、工厂等信息。

其实第三步就是获取Bean在Spring容器中配置信息,即name或者id值。在Bean5.java文件中让Bean5实现BeanNameAware接口,并新增以下代码:

1
2
3
4
public void setBeanName(String name) {
//这里的name其实就是applicationContext.xml中Bean的id的值
System.out.println("第三步:设置Bean的名称"+name);
}

第四步是让Bean去了解工厂的信息。在Bean5.java文件中让Bean5实现ApplicationContextAware接口,并新增以下代码:

1
2
3
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("第四步:了解工厂信息");
}

这其中的第五步和第八步是较为核心的步骤。第五步,如果存在类实现BeanPostProcessor(后处理Bean),则会执行其中的postProcessBeforeInitialization方法。

先创建一个MyBean5PostProcessor.java文件,去实现BeanPostProcessor接口,并重写其中的两个方法(一般该两个方法均有返回值):

1
2
3
4
5
6
7
8
9
10
11
public class MyBean5PostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String s) throws BeansException {
System.out.println("第五步:这是初始化前的方法...");
return bean;
}

public Object postProcessAfterInitialization(Object bean, String s) throws BeansException {
System.out.println("第八步:这是初始化后的方法...");
return bean;
}
}

既然新建了一个类,就得去applicationContext.xml文件中新增一个Bean:

1
<bean class="com.envy.demo.MyBean5PostProcessor"></bean>

请注意该Bean没有id属性,因为通常设置id属性是可以让你在工厂或者其他地方可以使用到该类,而这个MyBean5PostProcessor类Spring则会在生成类的实例过程中会自动去调用,并不需要其他地方去引用,因此无需配置id。

第六步,如果Bean实现InitializingBean,那么必须重写afterPropertiesSet方法。让Bean5类去实现InitializingBean接口,并重写afterPropertiesSet方法:

1
2
3
public void afterPropertiesSet() throws Exception {
System.out.println("第六步:属性设置完后就会调用");
}

第七步,执行applicationContext.xml文件中<bean id="bean5" init-method="initBean5">中的指定的init-method方法initBean5。修改Bean5.java文件中的initBean5方法代码:

1
2
3
public void initBean5(){
System.out.println("第七步:Bean5被初始化了");
}

第八步,如果存在类实现BeanPostProcessor(后处理Bean),则会执行其中的postProcessAfterInitialization方法。

第九步,执行Bean5.java文件中自身的逻辑方法,如定义一个run方法:

1
2
3
public void run(){
System.out.println("第九步:执行自身业务逻辑方法");
}

第十步,如果Bean实现DisposableBean接口,那么必须重写destroy方法。让Bean5类去实现DisposableBean接口,并重写destroy方法:

1
2
3
public void destroy() throws Exception {
System.out.println("第十步:执行Spring中的destroy方法");
}

第十一步,执行applicationContext.xml文件中<bean id="bean5" destroy-method="destroyBean5>中的指定的destroy-method方法destroyBean5。修改Bean5.java文件中的destroyBean5方法代码:

1
2
3
public void destroyBean5(){
System.out.println("第十一步:Bean5被销毁了");
}

最后在SpringBeanTest.java测试文件中添加testSix方法:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testSix(){
/**
* 测试Bean的完整生命周期
* **/
//获得Spring工厂
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean5 bean5 = (Bean5) applicationContext.getBean("bean5");
bean5.run();
((ClassPathXmlApplicationContext) applicationContext).close();
}

运行testSix方法,发现控制台输出:

1
2
3
4
5
6
7
8
9
10
11
第一步:Bean5被实例化了
第二步:封装name属性
第三步:设置Bean的名称bean5
第四步:了解工厂信息
第五步:这是初始化前的方法...
第六步:属性设置完后就会调用
第七步:Bean5被初始化了
第八步:这是初始化后的方法...
第九步:执行自身业务逻辑方法
第十步:执行Spring中的destroy方法
第十一步:Bean5被销毁了

前面也说过这里面最重要的是第五步和第八步(后处理Bean),它是在我们整个类的生成过程中需要执行的一个步骤,它可以在不修改Bean中的代码实现对类功能的增强。

BeanPostProcessor的作用

前面说过BeanPostProcessor(后处理类)可以在生成类的过程中对类进行代理,且可以对其中的方法进行增强。接下来通过面向接口的方式来演示如何增强一个类中的方法。

新建一个UserDao.java的接口文件:

1
2
3
4
5
6
7
8
9
public interface UserDao {
public void findAll();

public void save();

public void update();

public void delete();
}

接着新建一个UserDaoImpl.java文件,去实现它并重写其中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UserDaoImpl implements UserDao {
public void findAll() {
System.out.println("查询用户");
}

public void save() {
System.out.println("保存用户");
}

public void update() {
System.out.println("修改用户");
}

public void delete() {
System.out.println("删除用户");
}
}

既然新建了一个类,就得去applicationContext.xml文件中新增一个Bean:

1
<bean id="userDao" class="com.envy.demo.UserDaoImpl"></bean>

接着注释掉其中一些没有的代码,只保留以下代码:

1
2
3
<bean class="com.envy.demo.MyBean5PostProcessor"></bean>
<!-- 测试BeanPostProcessor的作用 -->
<bean id="userDao" class="com.envy.demo.UserDaoImpl"></bean>

最后在SpringBeanTest.java测试文件中添加testSeven方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testSeven(){
/**
* 测试BeanPostProcessor的作用
* **/
//获得Spring工厂
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = (UserDao) applicationContext.getBean("userDao");
userDao.findAll();
userDao.save();
userDao.update();
userDao.delete();
}

运行testSeven方法,发现控制台输出:

1
2
3
4
5
6
第五步:这是初始化前的方法...
第八步:这是初始化后的方法...
查询用户
保存用户
修改用户
删除用户

确实调用了MyBean5PostProcessor文件中的方法并进行输出。MyBean5PostProcessor中的两个方法目前只是直接返回了Bean实例,其实是可以返回一个代理对象的,因此可以先对Bean进行处理,最后返回一个代理对象即可。或者说需要对UserDaoImpl类中的save方法进行功能上增强,如增加权限控制。实现这个过程只需要在MyBean5PostProcessor类中修改postProcessAfterInitialization方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object postProcessAfterInitialization(final Object bean, String beanName) throws BeansException {
System.out.println("第八步:这是初始化后的方法...");
if ("userDao".equals(beanName)) {
Object proxy = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("save".equals(method.getName())) {
System.out.println("执行对应的权限操作验证");
return method.invoke(bean, args);
}
return method.invoke(bean, args);
}
});
return proxy;
}
return bean;
}
}

最后在SpringBeanTest.java测试文件中运行testSeven方法,发现控制台输出:

1
2
3
4
5
6
7
第五步:这是初始化前的方法...
第八步:这是初始化后的方法...
查询用户
执行对应的权限操作验证
保存用户
修改用户
删除用户

看到没有我们并没有在实现类中进行任何操作,仅仅在postProcessAfterInitialization方法中进行了处理就实现了相应的功能。

DI依赖注入

spring在创建类的过程中会将类依赖的属性注入进来,接下是spring类的属性注入方式。对于类成员变量而言,注入方式有三种:1、构造函数注入;2、属性setter方法注入;3、接口注入(不常用)。但是Spring只支持前两种也就是构造函数注入和属性setter方法注入。

构造函数注入

通过构造方法注入Bean的属性值或依赖的对象,它保证了Bean实例在实例化后就可以使用。构造器注入在元素里声明的属性。

第一步:新建Bean6.java文件,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Bean6 {
private String name;
private Integer age;

public Bean6(String name,Integer age){
this.name =name;
this.age=age;
}

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

第二步:往applicationContext.xml文件中新增Bean6的信息:

1
2
3
4
5
<!-- 使用构造器函数给Bean注入属性 -->
<bean id="bean6" class="com.envy.demo.Bean6">
<constructor-arg name="name" value="张三"></constructor-arg>
<constructor-arg name="age" value="22"></constructor-arg>
</bean>

第三步:新建一个测试方法testBeanOne:

1
2
3
4
5
6
7
8
9
@Test
/**
* 使用构造器函数给Bean注入属性
* **/
public void testBeanOne(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean6 bean6 =(Bean6)applicationContext.getBean("bean6");
System.out.println(bean6);
}

运行结果:

1
Bean6{name='张三', age=22}

属性setter方法注入

使用属性的setter方法,并通过Spring配置文件中的元素来声明属性的名称及值。

第一步:新建Bean7.java文件,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Bean7 {
private String id;

public String getId(){
return this.id;
}
public void setId(String id){
this.id=id;
}
@Override
public String toString() {
return "Bean7{" +
"id='" + id + '\'' +
'}';
}
}

第二步:往applicationContext.xml文件中新增Bean7的信息:

1
2
3
4
<!-- 属性setter方法注入 -->
<bean id="bean7" class="com.envy.demo.Bean7">
<property name="id" value="001"></property>
</bean>

第三步:新建一个测试方法testBeanTwo:

1
2
3
4
5
6
7
8
9
@Test
/**
* 属性setter方法注入
* **/
public void testBeanTwo(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean7 bean7 =(Bean7)applicationContext.getBean("bean7");
System.out.println(bean7);
}

运行结果:

1
Bean7{id='001'}

其实在实际中setter属性注入是用的较多的方法,而更多的被用在属性是引用对象的情况,如现在的Bean6有一个属性是Bean7,则应该怎么修改代码呢?按照下面的步骤进行:

第一步:新建Bean67.java文件,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Bean67 {
private String name;
private Integer age;
private Bean7 bean7;

public String getName() {
return name;
}

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

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public Bean7 getBean7() {
return bean7;
}

public void setBean7(Bean7 bean7) {
this.bean7 = bean7;
}

@Override
public String toString() {
return "Bean67{" +
"name='" + name + '\'' +
", age=" + age +
", bean7=" + bean7 +
'}';
}
}

第二步:往applicationContext.xml文件中新增Bean67信息:

1
2
3
4
5
6
7
8
9
10
<!-- 属性setter方法注入 -->
<bean id="bean7" class="com.envy.demo.Bean7">
<property name="id" value="001"></property>
</bean>

<bean id="bean67" class="com.envy.demo.Bean67">
<property name="name" value="张三"></property>
<property name="age" value="22"></property>
<property name="bean7" ref="bean7"></property> //ref中为引用对象的id或者name
</bean>

第三步:新增测试方法testBeanThree:

1
2
3
4
5
6
7
8
9
@Test
/**
* 属性setter方法注入(参数中包含引用对象)
* **/
public void testBeanThree(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean67 bean67 =(Bean67)applicationContext.getBean("bean67");
System.out.println(bean67);
}

运行结果:

1
Bean67{name='张三', age=22, bean7=Bean7{id='001'}}

也就是说普通属性的注入使用name,而引用属性使用ref,这种方式在构造方法中注入也是适用的。

p名称空间属性注入

当然还可以使用p名称空间的方法来实现属性的注入,它是为了简化xml文件配置,从Spring2.5开始引入的。使用p:<属性名>="xxx"引入常量值;使用p:<属性名>-ref="xxx"引用其他Bean对象。

接下来使用前面介绍的Bean67进行p名称空间属性注入的演示。你只需要注释前面的配置,并使下面的配置生效即可:

1
2
3
4
5
6
<!-- 导入p名称空间 -->
xmlns:p="http://www.springframework.org/schema/p"

<!-- 属性p名称空间注入 -->
<bean id="bean7" class="com.envy.demo.Bean7" p:id="001"></bean>
<bean id="bean67" class="com.envy.demo.Bean67" p:name="李四" p:age="18" p:bean7-ref="bean7"></bean>

SpEL注入

SpEL(Spring Expression Language),使用Spring表达式语言对依赖注入进行简化。语法是#{表达式},通常是<bean id="xxx" value="#{表达式}"> 。下面介绍一些常用的SpEL的用法:1、传递变量使用#{变量名称};2、传递字符串使用#{'字符串名称'};3、使用另一个Bean对象采用#{helloId2};4、使用指定名属性且使用方法#{world.print.toUpperCase()};5、使用静态字段或者方法#{T(java.lang.Math).PI}等等。

下面通过product和Category这两个类来演示SpEL的属性注入。

第一步,新建Product.java文件,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Product {
private String name;
private double price;
private Category category;

public String getName() {
return name;
}

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

public double getPrice() {
return price;
}

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

public Category getCategory() {
return category;
}

public void setCategory(Category category) {
this.category = category;
}

@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", price=" + price +
", category=" + category +
'}';
}
}

以及Category.java文件,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Category {
private String name;

public String getName() {
return name;
}

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

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

第二步,往applicationContext.xml文件中新增以下代码:

1
2
3
4
5
6
7
8
9
<!-- SpEL属性注入  -->
<bean id="product" class="com.envy.demo.Product">
<property name="name" value="#{'童装'}"></property>
<property name="price" value="#{99.00}"></property>
<property name="category" value="#{category}"></property> <!-- 注意value的值必须是引用对象Bean的id或者name属性 -->
</bean>
<bean id="category" class="com.envy.demo.Category">
<property name="name" value="#{'001'}"></property>
</bean>

第三步,新增测试方法testBeanFour:

1
2
3
4
5
6
7
8
9
@Test
/**
* 属性SpELl方法注入(参数中包含引用对象)
* **/
public void testBeanFour(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Product product =(Product) applicationContext.getBean("product");
System.out.println(product);
}

运行结果:

1
Product{name='童装', price=99.0, category=Category{name='001'}}

现在只是单纯的使用到了引用对象的属性,接下来演示引用对象的方法注入。

可以新建一个ProductPrice.java文件:

1
2
3
4
5
public class ProductPrice {
public double CalcPrice(){
return Math.random()*99;
}
}

接着修改applicationContext.xml文件为:

1
2
3
4
5
6
7
8
9
10
11
<!-- SpEL属性注入  -->
<bean id="product" class="com.envy.demo.Product">
<property name="name" value="#{'童装'}"></property>
<property name="price" value="#{productPrice.CalcPrice()}"></property>
<property name="category" value="#{category}"></property> <!-- 注意value的值必须是引用对象Bean的id或者name属性 -->
</bean>
<bean id="productPrice" class="com.envy.demo.ProductPrice"></bean>

<bean id="category" class="com.envy.demo.Category">
<property name="name" value="#{'001'}"></property>
</bean>

最后运行一下测试方法testBeanFour,结果为:

1
Product{name='童装', price=21.540606263709023, category=Category{name='001'}}

复杂类型的属性注入

复杂类型是指属性为数组、List、Map、Set、Properties类型。

新建一个CollectionBean.java文件,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class CollectionBean {
private String[] stringArray; //数组类型
private List<String> stringList; //列表类型
private Set<String> stringSet; //集合类型
private Map<String,Integer> stringIntegerMap; //map类型
private Properties properties; //属性类型

public String[] getStringArray() {
return stringArray;
}

public void setStringArray(String[] stringArray) {
this.stringArray = stringArray;
}

public List<String> getStringList() {
return stringList;
}

public void setStringList(List<String> stringList) {
this.stringList = stringList;
}

public Set<String> getStringSet() {
return stringSet;
}

public void setStringSet(Set<String> stringSet) {
this.stringSet = stringSet;
}

public Map<String, Integer> getStringIntegerMap() {
return stringIntegerMap;
}

public void setStringIntegerMap(Map<String, Integer> stringIntegerMap) {
this.stringIntegerMap = stringIntegerMap;
}

public Properties getProperties() {
return properties;
}

public void setProperties(Properties properties) {
this.properties = properties;
}

@Override
public String toString() {
return "CollectionBean{" +
"stringArray=" + Arrays.toString(stringArray) +
", stringList=" + stringList +
", stringSet=" + stringSet +
", stringIntegerMap=" + stringIntegerMap +
", properties=" + properties +
'}';
}
}

接着注释掉applicationContext.xml文件中的其他代码,添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!-- 复杂(集合)类型的属性注入 -->
<bean id="collectionBean" class="com.envy.demo.CollectionBean">
<!-- 数组类型属性注入 -->
<property name="stringArray">
<list>
<value>数组1</value>
<value>数组2</value>
<value>数组3</value>
</list>
</property>

<!-- List类型属性注入 -->
<property name="stringList">
<list>
<!-- 普通属性用value -->
<value>List1</value>
<value>List2</value>
<!-- 对象属性使用ref -->
<!--<ref>List3</ref>-->
<!--<ref>List4</ref>-->
</list>
</property>

<!-- Set类型属性注入 -->
<property name="stringSet">
<set>
<value>Set1</value>
<value>Set2</value>
<value>Set3</value>
</set>
</property>

<!-- Map类型属性注入 -->
<property name="stringIntegerMap">
<map>
<entry key="map1" value="01"></entry>
<entry key="map2" value="02"></entry>
<entry key="map3" value="03"></entry>
</map>
</property>

<!-- Properties类型属性注入 -->
<property name="properties">
<props>
<prop key="username">hello</prop>
<prop key="password">world</prop>
<prop key="update">good</prop>
</props>
</property>
</bean>

最后新建一个测试testBeanFive方法,里面的代码如下:

1
2
3
4
5
6
7
8
9
@Test
/**
* 复杂类型的属性注入
* **/
public void testBeanFive(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
CollectionBean collectionBean =(CollectionBean) applicationContext.getBean("collectionBean");
System.out.println(collectionBean);
}

运行结果为:

1
CollectionBean{stringArray=[数组1, 数组2, 数组3], stringList=[List1, List2], stringSet=[Set1, Set2, Set3], stringIntegerMap={map1=1, map2=2, map3=3}, properties={password=world, username=hello, update=good}}

复杂属性注入一般在进行框架整合的时候才会使用,而用的较多的属性则是properties和map。

Spring管理Bean(注解方式)

使用注解定义Bean

由于注解方式和XML配置方式差异较大,因此新建一个Spring项目SpringIOCAnnotation。在pom.xml文件中以下依赖:

1
2
3
4
5
6
spring-context
spring-core
spring-beans
spring-expression
spring-aop(注解方式必须导入)
junit

接着新建applicationContext.xml文件,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<!--开启注解扫描-->
<context:component-scan base-package="com.envy.demo"/>
<context:annotation-config/>
</beans>

然后继续新建一个UserService.java文件,里面的代码如下:

1
2
3
4
5
6
@Component("userService")
public class UserService {
public String sayHello(String name){
return "hello" +name;
}
}

接着新建SpringDemoTest.java文件,里面的代码如下:

1
2
3
4
5
6
7
8
9
public class SpringDemoTest {
@Test
public void testOne(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService");
String result = userService.sayHello("张三");
System.out.println(result);
}
}

最后执行testOne测试方法,结果如下:

1
hello张三

Spring自2.5版本后使用注解去定义Bean,@Component用于描述Spring框架中的Bean,当然除了@Component外,Spring提供了3个功能基本和@Component一样的注解。其中@Repository用于对DAO层实现类进行标注;@Service用于对Service层实现类进行标注;@Controller用于对Controller层实现类进行标注,其实这三个类的作用就是细化了@Component的作用,使得标注类的用途变得更加清楚,Spring在后续版本会对其进行增强。由于本例中的UserService文件是用于书写业务逻辑,因此此处的@Component通常会使用@Service来代替使用。

使用注解方式实现Spring属性注入

前面也说过属性分普通属性和引用属性,因此使用注解方式实现Spring属性注入时也存在一些区别。先介绍普通属性的注入方法。给UserService类新增一个eat方法和something属性:

1
2
3
4
5
private String something;

public String eat(){
return "正在eat:"+something;
}

然后在something上面使用@Value注解:

1
2
@Value("面条")
private String something;

然后新建一个测试方法testTwo,里面的代码如下:

1
2
3
4
5
6
7
@Test
public void testTwo(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService");
String result = userService.eat();
System.out.println(result);
}

运行结果为:

1
正在eat:面条

发现没有?此时不需要给something提供setter方法就能实现属性注入,注意如果该属性没有提供setter方法,那么这个@Value("面条")就需要添加到属性上;如果存在setter方法则@Value("面条")需要加载setter方法上:

1
2
3
4
@Value("面条")
public void setSomething(String something){
this.something=something;
}

在实际开发中这种提供简单属性值传入的方式其实并不常见,因为Service层需要调用Dao层的数据,因此需要考虑如何把Dao层的对象注入进来,这里考虑如何将Dao层中的某个方法注入到Service层中,然后被Service调用。

新建一个UserDao.java文件,里面的代码如下:

1
2
3
4
5
6
@Repository("userDao")
public class UserDao {
public void save(){
System.out.println("这是UserDao中的save方法");
}
}

接着在UserService.java文件中使用@Autowired来自动注入UserDao对象:

1
2
3
4
5
6
7
@Autowired
private UserDao userDao;

public void save(){
System.out.println("这是UserService中的save方法");
userDao.save();
}

通过@Autowired实现了UserDao对象的自动注入后,就可以在Service中调用了。然后新建一个测试方法testThree,里面的代码如下:

1
2
3
4
5
6
@Test
public void testThree(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService");
userService.save();
}

运行结果为:

1
2
这是UserService中的save方法
这是UserDao中的save方法

这里我们使用了@Autowired实现了对象的自动注入,注意@Autowired默认使用类型注入,如果存在两个Bean类型相同,则会按照Bean的名称进行注入。什么意思,意思就是你修改了@Repository("userDao")中的”userDao”,依然是可以实现对象注入,因为只要是UserDao类型private UserDao userDao;及UserDao类,无论你@Repository("userDao")括号中的字符串是什么,都是可以实现自动注入的,因为@Autowired默认使用类型注入,它们只需要保证类型相同就可以。@Autowired注入时可以针对成员变量或者setter方法。

当然如果你需要使用名称注入,可以通过@Autowired的required属性用于设置一定要找到匹配的Bean的名称,然后再使用@Qualifier注解指定注入的Bean名称:

1
2
3
@Autowired
@Qualifier("userDao")
private UserDao userDao;

注意此时@Qualifier("userDao")中的userDao必须与@Repository("userDao")中的userDao保持一致,否则运行会出错。即使用@Qualifier指定Bean名称后,注解Bean必须指定相同名称。

Spring提供对JSR-250中定义@Resource标准注解的支持。@Resource@Autowired注解的功能相似,@Resource会强制按照名称进行注入。也就是说 @Resource(name="userDao")的作用与下面两行代码的作用是一致的:

1
2
@Autowired
@Qualifier("userDao")

因此在大多数情况下我们会使用@Resource注解,达到根据Bean名称注入的目的。

Spring的其他注解

新建一个Bean.java文件,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component("bean")
public class Bean {

@PostConstruct
public void init(){
System.out.println("this is init method");
}

public void hello(){
System.out.println("this is hello method");
}

@PreDestroy
public void destory(){
System.out.println("this is destory method");
}
}

接着新建一个测试方法testFour:

1
2
3
4
5
6
7
@Test
public void testFour(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean bean = (Bean) applicationContext.getBean("bean");
bean.hello();
((ClassPathXmlApplicationContext) applicationContext).close();
}

运行该测试方法结果如下:

1
2
3
this is init method
this is hello method
this is destory method

使用注解配置的Bean和配置的一样,默认作用范围都是singleton,当然还可以使用@Scope注解用于指定Bean的作用范围。

新建Bean2.java文件,里面的代码如下:

1
2
3
@Component("bean2")
public class Bean2 {
}

接着新建一个测试方法testFive,代码为:

1
2
3
4
5
6
7
8
@Test
public void testFive(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Bean2 bean1 = (Bean2) applicationContext.getBean("bean2");
Bean2 bean2 = (Bean2) applicationContext.getBean("bean2");

System.out.println(bean1==bean2);
}

运行结果为:

1
2
this is init method
true

注意只要Bean被加载到容器中时,@PostConstruct注解的方法就会被调用。从上述运行结果可知,scope默认是singleton模式(单例模式),尝试使用@Scope注解将Bean的作用范围变为prototype(多例模式):

1
2
3
4
@Component("bean2")
@Scope("prototype")
public class Bean2 {
}

然后再次运行testFive测试方法,结果为:

1
2
this is init method
false

传统XML配置和注解配置混合使用

XML配置的优势是结构清晰,易于阅读;而注解方式的优点是开发迅速,属性注入更加方便。传统XML配置和注解配置混合使用需要两个步骤:1、引入context命名空间;2、在配置文件中添加context:annotation-config标签。

为了演示传统XML配置和注解配置混合使用,先通过单纯的XML配置方式,其次是混合使用XML配置和注解配置。

新建ProductDao.java文件,里面的代码如下:

1
2
3
4
5
public class ProductDao {
public void say(){
System.out.println("this is say method of ProductDao");
}
}

接着新建CategoryDao.java文件,里面的代码如下:

1
2
3
4
5
public class CategoryDao{
public void say(){
System.out.println("this is say method of CategoryDao");
}
}

然后新建ProductService.java文件,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ProductService {
private ProductDao productDao;

private CategoryDao categoryDao;

public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}

public void setCategoryDao(CategoryDao categoryDao) {
this.categoryDao = categoryDao;
}

public void say(){
System.out.println("this is say method of ProductService");
productDao.say();
categoryDao.say();
}
}

接着打开applicationContext.xml文件,新增以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
<bean id="productService" class="com.envy.demo.ProductService">
<property name="categoryDao" ref="categoryDao"></property>
<property name="productDao" ref="productDao"></property>
</bean>

<bean id="categoryDao" class="com.envy.demo.CategoryDao">

</bean>

<bean id="productDao" class="com.envy.demo.ProductDao">

</bean>

最后新建测试方法testOne,方法内的代码为:

1
2
3
4
5
6
@Test
public void testOne(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
ProductService productService = (ProductService) applicationContext.getBean("productService");
productService.say();
}

运行结果为:

1
2
3
this is say method of ProductService
this is say method of ProductDao
this is say method of CategoryDao

这是单纯的XML配置方式,接下来想让XML配置只是用来管理类,而属性注入采用注解的方式,只需将applicationContext.xml文件的property标签内的信息注释掉,只保留以下代码:

1
2
3
4
5
6
7
8
<bean id="productService" class="com.envy.demo.ProductService">
</bean>

<bean id="categoryDao" class="com.envy.demo.CategoryDao">
</bean>

<bean id="productDao" class="com.envy.demo.ProductDao">
</bean>

既然是使用属性注入,那么去掉ProductService文件中的setter方法,还需要在添加@Resource注解:

1
2
3
4
@Resource(name="productDao")
private ProductDao productDao;
@Resource(name="categoryDao")
private CategoryDao categoryDao;

当然仅仅是这样是无法是注解生效的(因为我们对类使用了XML配置,不能配置@Component注解),需要打开applicationContext.xml文件,添加<context:annotation-config/>,该标签表示打开了属性注入功能。而前面由于开启了包扫描<context:component-scan base-package="com.envy.demo"/>其实相当于不仅仅开启了属性注解,还可以使用类注解,而这个<context:annotation-config/>标签仅仅是开启了属性注解,因此对于类注解的@Component@Controller@Serivice等是无法使用的。

最后运行testOne方法,运行结果为:

1
2
3
this is say method of ProductService
this is say method of ProductDao
this is say method of CategoryDao