文章标题
Spring
框架最厉害的是提出了两个概念,IOC
容器和AOP
切面编程。
IOC
容器的本质是个创建并管理对象的工厂,关于它的两个重要特性,是“控制反转”和“依赖注入”:
- 控制反转:这里的“控制”是指创建对象,这原本是程序员的权利,但是应用
IOC
容器以后,对象的创建权被容器拿走了,留给程序员做的工作,就是按照Spring
的规范,声明对象及对象间的依赖关系,这就是“反转”的含义。 - 依赖注入:对象内部的属性就是该对象的依赖,如果没有这些属性,对象就不能正常工作。程序员声明出对象间依赖关系后,
IOC
容器会在对象的生命周期内,负责为对象注入这些属性。
由IOC
容器创建并管理的对象都叫做Bean
,本文讲述Bean
的声明及实例化方法,以及依赖注入的方法。
Spring
声明对象及其依赖的方式有两种:XML
、注解。
平时应用中,可能注解的使用频率更高,但我觉的XML
更好理解,所以下文讲解都采用XML
声明方式。
Bean的声明及实例化
方法一:构造器
写个User
类定义:
package x.y;
public class User {
}
按照Spring
规范,声明如下:
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="user" class="x.y.User"></bean>
</beans>
其中,id
声明对象在容器中的唯一标识符,class
声明对象的全路径类名。
方法二:静态工厂方法
改造User
类定义,构造器私有化,提供静态工厂方法创建对象:
package x.y;
public class User {
private static User user = new User();
private User() {
}
public static User createInstance() {
return user;
}
}
Spring
声明如下:
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="user" class="x.y.User" factory-method="createInstance"></bean>
</beans>
factory-method
声明告诉容器,在进行实例化时,需调用内部工厂方法createInstance()
创建对象。这里有两点需要注意:
- 工厂方法的返回值必须是
class
声明的类型 - 工厂方法必须属于
class
声明的类
方法三:对象工厂方法
User
类定义回到最初的样子:
package x.y;
public class User {
}
新增Locator
工厂类定义,通过工厂方法提供User
类对象:
package x.y;
public class Locator {
private static User user = new User();
public User createUserInstance() {
return user;
}
}
Spring
声明如下:
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="locator" class="x.y.Locator"></bean>
<bean id="user" factory-bean="locator" factory-method="createUserInstance"></bean>
</beans>
声明中,locator
对象通过构造器创建,user
对象通过locator
的createUserInstance
方法创建。
Bean的依赖注入
注入对象的依赖属性,总体而言有两种方法:
- 通过对象构造器注入
- 通过对象的
setter
方法注入
构造器注入
对象需要提供有参构造器,将依赖属性通过参数传入。
根据构造器的参数类型及顺序,这里又分为两种情况。
构造参数无歧义
如下,通过构造参数类型就可以很明显的区分依赖对象:
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
XML
声明就可以如下:
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
</beans>
构造参数有歧义
有歧义的示例如下:
package examples;
public class ExampleBean {
private int years;
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
如果只看这个类的构造器,是看不出参数有什么歧义的。但是要考虑到,XML
的配置只能提供字符串类型的值,歧义就出现了。
比方说,XML
提供如下配置(错误的配置):
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg value="7500000"/>
<constructor-arg value="42"/>
</bean>
这两个值的注入,能分清哪一个是years
,哪一个是ultimateAnswer
吗?
如果有类似这样的歧义出现,需要使用如下更详细的声明方式:
提供构造参数的具体类型
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
提供构造参数的定义顺序(索引从0
开始)
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
提供构造参数的名称
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
注意!采用这种声明方式,代码编译时必须启用debug
模式!因为release
模式编译出来的字节码中,构造参数的名称会发现变化,导致Spring
的XML
声明对应不上!
如果必须采用release
模式编译,可以在Java
类构造器中通过注解java.beans.ConstructorProperties
明确定义出构造参数名称:
package examples;
public class ExampleBean {
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
setter方法注入
setter
注入比构造器注入要简单,因为setter
注入针对的都是单一属性,而构造器可以同时注入多个依赖属性。
两种注入方式的XML
声明配置都是一样的,只是Java
代码写法稍微不一样:
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}
对应的XML
声明:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- setter injection using the neater ref attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
构造器注入和setter注入的选择
Spring
官网文档提供的经验法则是:
- 强制依赖项通过构造器注入
- 可选依赖项通过
setter
注入
Spring
团队通常是提倡使用构造器注入的,因为它允许你将组件实现为不可变对象,并确保所有的依赖项都不为空。此外,通过构造器注入依赖的对象,总是能以完全初始化的状态返回给客户端调用。
采用setter
注入的一个好处是留有一个途径,以后可以重新注入依赖。
总体来说,还是要根据不同的特定类,选择最有意义的注入方式。比如说,某个第三方类不提供setter
方法的时候,就只能选择构造器注入的方式了。