Spring框架从2004年发布的第一个版本以来,如今已经迭代到5.x,逐渐成为JavaEE开发中必不可少的框架之一,也有人称它为Java下的第一开源平台。单从Spring的本身来说,它贯穿着整个表现层,业务层与持久层,它并没有取代其他框架的意思,而更多的是从整体上管理这些框架,降低系统的耦合性。系列文章将逐渐完成对Spring的学习,本篇首先学习它的一个核心机制:依赖注入,主要涉及内容如下:
- 理解依赖注入
- 理解Spring容器
- 配置和使用bean
一、理解依赖注入
在正式介绍依赖注入之前,我们先通过一个简单的Spring程序感受下Spring的一个最常用的功能。首先,导入必需的四个Spring包和一个它依赖的日志包:
- spring-beans-4.0.0.RELEASE.jar
- spring-context-4.0.0.RELEASE.jar
- spring-core-4.0.0.RELEASE.jar
- spring-expression-4.0.0.RELEASE.jar
- commons-logging-1.2.jar
然后创建两个类,
public class Person {
private String name;
private int age;
private Parents parents;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setParents(Parents parents) {
this.parents = parents;
}
}
public class Parents {
private String pFatherName;
private String pMotherName;
public void setpFatherName(String pFatherName) {
this.pFatherName = pFatherName;
}
public void setpMotherName(String pMotherName) {
this.pMotherName = pMotherName;
}
}
此时,我们的每个Person实例都在内部引用了一个Parents实例,那么当我们想要获取Person对象的时候,就需要首先初始化一个Parents实例给Person对象,程序如下:
public static void main(String[] args){
Parents p = new Parents();
p.setpFatherName("father");
p.setpMotherName("mother");
Person person = new Person();
person.setName("single");
person.setAge(22);
person.setParents(p);
}
传统的写法,我们构建一个person实例需要这么多行代码,而Spring给我们带来的改变就是,通过构建Spring容器,所有的对象都会在容器中生成,外部只需要向容器索要对象即可。例如:
//新建Spring配置文件,命名为application.xml
//此处省略XML头文件部分
<bean id="parents" class="MyPackage.Parents">
<property name="pFatherName" value="father"/>
<property name="pMotherName" value="mother"/>
</bean>
<bean id="person" class="MyPackage.Person">
<property name="name" value="single"/>
<property name="age" value="22"/>
<property name="parents" ref="parents"/>
</bean>
然后看我们的主程序如何获取person实例:
public static void main(String[] args){
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Person person = (Person) context.getBean("person");
}
一共两行代码,第一行代码用于创建Spring容器,第二行代码用于获取容器中名为person的Person实例。在Spring配置文件加载的时候,容器会初始化所有bean,也就是说所有配置在容器中的bean都会被创建实例并保存在容器中等待调用。依赖注入就是说,在容器加载结束之后,所有实例的属性都被容器注入相应的值,不需要程序员手动去赋值。而在外部获取该实例的时候不需要知道容器是如何为各个属性注入值的。如果某个bean关联了其他bean,那么容器也会为它自动注入其他bean的引用。
依赖注入主要有两种方式,一种是设值注入,另一种是构造注入,我们将在介绍bean的配置的时候详细学习。
二、理解Spring容器
Spring容器就相当于一个大的工厂一样,所有的bean都是工厂中的产品。BeanFactory是Spring容器的最基本的接口,它负责配置、创建和管理Bean,该接口中有如下几个方法:
- Object getBean(String var1):返回容器中id为var1的实例
- <T>T getBean(String var1, Class<T> var2):返回容器中id为var1的实例,并转换该实例类型为var2
- boolean containsBean(String var1):返回容器中是否包含id为var1 的实例
- Class<?> getType(String var1):返回容器中id为var1的实例的所属类型
ApplicationContext是BeanFactory的子接口,它扩展了BeanFactory,提供更加完善的容器操作,对于大部分的JavaEE应用来说,使用ApplicationContext作为Spring容器。它也有几个常用的实现类:
- FileSystemXmlApplicationContext:表示从文件系统中加载配置文件并创建Spring容器
- ClassPathXmlApplicationContext:表示从应用的类路径(src)下加载配置文件并创建Spring容器
- XmlWebApplicationContext和AnnotationConfigWebApplicationContex:它们主要应用于web应用中
一般来说,首选ApplicationContext作为Spring容器,除非在一些对内存要求比较苛刻的应用中才考虑使用BeanFactory。
三、配置和管理Bean
在Spring的配置文件中,我们使用bean标签配置一个实例对象。例如:
<bean id="person" class="MyPackage.Person">
</bean>
通常来说,我们配置bean的时候会指定一个id属性值和一个class属性值,id属性用于标识该实例,是该实例在容器中的唯一标识,class属性指向该实例的类型。显然框架会利用反射根据这个class属性值调用newInstance方法创建该类的一个实例对象。
下面我们学习下bean的作用域,
容器中的bean支持以下几种不同范围的作用域:
- singleton:单例模式,在整个容器中,该bean只会被创建一次。
- prototype:该模式指定,每次外部调用getBean获取该实例的时候,都会创建一个新的实例对象。
- request:在同一次http请求中,程序请求该bean将会得到同一个实例。
- session:在同一次http会话期间,程序对该bean的请求将会得到同一个实例。
- global session:在全局的session会话期间对应同一个实例。
Spring中默认bean的作用域为singleton,即每个bean只会在容器中被创建一次。例如:
/*配置一个Date实例*/
<bean id="date" class="java.util.Date" scope="singleton">
</bean>
/*获取该实例*/
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Date date = (Date) context.getBean("date");
Thread.sleep(1000);
Date date2 = (Date) context.getBean("date");
System.out.println("date:"+date);
System.out.println("date2:"+date2);
输出结果:
date:Thu Nov 02 13:35:15 CST 2017
date2:Thu Nov 02 13:35:15 CST 2017
显然,这两次获取的date是同一实例对象,我们修改bean的配置:
<bean id="date" class="java.util.Date" scope="prototype">
</bean>
再次执行程序:
date:Thu Nov 02 13:39:09 CST 2017
date2:Thu Nov 02 13:39:10 CST 2017
显然,两个date的值并不相等,所以它们并不指向同一实例。当然,Spring中默认bean的作用域为singleton。
下面我们学习如何配置依赖关系,让容器为我们注入依赖,
根据注入方式的不同,Bean的依赖注入可以有如下两种形式:
- 设值注入:通过<property..>元素驱动Spring执行setter方法实现。
- 构造注入:通过<constructor-arg...>元素驱动Spring执行有参构造器完成注入。
我们首先看看设值注入的使用,
/*定义一个person类*/
public class Person {
private String name;
private int age;
public void setName(String name) {
System.out.println("调用setname方法注入name的值");
this.name = name;
}
public void setAge(int age) {
System.out.println("调用setage方法注入age的值");
this.age = age;
}
}
/*配置一个person类实例*/
<bean id="person" class="MyPackage.Person">
<property name="name" value="single"/>
<property name="age" value="22"/>
</bean>
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Person person = (Person) context.getBean("person");
System.out.println(person);
从容器中获取person实例,输出结果:
可以看到,当容器初始化完毕的时候,会通过反射创建每个bean元素所对应的实例,遇到property元素,Spring则会反射调用该实例的setter方法初始化这些属性值,也可以叫做注入依赖。这种方式的注入依赖,唯一需要的是类所对应的属性必须配置一个setter方法。
构造注入方式:
/*向person中增加一个有参构造器*/
public Person(String name,int age){
System.out.println("调用构造器注入属性值");
this.name = name;
this.age = age;
}
配置bean:
<bean id="person" class="MyPackage.Person">
<constructor-arg name="name" value="single"/>
<constructor-arg name="age" value="22"/>
</bean>
输出结果:
很显然,Spring在加载配置文件的时候,发现bean的配置中并没有<costructor-arg>元素,那么它将会驱动默认的构造器创建一个类实例,否则将<costructor-arg>元素的个数驱动具有相对应的构造器。
总的来说,两者都有优缺点,设置注入更容易理解和使用,构造注入只允许属性的值在构造实例的时候赋值,一旦实例构建完成,其属性将不具备修改的能力,使得依赖关系不易被破坏,但是大量重复臃肿的构造代码使得程序很笨重。一般建议以设值依赖为主,构造注入为辅。
Spring允许通过以下几种类型的的元素作为setter方法的参数传入类属性的setter方法中,
- 普通属性值
- 引用
- 内部bean
- 集合以及属性集
1、普通属性值
对于基本类型已经String类型的属性值,我们通过
<property name="" value="" />
直接将值填在value中即可,Spring调用XML的解析器将所有的String自动转换为对应的参数类型并传入setter方法中。
2、引用类型
我们定义一个Person类和一个Parents类:
public class Parents {
private String pFatherName;
private String pMotherName;
//省略setter方法
}
public class Person {
private String name;
private int age;
private Parents parents;
//省略setter方法
}
我们的Person实例内部有一个Parents 类型的属性,那么容器在注入的时候该如何将一个Parents 类型的实例注入到Person的parents属性中呢?
<bean id="parents" class="MyPackage.Parents">
<property name="pFatherName" value="father"/>
<property name="pMotherName" value="mother"/>
</bean>
<bean id="person" class="MyPackage.Person">
<property name="name" value="single"/>
<property name="age" value="22"/>
<property name="parents" ref="parents"/>
</bean>
我们通过<property>元素的ref属性配置这种引用依赖,首先容器会创建一个名为parents的Parents实例并为其内部的各个属性注入数值,接着容器会创建一个名为person的Person实例,并注入其普通属性值。当为其parents属性注入值的时候,传入的是已存在的实例parents的引用。
3、定义内部bean
如果某个bean不想被容器外部引用,而只想作为一个属性的值传入setter方法中,那么我们可以将它定义成内部bean。例如:
<bean id="person" class="MyPackage.Person">
<property name="name" value="single"/>
<property name="age" value="22"/>
<property name="parents">
<bean class="MyPackage.Parents">
<property name="pFatherName" value="father"/>
<property name="pMotherName" value="mother"/>
</bean>
</property>
</bean>
这样我们就配置了一个内部bean,该bean没有标识,用完就丢失对它的引用了。其实定义内部bean来给某个实例属性赋值和使用ref引用在本质上是一样的。
4、集合属性以及属性集
我们重新定义Person类如下:
public class Person {
private List<String> address;
private Map<String,String> creditCard;
private Set<String> hobbies;
//省略setter方法
}
各种类型的集合的配置如下:
<bean id="person" class="MyPackage.Person">
<property name="address">
<list>
<!--每个value,ref,bean都对应于list中的一个元素-->
<value>南京</value>
<value>上海</value>
<value>北京</value>
</list>
</property>
<property name="creditCard">
<map>
<!--每个entry元素对应于一个键值对,map中的-->
<entry key="中国银行" value="3231243234"></entry>
<entry key="江苏银行" value="5453454434"></entry>
</map>
</property>
<property name="hobbies">
<set>
<!--每个value,ref,bean都对应于list中的一个元素-->
<value>乒乓球</value>
<value>网球</value>
</set>
</property>
</bean>
属性集的配置和map的配置类似,使用键值对的形式进行配置:
<property name="properties">
<props>
<prop key="hello">single</prop>
</props>
</property>
至此,我们简单的介绍了Spring的依赖注入的一些基本的内容,有关bean的更高级的使用将在后续文章中进行介绍。总结不到之处,望指出!