依赖注入简介
依赖注入是一个很常用的词。Java新手常会写出如下的代码,直接在写一个类的时候让它自己初始化自己。但是这不是一个好办法。一个类的状态应该由创建它的类决定,不能由自己决定自己。因此更好的办法是交给构造方法来初始化。
public class User {
private long id;
private String username;
private String password;
private LocalDate birthday;
public User() {
id = 1;
username = "yitian";
password = "123456";
birthday = LocalDate.now();
}
}
也就是改成下面这样。这样一来,类不在由自己初始化自己,而是交给它的创造者处理,这就叫做控制反转,英文是(Inverse of Controll,简称IoC)。另外,由于数据由外界传入,所以这种方式又叫做依赖注入。这种使用构造方法注入的方式就叫做构造器注入。当然相应的还有使用Setters方法的依赖注入。这两种方式是最基本的,在此基础上例如Spring框架还提供了高级的基于注解的依赖注入等方式。
public User(long id, String username, String password, LocalDate birthday) {
this.id = id;
this.username = username;
this.password = password;
this.birthday = birthday;
}
使用依赖注入的好处很明显。假如我们正在实现一个复杂的系统,需要将业务对象(比如上面的User对象)进行一些业务操作,然后用JDBC保存到数据库中。传统方式下,我们需要手动控制这些对象之间的关系。这样一来代码就耦合在一起,难以调试和维护。如果使用依赖注入方式,业务对象和数据库连接全部由IoC容器传入,我们要做的事情仅仅是处理业务逻辑。这样一来,数据的流入流出全部由依赖注入容器管理,我们编码不仅方便了,而且代码的可维护性也极大提高了。如果对此还有疑问的话,可以自己尝试不使用任何框架实现一个微型博客系统,然后在使用依赖注入重构一下。然后,你就会发现自己再也离不开依赖注入了。
配置Spring环境
添加依赖
一开始我用的是Spring Boot,它自动为我们做了几乎所有的配置工作。这样虽然方便,但是对于初学者来说可能会隐藏一些重要的细节。因此这里用Gradle来说明一下Spring依赖注入的配置过程。Spring模块化做得非常好,如果我们想要使用某个功能,只需要导入对应的模块,也就是Jar包即可。要使用依赖注入和上下文管理,我们要导入spring-core.jar
和spring-context.jar
这两个包。为了启用spring的单元测试支持,需要添加spring-test.jar
和junit-4.12.jar
。要运行最后面的Hibernate小例子,需要添加MySQL驱动和Hibernate核心包。在Gradle中,也就是简单地在配置文件中添加如下几行。springVersion
是现在最新的稳定版Spring版本,值为'4.3.5.RELEASE'
。
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
compile group: 'org.springframework', name: 'spring-core', version: springVersion
compile group: 'org.springframework', name: 'spring-context', version: springVersion
compile group: 'org.springframework', name: 'spring-test', version: springVersion
compile group: 'org.hibernate', name: 'hibernate-core', version: '5.2.6.Final'
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.40'
}
使用到的类
我们来用几个类简单模拟一下教师上课的情景,Getter、Setter、构造器等方法均省略了。首先需要一个教师类:
public class Teacher {
private String name;
private int age;
}
然后需要一个学生类:
public class Student {
private String name;
private int age;
}
最后需要一个教室类:
public class Classroom {
private Teacher teacher;
private List<Student> students;
}
为了能方便的创建学生,还需要一个工厂类:
public class StudentFactory {
public static Student getStudent(String name, int age) {
return new Student(name, age);
}
public static List<Student> getStudents() {
List<Student> students = new ArrayList<>();
students.add(getStudent("男生甲", 16));
students.add(getStudent("男生乙", 15));
students.add(getStudent("女生甲", 15));
return students;
}
}
基于XML的配置
使用最广泛和传统的方式就是XML文件配置了。Spring对于配置文件的名称没有固定要求。一个Spring XML配置文件应该类似下面这样。我们要配置依赖注入,也就是在下面添加各种各样的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.xsd">
</beans>
定义Bean
Spring中Bean的意思就是我们需要进行依赖注入配置的类,比如JDBC连接、Hibernate的SessionFactory以及其它程序中会用到的类。要定义一个可以被注入到其他地方的Bean,只要在beans父节点下添加一个bean节点,指明要定义的Bean名称和类即可。
<bean id="student"
class="yitian.learn.ioc.Student"/>
除了直接定义Bean之外,还可以由静态工厂方法生成Bean。这时候需要额外指定一个属性factory-method指明要使用的静态工厂方法。
<bean id="someStudents"
class="yitian.learn.ioc.StudentFactory"
factory-method="getStudents"/>
构造器注入
构造器注入需要Bean有相应的构造器。构造器注入类似下面这样,定义一个bean节点,id属性指明该Bean的唯一标识符,class属性指定Bean对应的类名。然后在Bean节点中指明构造器的参数类型和值。对于基本类型和字符串等类型,直接将值写在双引号中即可。
<bean id="maleStudent" class="yitian.learn.ioc.Student">
<constructor-arg type="int" value="18"/>
<constructor-arg type="java.lang.String" value="一个男生"/>
</bean>
上面是按照构造器参数类型来进行的依赖注入,如果构造器有相同的类型,上面的注入就无法进行了。这时候可以按照参数的索引来进行注入。
<bean id="femaleStudent" class="yitian.learn.ioc.Student">
<constructor-arg index="0" value="一个女生"/>
<constructor-arg index="1" value="16"/>
</bean>
还可以使用c命名空间来简化构造器注入的编写。要使用c命名空间,需要在根节点beans上添加如下属性声明:
xmlns:c="http://www.springframework.org/schema/c"
然后构造器注入可以写为如下形式:
<bean id="englishTeacher" class="yitian.learn.ioc.Teacher"
c:name="英语老师"
c:age="32"/>
c命名空间也支持按照参数索引的方式注入,这时候的语法稍微有点奇怪,由于XML不支持数字开头的属性名,因此需要以下划线开头。
<bean id="mathTeacer" class="yitian.learn.ioc.Teacher"
c:_0="数学老师"
c:_1="54"/>
属性注入
除了构造器注入之外,还有属性注入,也就是Setter注入。和构造器注入类似,只不过Bean里面使用property节点指定属性和值。
<bean id="anotherMaleStudent" class="yitian.learn.ioc.Student">
<property name="name" value="另一个男生"/>
<property name="age" value="15"/>
</bean>
对于每一个属性都要编写一个property节点,稍嫌麻烦。Spring因此提供了p命名空间用来简化属性注入的编写。要使用p命名空间,需要在根节点beans上添加一行属性声明:
xmlns:p="http://www.springframework.org/schema/p"
使用p命名空间,Bean定义会简化不少。不过p命名空间也有缺点,p命名空间的类型需要在运行时动态读取,性能相比于上面的传统属性注入有所降低。所以如果对于性能要求很高,还是使用传统property节点最好。
<bean id="anotherFemaleStudent" class="yitian.learn.ioc.Student"
p:name="另一个女生"
p:age="16"/>
注入其他Bean
上面几个例子都是在Bean中注入简单类型。当然,也可以在Bean中注入其他Bean,只需要使用ref属性指向已定义的一个Bean的id。
<bean id="mathClassroom" class="yitian.learn.ioc.Classroom">
<property name="teacher" ref="mathTeacer"/>
<property name="students" ref="someStudents"/>
</bean>
前面几种注入方式自然也可以注入Bean,只需要将value属性改为ref属性即可。如果使用命名空间的话,就写为p:name-ref="someBean"
这样的方式即可。
使用集合
在Bean中除了单个参数之外,还可以使用集合。支持的集合包括<list/>
、<map/>
、<set/>
以及<props/>
,对应于Java的集合List
、Map
、Set
以及Property
。
<bean id="englishClassroom" class="yitian.learn.ioc.Classroom">
<property name="teacher" ref="englishTeacher"/>
<property name="students">
<list>
<ref bean="anotherFemaleStudent"/>
<ref bean="anotherMaleStudent"/>
<ref bean="femaleStudent"/>
<ref bean="maleStudent"/>
</list>
</property>
</bean>
空值处理
如果我们需要一个空字符串,XML可以写为如下这样:
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>
如果我们需要一个空引用,可以写为如下这样:
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>
延迟加载
默认情况下,ApplicationContext会在初始化的时候创建和配置所有Bean。这样做的优点是如果Bean配置有错误,我们可以立即发现这些错误。不过有时候可能会需要延迟加载,将这些Bean的创建延迟到真正使用它的时候。要这么做很简单,只需要在bean上添加lazy-init="true"
即可。
<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
如果要让所有Bean都延迟加载,可以在配置文件的根节点beans上添加default-lazy-init="true"
。
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>
Bean作用域
默认情况下Bean的作用域是单例,这就是说在整个应用程序中每次获得的Bean,完全就是同一个对象。这样一来,我们只要使用Spring依赖注入,就完全不需要实现单例模式了。Spring会帮我们把Bean设置成单例的。
除了单例之外,还有一种常用的作用域——原型。原型作用域会在每次请求Bean的时候创建一个新对象。这种作用域用来定义有状态的Bean,比如用户会话。每次请求用户会话,都会返回一个新的会话,每个用户的会话因此不同。Spring IoC容器只负责创建和分配原型Bean,销毁工作需要由请求方进行。
使用Bean
前面说了这么多XML配置来定义Bean,下面来看看如何使用Bean。先来介绍一下ApplicationContext接口。
Spring项目中,ApplicationContext可能是最重要的接口之一了。这个接口充当着应用程序环境的作用,我们在定义好了Bean之后,就可以通过ApplicationContext来获取相应的Bean。对于使用Java配置或者XML配置,以及其它环境例如Web应用程序等,都有相应的ApplicationContext。在真正使用Spring依赖注入的时候,我们一般情况下根本不需要关心ApplicationContext接口,它会由底层自动创建和使用。
下面我们使用ApplicationContext来获取前面我们定义的Bean实例。这里用到了ClassPathXmlApplicationContext,一个接受类路径上的XML文件作为配置文件的ApplicationContext。要获取某个Bean,只需要调用ApplicationContext的getBean方法,这个方法接受Bean的id,以及Bean的类型。
public void testXmlConfig() {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
Student maleStudent = context.getBean("maleStudent", Student.class);
System.out.println(maleStudent);
Teacher mathTeacher = context.getBean("mathTeacher", Teacher.class);
System.out.println(mathTeacher);
Classroom mathClassroom = context.getBean("mathClassroom", Classroom.class);
System.out.println(mathClassroom);
Classroom englishClassroom = context.getBean("englishClassroom", Classroom.class);
System.out.println(englishClassroom);
}
基于代码的配置
Spring提供了多种方式来配置依赖注入,最常用和传统的方式就是使用XML文件进行配置。但是使用XML文件配置有一些弊端。如果看过一点Spring文档,就会发现几乎一半以上的篇幅都在介绍XML配置。为了灵活的实现各种依赖注入,Spring提供了一个强大的XML配置,但是这也同时使得XML配置变得复杂。所以现在基于代码的配置越来越流行,这种配置使用普通的Java方法和Spring提供的注解,让依赖注入配置变得非常方便。
当然XML配置和代码配置相比,并不存在绝对的优劣问题。XML配置将配置和代码分离,让我们不需要重新编译就可以更改配置。而代码配置避免了繁复的XML节点编写,但是相应的配置类更难管理。具体使用哪种方式,还得取决于开发者的习惯和综合考虑。由于代码配置在XML配置之前,所以XML配置有可能会覆盖代码配置。
定义Bean
定义Bean很简单,首先定义一个类,然后使用@Configuration注解,这样Spring就会将这个类识别为一个配置类,并从中寻找Bean定义。在一个@Configuration类中可以定义多个以@Bean注解的方法,在这些方法中我们可以通过普通的Java代码来初始化一个对象,然后返回这个对象。Spring用这些方法的名称作为返回的Bean的名称。当然还可以自定义Bean名称,这需要在@Bean注解中添加一个name参数,可以接受一组名称。还可以使用@Description注解为Bean添加一段描述信息。
由于是Java代码来配置Bean,所以前面那些XML配置大部分都不需要了。我们只要像写普通代码那样来写这些Bean代码即可。如果需要引用其他Bean,使用注解将Bean注入到所在类即可。
@Configuration
public class SpringConfig {
@Autowired
private Teacher englishTeacher;
@Autowired
private List<Student> someStudents;
@Bean(name = {"englishClassroom"})
@Description("英语教室")
public Classroom englishClassroom() {
return new Classroom(englishTeacher, someStudents);
}
@Bean
public List<Student> someStudents() {
return StudentFactory.getStudents();
}
@Bean
public Teacher englishTeacher() {
return new Teacher("英语老师", 32);
}
}
使用Bean
我们同样使用ApplicationContext来获取Bean。这里使用AnnotationConfigApplicationContext,从一个标记了@Configuration的配置类中读取Bean对象。
public void testJavaConfig() {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
Classroom englishClassroom = context.getBean("englishClassroom", Classroom.class);
System.out.println(englishClassroom);
}
使用注解获取Bean
前面我们用了ApplicationContext来获取Bean。但是在实际应用中,完全不需要这么做。ApplicationContext是一个底层接口,Spring会将其封装起来。我们可以使用注解,透明地获取Bean。这也是我们以后使用Spring依赖注入的主要方式。最常用的注解是Autowired。
Autowired注解
定义好Bean之后,我们就可以通过使用自动装配来使用Bean了。Spring框架扫描到标记了Autowired注解的字段、构造器、Setter方法之后,就会从Bean定义中搜索对应的Bean来进行注入。Autowired首先会按照类型进行查找,如果发现同类型的多个Bean,就会按照名称进行匹配。如果既没有同类型的Bean也没有相同名称的Bean,Spring就会抛出异常。
@ContextConfiguration(classes = {SpringConfig.class})
@RunWith(SpringRunner.class)
public class IoCTest {
@Autowired
private Classroom englishClassroom;
@Test
public void testTestContext() {
assertNotNull(englishClassroom);
}
}
@Autowired可以应用在字段、Setter方法和构造器上。Spring官方建议我们将其应用在Setter方法和构造器上,最好不要直接注入到字段中,除非是在单元测试这种情况下。
Primary注解和Qualifier注解
在自动装配中,如果想要同类型的某个Bean优先被使用,可以向其添加Primary注解。这样Spring在自动装配的时候,就会优先使用Primary注解的那个Bean,即使这个Bean名称不匹配。
如果还想要具体的控制到底使用哪个Bean,还可以使用Qualifier注解。在定义Bean的时候,使用Qualifier注解为同类型的Bean提供一个名称:
@Configuration
public class BeanConfiguration {
@Bean
@Qualifier("mathTeacher")
public Teacher mathTeacher() {
return new Teacher("数学老师", 46);
}
}
然后再使用Bean的时候,也用Qualifier注解,就可以指定注入哪个Bean了。
@ContextConfiguration(classes = {SpringConfig.class})
@RunWith(SpringRunner.class)
public class AnnotationTest {
@Autowired
//如果同类型存在多个Bean,优先使用@Primary
private Teacher teacher;
@Autowired
@Qualifier("mathTeacher")
//注入相同Qualifier的Bean
private Teacher qualifierTeacher;
@Test
public void testQualifier() {
assertEquals("数学老师", qualifierTeacher.getName());
}
}
例子
说了这么多,我们来看一个Hibernate的例子。我们将Hibernate的SessionFactory注册成Spring Bean,这样就不需要我们使用单例模式或者静态类什么的了。这个例子很简单。首先我们添加一个Hibernate配置文件hibernate.cfg.xml
。
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost:3306/test</property>
<property name="connection.username">root</property>
<property name="connection.password">12345678</property>
<property name="dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="show_sql">true</property>
<property name="hbm2ddl.auto">create</property>
</session-factory>
</hibernate-configuration>
然后,我们将SessionFactory注册为一个Spring Bean。
@Configuration
public class HibernateConfig {
@Bean
public SessionFactory sessionFactory() {
final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.configure()
.build();
try {
SessionFactory sessionFactory = new MetadataSources(registry).buildMetadata().buildSessionFactory();
return sessionFactory;
} catch (Exception e) {
StandardServiceRegistryBuilder.destroy(registry);
throw new RuntimeException(e);
}
}
}
最后,放到单元测试中来测一下。如果配置文件路径和配置都正确的话,就可以正确通过。这样,我们就将Hibernate的SessionFactory注册为了一个Spring Bean了。以后在项目中可以使用依赖注入直接注入SessionFactory了。
@ContextConfiguration(classes = {HibernateConfig.class})
@RunWith(SpringRunner.class)
public class HibernateTest {
@Autowired
private SessionFactory sessionFactory;
@Test
public void testSessionFactory() {
assertNotNull(sessionFactory);
}
}
参考资料
http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans
项目代码我托管在Csdn Code上了,有兴趣的同学可以看看。