Spring IOC容器和DI依赖注入
Spring概览
spring概述
Spring是一个于2003年兴起的轻量级的Java开源开发框架,由Rod Johnson在其著作Expert One-On-One J2EE Development and Design中阐述的部分理念和原型衍生而来。Spring让开发人员有更多的精力投入到业务逻辑开发中,而不需要将其应用程序绑定到特定的部署环境,是为了降低企业应用开发的复杂性而创建的。可以把Spring理解成一个大容器,这个容器可以整合现有的各种技术框架。Spring框架的主要优势是方便各种框架集成,降低了Java EE开发的难度。
Spring体系结构
Spring是一个大容器,可以集成各种技术。如图所示是Spring支持的各种技术。
下面依次介绍Spring支持的各种技术。
- Core Container
Spring的核心容器由Beans、Core、Context、SpEL等模块组成。所有Spring的其他模块都是建立在Core Container基础模块上的。该模块规定了创建和维护Bean的方式,提供了控制反转(IoC)和依赖注入(DI)等特性。
- Spring-beans模块:提供了BeanFactory,是工厂模式的一个经典实现,Spring将管理对象称为Bean。
- Spring-context模块:建立在Core和Beans模块的基础之上,提供一个框架式的对象访问方式,是访问定义和配置的任何对象的媒介。ApplicationContext接口是Context模块的焦点。
- Spring-context-support模块:支持整合第三方库到Spring应用程序上下文,特别是用于高速缓存(EhCache、JCache)和任务调度(CommonJ、Quartz)的支持。
- Spring-expression模块:提供了强大的表达式语言去支持运行时查询和操作对象图。这是对JSP 2.1规范中规定的统一表达式语言(UnifiedEL)的扩展。该语言支持设置和获取属性值、属性分配、方法调用、访问数组、集合和索引器的内容、逻辑和算术运算、变量命名以及从Spring的IoC容器中以名称检索对象。它还支持列表投影、选择以及常见的列表聚合。
- Data Access/Integration
数据访问/集成模块提供了对JDBC、ORM、OXM、JMS和Transaction等模块的集成。使主流的ORM框架,持久化框架和消息中间件可以很方便地集成到Spring中,降低开发人员对这些框架的维护成本,提升了开发效率。
- Spring-jdbc模块:提供了一个JDBC的抽象层,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析。
- Spring-orm模块:为流行的对象关系映射(Object-RelationalMapping)API提供集成层,包括JPA和Hibernate。使用Spring-orm模块可以将这些O/R映射框架与Spring提供的所有其他功能结合使用,例如声明式事务管理功能。
- Spring-oxm模块:提供了一个支持对象/XML映射的抽象层实现,例如JAXB、Castor、JiBX和XStream。
- Spring-jms模块(Java Messaging Service):指Java消息传递服务,包含用于生产和使用消息的功能。自Spring 4.1以后,提供了与Spring-messaging模块的集成。
- Spring-tx模块(事务模块):支持用于实现特殊接口和所有POJO(普通Java对象)类的编程和声明式事务管理。
- Web模块
Web模块提供了对Web开发相关技术的集成,对开发模型-视图-控制器(MVC)项目提供便利。
- Spring-web模块:提供了基本的Web开发集成功能,例如多文件上传功能、使用Servlet监听器初始化一个IoC容器以及Web应用上下文。
- Spring-webmvc模块:也称为Web-Servlet模块,包含用于Web应用程序的Spring MVC和REST Web Services实现。Spring MVC框架提供了领域模型代码和Web表单之间的清晰分离,并与Spring Framework的所有其他功能集成,本书后续章节将会详细讲解Spring MVC框架。
- Spring-websocket模块:Spring 4.0以后新增的模块,它提供了WebSocket和SockJS的实现。
- Portlet模块:类似于Servlet模块的功能,提供了Portlet环境下的MVC实现(已废弃)。
- AOP模块
AOP模块提供了AOP联盟提倡的面向切面编程的实现环境。AOP将代码按功能进行分离,降低了模块间代码的耦合度。
- Spring-aop模块:提供了一个符合AOP要求的面向切面的编程实现,允许定义方法拦截器和切入点,将代码按照功能进行分离,以便干净地解耦。
- Spring-aspects模块:提供了与AspectJ的集成功能,AspectJ是一个功能强大且成熟的AOP框架。
- Spring-instrument模块:提供了类植入(Instrumentation)支持和类加载器的实现,可以在特定的应用服务器中使用。
- Test模块
Test模块支持JUnit和TestNG等单元测试模块的集成,还提供了mock对象,使开发人员可以更加独立的测试代码。
新建Spring项目
新建Maven项目
在pom.xml中导入如下依赖:
1
2
3
4
5
6<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>在Java目录下新建JavaBean Student
在resource目录下新建spring.xml
新建测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14package cn.bytecollege.test;
import cn.bytecollege.bean.Student;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
Student student = (Student) context.getBean("student");
student.sayHello();
}
}运行测试类,可以看出成功调用了student对象的sayHello方法,也就是说,成功从Spring容器中获取了在Spring配置文件中配置的JavaBean。
Spring IOC容器和DI依赖注入
IOC的概念
IoC是Inversion of Control的简写,翻译成汉语就是“控制反转”。IoC并不是一门技术,而是一种设计思想。在没有IoC设计的场景下,开发人员在使用所需的对象时,需手动创建各种对象,如new Student()。
有了IoC这样的设计思想,在开发中,意味着将设计好的对象交给容器管理,而不再是像传统的编程方式中,在对象内部直接控制对象。
如何理解IoC呢?从IoC的字面意思上,无非是要把握好两块:控制、反转。下面将从这两方面着手分析IoC。
控制:由谁控制,控制了什么?
在传统Java程序设计中,开发人员直接在某个对象内部通过new关键字创建另一个对象,是开发人员主动去创建依赖对象;而IoC的设计思想,是通过专门的对象容器来创建和维护对象。于是就可以解答这个问题了。对于谁控制这个问题的回答是,由IoC容器来控制;对于控制了什么这个问题的回答是:控制了对象。
反转:什么是反转,反转了哪些方面?
在传统Java应用程序开发中,由开发人员在对象中主动控制其需要的依赖对象;而反转则是把对象依赖的过程颠倒了,即开发人员不再控制其依赖对象,而是由容器来帮助开发人员创建其需要的对象。于是就可以解答这个问题了。对于什么是反转这个问题的回答是,由容器帮开发人员创建依赖对象,对象只是被动地接受依赖对象,对象的控制权不再是开发人员,而是容器;对于反转了哪些方面这个问题的回答是,开发人员需要依赖的对象被反转。
Spring IOC容器
Spring IOC容器就是Spring存放JavaBean实例的容器,org.springframework.context.ApplicationContext接口代表 Spring IoC 容器,负责实例化配置和组装Bean,容器通过读取配置数据来获取有关要实例化,配置和组装哪些对象的指令。配置数据以XML,Java注解或者Java代码表示。
Spring IoC容器的设计主要是基于BeanFactory和ApplicationContext两个接口。
BeanFactory
BeanFactory由org.springframework.beans.factory.BeanFactory接口定义,它提供了完整的IoC服务支持,是一个管理Bean的工厂,主要负责初始化各种Bean。
BeanFactory在Spring中的地位举足轻重,因为在这个接口中规定了所有Spring容器的基本行为,以水桶为例,在商店中出售的水桶有大有小,制作材料也各不相同,有金属的、塑料的等,总之是各式各样的,但只要能装水,具备水桶的基本特性,那就可以作为水桶来出售,来让用户使用。BeanFactory就相当于规定水桶的基本行为,必须能装水,并且能从水桶中获取到水。
BeanFactory接口有多个实现类,其中比较常用的是org.springframework.beans.factory.xml.XmlBeanFactory,该类会根据XML配置文件中的定义来装配Bean。
首先查看一下BeanFactory接口源码:
1 | public interface BeanFactory { |
查看BeanFactory的区别可以看出其中的方法大概可以包含以下几类:
- getBean():这些方法都是用于从IOC容器中获取JavaBean,区别就是可以根据配置的名称获取也可以通过字节码对象获取
- containsBean():判断容器中是否包含某个Bean。
- isSingleton():判断该对象在容器中是否是单实例。
- isPrototype():判断该对象在容器中是否是原型(多实例)。
- isTypeMatch():判断该对象类型是否匹配,也可以理解为该对象是否是某个类型的对象。
- getType():获取该对象的类型。
- getAliases():获取该对象的别名。
下面通过示例学习BeanFactory的用法:
首先,新建一个JavaBean,代码如下:
1 | package cn.bytecollege.bean; |
接着,在resource目录中新建Spring配置文件,内容如下:
1 |
|
在上述配置代码中,bean元素是指配置需要让容器实例化的对象。
- id:相当于实例化对象后的对象名称
- class:需要实例化的类的全限定名
接着,新建测试类,创建Spring容器,并从容器中获取配置的对象:
1 | package cn.bytecollege.test; |
在上例的代码中,注释1处代码是使用Spring中Resource对象加载了classes路径下的配置文件。注释2处代码则是创建了一个对象工厂。接着通过BeanFactory的方法获取了Spring容器中的对象,并调用了对象的方法。
需要注意的是,直接使用BeanFactory创建容器的方式几乎在开发中不使用。使用的是后面章节中讲解的ApplicationContext子类容器。
ApplicationContext
ApplicationContext是一个高级形态意义的IoC容器,ApplicationContext在BeanFactory的基础上添加的附加功能,这些功能为ApplicationContext提供了以下BeanFactory不具备的新特性。
Spring提供了ApplicationContext接口的几种实现,在独立应用中通常创建以下两个容器实例:ClassPathXmlApplicationContext或FileSystemXmlApplicationContext的实例。
在大多数应用场景中,不需要实例化用户代码即可实例化一个Spring IOC容器的一个或者多个实例。例如在Web应用程序场景中,应用程序web.xml中进行简单配置,就可以自动创建一个IOC容器。
ClassPathXmlApplicationContext
从这个容器的名字即可看出,这个容器会从classe路径读取配置文件,从而创建一个Spring容器。继续使用2.2.1中的配置文件和JavaBean,创建一个ClassPathXmlApplicationContext容器。代码如下:
1 | public class Test { |
对比2.2.1中的代码可以发现,代码并没有重大的改变,仅仅是将Spring容器更换成了ClassPathXmlApplicationContext。
FileSystemXmlApplicationContext
这个容器使用方式和ClassPathXmlApplicationContext基本一致,唯一不同的是ClassPathXmlApplicationContext只能从项目的classes路径中去读取配置文件,FileSystemXmlApplicationContext则可以从绝对路径中读取配置文件,再次使用2.2.1中的JavaBean和配置文件创建FileSystemXmlApplicationContext容器。代码如下:
1 | public class Test { |
运行上例中的代码可以看出程序正常运行,并且调用了student对象的sayHello()方法。
Bean管理
Spring可以看作一个大型工厂,用于生产和管理Spring容器中的Bean。如果要使用这个工厂生产和管理Bean,需要开发者将Bean配置在Spring的配置文件中。Spring框架支持XML和Properties两种格式的配置文件,在实际开发中常用XML格式的配置文件或者使用注解的形式配置Bean。
在容器内,这些Bean被抽象成BeanDefinition对象,这个对象中包含了以下元数据:
- 包限定的类名:通常,定义了 Bean 的实际实现类。
- Bean 行为配置元素,用于声明 Bean 在容器中的行为(作用域,生命周期回调等)。
- 引用其他 bean 进行其工作所需的 bean。这些引用也称为协作者或依赖项。
该元数据转换为构成每个 bean 定义的一组属性。下表描述了这些属性:
属性 | 说明 |
---|---|
id | Bean在BeanFactory中的唯一标识,在代码中通过BeanFactory获取Bean实例时使用 |
Class | Bean的具体实现类,配置类的全限定名 |
Name | 作用和id一致,对象在BeanFactory中的唯一标识 |
Scope | Bean的作用域 |
Constructor arguments | bean元素的子元素,使用构造方法注入,指定构造方法的参数,该元素的index属性指定参数的序号,ref属性指定对BeanFactory中其他Bean的引用关系,type属性指定参数类型,value属性指定参数的常量值 |
Properties | bean元素的子元素,用于设置一个属性,该属性的name属性指定Bean实例中相应属性的名称,value属性指定Bean属性的值,ref属性指定属性对BeanFactory中其他Bean的引用关系 |
Autowiring mode | 自动装备的模式 |
延迟初始化模式 | 懒加载Bean的模式 |
Initialization method | 初始化后的回调方法,及实例化对象后会调用指定的方法 |
Destruction method | 容器被销毁时的回调方法 |
Bean命名
在BeanFactory中每个Bean具有一个或者多个标识符,这些标识符在容器中必须唯一,如果需要多个则可以将多余的名称配置为别名。
在XML配置中,可以使用id属性或者name属性指定bean的标识符,id属性的值只要符合标识符命名规则即可,通常情况下会使用类名的驼峰命名方式来指定。如果要为bean引入其他别名,可以在name属性中指定,并使用逗号“,”或者分号“;”或者空格分隔。
同时,如果在配置中既不指定id属性,也不指定name,那么在容器中查找Bean时就不能使用通过名称获取了。但是通常并不建议不指定id或者name。
下面实例,如果不指定Bean的id或者name属性在BeanFactory中查找Bean。
1 | public class Test { |
从代码中可以看出,在调用getBean方法时,传入的参数并不是Bean的名称,而是Student.class,也就是对象的类型,换句话说Spring容器时通过传入的类型获取的实例。
在Bean定义中,可以使用id属性指定最多一个名称,也可以使用name指定多个数量的其他名称。同时,也可以使用
1 |
|
在上述配置中使用<alias>元素为Student的student实例起了别名student2。
1 | public class Test { |
运行上述代码可以看出根据别名student2,也可以获取到实例。
实例化Bean
实例化Bean本质上就是创建一个对象或者多个对象的方法。容器会根据配置文件中Bean的配置来创建或者获取实际对象。
如果使用XML的配置方式,可以在
下面就这3中创建对象的方式进行学习。
使用构造函数实例化
当通过构造方法创建Bean时,所有的类都可以被Spring兼容,换句话说,类不需要实现额外的接口,也不需要使用特定的方法编码。只需要指定Bean即可。
使用构造方法创建对象时,可以使用无参的构造方法,也可以使用无参的构造方法,需要注意的是,当使用有参的构造方法创建对象时,需要一些额外的配置。
如果使用无参的构造方法创建对象时,只需要在配置文件中进行如下配置:
1 | <bean class="cn.bytecollege.bean.Student" id="student"></bean> |
如果使用有参的构造方法,在配置bean时,需要指定对应位置参数的值。例如,为上例中的Student提供一个有参构造方法,代码如下:
1 | public class Student { |
在配置文件中进行如下配置:
1 | <bean class="cn.bytecollege.bean.Student" id="student"> |
新建测试类:
1 | public class Test { |
运行上述测试代码,可以看出成功获取了Student的实例,并且将配置的值注入了对象中。
使用静态工厂方法实例化
使用静态工厂方法实例化对象时,需要先建立创建对象的工厂,以Student为例,首先创建工厂类,代码如下:
1 | /** |
在配置文件中进行如下配置:
1 | <bean id="StudentFactory" class="cn.bytecollege.factory.StaticStudentFactory" factory-method="getInstance"> |
下面对上述代码的属性进行讲解:
- id:工厂方法创建出的对象在容器中的唯一标识
- class:工厂类对应的全限定名
- factory-method:创建对象的静态工厂方法
运行如下测试类:
1 | public class Test { |
结果如下图:
使用实例工厂方法实例化
与通过静态工厂方法实例化类似,使用实例工厂方法创建对象时,需要先创建一个实例工厂,然后再配置实例化bean对象,还要将class属性空置,在factory-bean属性中,在当前容器中指定包含要创建该对象的实例方法的bean名称。实例如下:
首先创建实例工厂:
1 | public class StudentFactory { |
在配置文件中进行如下配置:
1 | <!--1.首先实例化工厂类--> |
首先需要实例化工厂类,实例化工厂类后配置工厂类实例化Student,其中注释2处的配置不需要指定class属性,factory-bean指定工厂实例,factory-method属性则用于指定实例化对象的方法。
Bean的作用域
在Spring中不仅可以完成Bean的实例化,还可以为Bean指定作用域。Spring支持6个范围。如下表所示:
Scope | Description |
---|---|
singleton | (默认)将每个 Spring IoC 容器的单个 bean 定义范围限定为单个对象实例。 |
prototype | 将单个 bean 定义的作用域限定为任意数量的对象实例。 |
request | 将单个 bean 定义的范围限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有一个在单个 bean 定义后面创建的 bean 实例。仅在web环境中有效。 |
session | 将单个 bean 定义的范围限定为 HTTP Session的生命周期。仅在web环境中有效。 |
application | 将单个 bean 定义的范围限定为ServletContext的生命周期。仅在web环境中有效。 |
websocket | 将单个 bean 定义的范围限定为WebSocket的生命周期。仅在web环境中有效。 |
单例范围
单例模式是指在Spring容器中有且仅有一个实例,无论在Spring容器中获取多少次,容器都会返回相同的对象。换句话说,当指定一个bean的作用域为单例时,该单例存储在缓存中,并对该命名的bean所有请求都会返回缓存中的对象。如下图所示:
下面通过示例,来学习单例范围:
1 | <bean class="cn.bytecollege.bean.Student" id="student" scope="singleton"></bean> |
在上述配置中实例化Student类型对象,并且通过scope属性指定了该对象的作用范围是单例。
1 | public class Test { |
在测试类中两次获取了Student对象,并且判断两个对象是否是同一对象,从结果可以看出,打印了true,这说明两次从Spring容器中获取的对象为同一对象,运行结果如下:
原型范围
原型范围是指,每次对从容器中获取bean时,Spring容器都会创建一个实例。下面的图示说明了Spring原型范围
修改上例中配置文件,将scope修改为prototype,再次运行测试类:
1 | <bean class="cn.bytecollege.bean.Student" id="student" scope="prototype"></bean> |
从结果中可以看出,两次向容器请求获得的对象并不是同一对象。
DI 依赖注入
在前面有关MVC的章节内容中,可以看出Controller通常需要调用Service对象的方法,而Service又需要调用Dao对象中的方法,这就需要Controller中需要封装一个Service对象,Service中需要封装Dao对象。通常的做法是在Controller中new一个Service对象,同理在Service中new一个Dao对象,用于调用相关的方法。
在Spring当中,创建对象的权力移交给了容器处理,如何在类内对封装的对象赋值就是急需解决的问题,也就是说如何从容器中获取创建好的对象,并赋值给被封装的对象。这就是依赖注入所解决的问题。
依赖注入(DI)是一个过程,通过该过程,对象只能通过构造方法参数,工厂方法的参数或在创建对象实例后在对象实例上设置的属性来定义其依赖关系(即,与它们一起工作的其他对象)。从工厂方法返回。然后,容器在创建 bean 时注入那些依赖项。
使用 DI 原理,代码更简洁,当为对象提供依赖项时,解耦会更有效。下面就来深入学习依赖注入的几种方式。
构造方法注入
通过参数顺序注入
基于构造方法的依赖注入是Spring通过调用具有多个参数(每个参数代表一个依赖项)的构造方法来完成。
下面,先通过一个简单的示例学习构造方法注入,首先提供一个JavaBean,
1 | package cn.bytecollege.bean; |
接着在配置文件中进行如下配置:
1 | <bean class="cn.bytecollege.bean.Student" id="student"> |
从配置中可以看出,使用了bean的子标签constructor-arg来指定有参构造参数中参数列表的信息:
- index:配置构造方法参数列表的索引,默认从0开始。
- value:则用于指定该参数的值
新建测试类,在测试类中打印该对象:
1 | public class Test { |
在示例中,使用配置文件实例化了Student,并且通过Student的构造方法,为实例变量name和年龄注入了值。
通过参数类型注入
除了上述配置文件中的配置方式以外,Spring还支持类型匹配,修改上述配置文件,代码如下:
1 | <bean class="cn.bytecollege.bean.Student" id="student"> |
可以看出在constructor-arg标签中使用的是type类型配置了对应位置参数的数据类型,而不是使用index配置参数的顺序。
再次运行测试类,可以看出成功创建了对象,并将值注入了Student对象的实例变量中。
通过参数名注入
上面的示例分别使用了参数的顺序和参数的类型注入实例变量的值,Spring还支持通过构造参数的名称对实例变量进行注入,修改上述配置文件如下:
1 | <bean class="cn.bytecollege.bean.Student" id="student"> |
在上面的示例中,通过构造方法注入的都是简单类型的值,那么如果注入一个复杂类型的呢,例如,StudentController中注入StudentService,下面通过示例来学习。
1 | package cn.bytecollege.controller; |
1 | package cn.bytecollege.service; |
在StudentController中封装了StudentService对象,并且利用构造方法对其进行赋值。然后调用service的方法,这里就可以通过使用构造方法注入来实现为studentService赋值。
从代码中可以看出StudentController对象中需要一个StudentService对象,因此在配置文件中需要先实例化StudentService对象,然后在实例化StudentController对象时,通过构造方法将studentService对象进行注入。
配置如下:
1 | <!--1--> |
在配置文件中分别使用了根据构造方法参数类型注入和根据构造方法参数名称注入,以及根据构造方法参数索引注入,这3种方式任选一种即可。仔细观察即可发现,这3种方式中为参数赋值时,都使用了ref属性,此处的ref属性是指引用一个已经实例化的对象,而这个对象就是代码注释1处配置的StudentService对象。
新建测试类,代码如下:
1 | public class Test { |
运行测试类可以看出成功实例化了StudentController和StudentService并且通过StudentController的构造方法将StudentService对象注入了StudentController中。
set方法注入
注入复杂类型
除了通过构造方法进行依赖注入以外,Spring还提供了通过set方法注入的方式,当然,这种方式是在调用构造方法或者工厂方法实例化对象以后,在实例化后的对象上调用set方法完成。下面通过简单示例来学习通过set方法注入。
以上例中StudentController和StudentService为例,此处不再提供构造方法,而是提供一个set方法,通过这么方式来进行注入:
1 | package cn.bytecollege.controller; |
1 | package cn.bytecollege.service; |
接下来在配置文件中进行配置:
1 | <bean id="studentService" class="cn.bytecollege.service.StudentService"></bean> |
在上述配置中可以看出在bean元素中使用了property属性进行了配置,其中name是指在StudentController中封装的对象的名称,ref则是引用了已经配置好的StudentService实例。
注入字符串
向类中注入字符串也是常见的需求之一,例如在JDBC中,通常需要配置数据库连接信息:数据库用户名、数据库密码等,在下面的实例中将模拟如何在对象中使用set方法注入字符串。代码如下:
1 | package cn.bytecollege.bean; |
在配置文件中进行如下配置:
1 | <bean id="DBHelper" class="cn.bytecollege.bean.DBHelper"> |
在上述代码中在DBHelper类中封装了url、driver等属性,并提供了set方法,在配置文件中通过<bean>的子标签<property>为属性注入值。
- name:指变量在对象中的名称
- value:为该变量注入的值
除此以外,Spring为开发者提供了更简介的配置方式,也就是p-namespace,使用方式如下:
1 | <bean id="DBHelper" class="cn.bytecollege.bean.DBHelper" |
注入数组
下面将示例,如何为对象中封装的数组注入值:
1 | package cn.bytecollege.bean; |
可以看出在Score类中封装了一个数组,使用下面的配置即可将值注入到数组中。
1 | <bean id="score" class="cn.bytecollege.bean.Score"> |
在上述代码中<array>元素表明该属性是一个数组,<array>中的<value>则是指数组中的值。
注入集合
在对象中封装集合,也是开发中经常遇到的场景,那么如何为集合注入值呢?
首先,定义一个JavaBean,在该类中分别封装List、Set、Map。代码如下:
1 | public class CollectionBean { |
在配置文件中进行如下配置:
1 | <bean id="collectionBean" class="cn.bytecollege.bean.CollectionBean"> |
从配置文件中可以看出,注入集合和注入数组类型,首先使用property指定要配置的属性,如果属性是List集合,则在<list>标签中放置<value>元素,并在<value>中配置值即可,Set集合和List集合类似,只需要在配置值时将<list>元素替换成<set>元素即可。当为Map注入值时,首先需要配置<map>元素,map中配置<entry>元素,使用entry的key和value表示键和值,一个entry就代表一个键值对。
懒加载
通常在Spring配置文件中配置了JavaBean以后,当Spring容器启动时,会立即对这个JavaBean进行实例化,但是这个实例必须是单例。下面将演示这种情况:
首先,创建Student类,代码如下:
1 | public class Student { |
在上述类中提供了一个无参构造器,当Spring容器对Student类实例化时会调用此方法。
接着在Spring配置文件中进行如下配置:
1 | <bean class="cn.bytecollege.bean.Student" id="student" scope="singleton"></bean> |
需要注意的是,这个实例必须是单例,也就是说要在scope属性中指定singleton。
新建测试类:
1 | public class Test { |
运行测试类时,启动Spring容器,从运行结果可以看出Spring实例化了Student。
当不需要在容器启动时就对JavaBean实例化时,可以配置
1 | <bean class="cn.bytecollege.bean.Student" id="student" |
配置懒加载以后,Spring会在请求该实例时才对其进行实例化。再次运行上例中测试类,会发现并没有实例化Student,修改测试类代码如下:
1 | public class Test { |
基于注解的容器配置
Spring初期在配置JavaBean时,只能在XML配置文件中进行配置,这也是被开发者诟病的问题之一,如果在项目中包含的JavaBean较少时,Spring的配置文件相对简单,但是当项目中包含了大量的JavaBean时,使用配置文件来配置JavaBean就显得很复杂,并且配置文件也很臃肿,使得开发者不但要关注业务,还需要关注和维护Spring的配置文件,这也违背了Spring框架诞生的初衷,因此Spring在2.0版本后引入了注解的支持,这样就可以抛开配置文件,直接使用注解来让Spring管理和注入JavaBean,在本小节内容中,将详细学习相关注解:
使用注解定义Bean
前面说过,不管是XML 还是注解,它们都是表达 Bean 定义的载体,其实质都是为Spring容器提供Bean 定义的信息,在表现形式上都是将XML 定义的内容通过类注解进行描述。Spring从2.0开始就引入了基于注解的配置方式,在2.5时得到了完善,在 4.0 时进一步增强。
Spring容器成功启动的三大组成部分分别是Bean定义的信息、Bean实现类及Spring本身,如果采用基于XML的配置,则Bean定义信息和Bean实现类本身是分离的;而如果采用基于注解的配置文件,则Bean定义信息通过在Bean实现类上标注注解实现:
下面的示例将使用注解来定义一个StudentService的Bean:
1 | //1. |
在代码的注释1处可以看到在类上标注了注解@Component,它可以被Spring容器识别,Spring容器会自动将该JavaBean转换为容器管理的Bean.
查看@Component的源代码:
1 |
|
可以看出该注解的代码比较简单,其中value属性指为该实例指定在Spring容器中的id,如果不配置该属性,则Spring会为该实例自动生成一个名称,名称即类名首字母小写,例如上例中的StudentService使用该注解且不指定value时,名称为studentService,也就是熟悉的驼峰命名法。
此外,注释1处的注解和以下XML配置是等效的。
1 | <bean id="studentService" class="cn.bytecollege.service.StudentService"></bean> |
除@Component 外,Spring 还提供了3个功能基本和@Component 等效的注解,分别用于对 DAO、Service 及 Web层的 Controller进行注解。
- @Repository∶用于对 DAO实现类进行标注。
- @Service∶用于对 Service 实现类进行标注。
- @Controller∶用于对Controller 实现类进行标注。
在MVC开发模式中会根据类的工程进行分层,上述对应的注解即可标注在对应的类上,但是一些JavaBean并不能划分在这三层中,就可以使用@Component注解标注,在Spring中这些注解是等效的。
扫描注解定义的Bean
Spring提供了一个context命名空间,它提供了通过扫描类包以应用注解定义Bean的方式,代码如下:
1 |
|
在注释1处声明 context 命名空间,在注释2处即可通过 context 命名空间的 component-scan 的 base-package 属性指定一个需要扫描的基类包,Spring 容器将会扫描这个基类包里的所有类,并从类的注解信息中获取 Bean 的定义信息。
自动装配Bean
通过上一小节的内容可以知道通过注解可以替代XML的配置用于实例化对象,如果对象中依赖了其他Bean,那么如何为依赖的Bean赋值呢,也就是说如何注入呢?在本小节将进行学习
使用@Autowired自动注入
Spring通过@Autowired注解实现Bean的依赖注入。首先查看该注解的源代码:
1 |
|
从源码中可以看出该注解可以标注在构造器、方法、方法参数、成员变量以及注解上,通常会将该注解标注在需要注入的属性或者set方法或者构造器上,Spring推荐标注在set方法或者构造器上。
现在定义StudentController,在StudentController中封装StudentService对象,并利用注解对其进行注入,代码如下:
1 |
|
StudentService代码如下:
1 |
|
从代码中可以看出在StudentService类上标注了注解@Service(此处标注@Component也可以),在StudentController类标注了@Controller注解,Spring容器在启动后会对这两个类进行实例化。
同时,在StudentController类中注释1处,使用了@Autowired注解自动注入了StudentServcie实例。
需要注意的是@Autowired注解默认按照类型(byType)匹配的方式在容器中查找对应类型的Bean,当有且仅有一个匹配的Bean时,Spring将其注入@Autowired标注的变量中。
在Spring配置文件中配置扫描包以后,新建并允许如下测试类:
1 | public class Test { |
从结果中可以看出Spring成功实例化了StudentController和StudentService,并将StudentService的实例注入了StudentController中。
如果容器中没有一个和标注变量类型匹配的Bean,那么Spring容器启动时将抛出NoSuchBeanDefinitionException异常。在上例中删除StudentService类上的@Service注解,再次运行上述测试类,结果如下图
如果希望Spring即使找不到匹配的Bean完成注入也不要抛出异常,那么可以使用@Autowired(required=false)进行标注,如下代码所示:
1 |
|
在默认情况下,@Autowired的required属性值为true,即要求必须找到匹配的Bean,否则将抛出异常。
使用@Qualifier指定注入Bean的名称
如果容器中有一个以上匹配的Bean时,则可以通过@Qualifier注解限定Bean的名称。代码如下:
在Spring.xml中配置两个StudentService的实例:
1 | <bean id="service1" class="cn.bytecollege.service.StudentService"/> |
这时容器中有两个类型为StudentService的实例,名称分别是service1和service2,因为@Autowired默认是根据类型注入,此时有两个相同类型的实例,Spring就会产生混乱,不知道开发者究竟需要哪个实例,便会抛出异常,如下图所示:
此时,就需要@Qualifier注解来告诉Spring在自动注入时注入哪个实例。代码如下:
1 |
|
使用@Resource注入
此外,Spring还支持JSR-250中定义的@Resource和JSR-330中定义的@Inject注解,这两个标准注解和@Autowired注解的功能相似,都是对类变量及方法输入参数提供自动注入功能。@Resource注解要求提供一个Bean名称的属性,如果属性为空,则自动采用标注处的变量名或者方法名作为Bean的名称。在上例中,可以将StudentController中的@Autowired注解更换成@Resource注解。
1 |
|
这时,如果@Resource未指定“studentService”,则可以根据属性方法得到需要注入的Bean的名称。由此可见@Autowired默认按类型匹配Bean,@Resource按照名称匹配注入Bean.@Inject和@Autowired同样也是类型匹配注入Bean,只不过@Inject没有required属性,可见不管是@Resource还是@Inject注解,功能都没有@Autowired丰富,同时@Autowired也是最常用的注解之一。
基于Java类的配置
使用Java类提供Bean定义信息
JavaConfig是Spring的一个子项目,它旨在通过Java类的方式提供Bean的定义信息,该项目在Spring2.0时就发布了1.0版本。Spring4.0基于Java类配置的核心就来源于JavaConfig。并且JavaConfig也成为了Spring4.0的核心功能。
普通的JavaBen只要标注@Configuration注解,就可以为Spring容器提供Bean定义的信息,每个标注了@Bean的类方法都相当于提供了一个Bean的定义信息。下面的代码将演示这种使用方式:
1 | package cn.bytecollege.conf; |
在注释1处,在BeanConf类的定义处标注了@Configuration注解,说明这个类可用于为Spring提供Bean定义的信息,它和Spring的配置文件是等效的。该类的方法可标注@Bean注解,Bean的类型由方法返回值的类型决定,名称默认和方法名相同,也可以通过@Bean的name属性指定Bean的名称,例如@Bean(name=”studentService”),@Bean所标注的方法提供了Bean的实例化逻辑。
在注释2处,studentService()方法定义了StudenService的Bean,它的名称是studentService。
在注释3处又定义了一个StudentController,并注入了注释2处定义的StudentService的Bean。
以上配置和如下XML配置是等效的:
1 | <bean id="studentService" class="cn.bytecollege.service.StudentService"></bean> |
基于 Java类的配置方式和基于XML 或基于注解的配置方式相比,前者通过代码编程的方式可以更加灵活地实现 Bean 的实例化及 Bean 之间的装配;后两者都是通过配置声明的方式,在灵活性上要稍逊一些,但在配置上要更简单一些。
如果Bean在多个@Configuration配置类中定义,如何引用不同配置类中定义的Bean 呢?例如,StudentController在ControllerConf中定义,StudentService在ServiceConf中定义,而StudentController需要引用ControllerConf中定义的Bean。下面的代码将演示这种情况。
在ServiceConf中配置实例化StudentService:
1 | package cn.bytecollege.conf; |
在ControllerConf中配置实例化StudentController:
1 | package cn.bytecollege.conf; |
由于@Configuration 注解类本身已经标注了@Component 注解,所以任何标注了@Configuration的类,本身也相当于标注了@Component,即它们可以像普通的 Bean一样被注入其他 Bean 中。ServiceConf 标注了@Configuration 注解后就成为一个Bean,它可以被自动注入ControllerConf中。
调用ServiceConf的studentService()方法,就像用将ServiceConf配置类中定义的Bean注入进来。Spring会对配置类所标注的@Bean的方法进行“增强”,将对Bean生命周期管理的逻辑植入进来。所以在注释2处调用serviceConf.studentService()方法时,不是简单的执行DaoConfig类中定义的方法逻辑,而是从Spring容器中返回相应Bean的单例。同时也可以在@Bean处标注@Scope注解以控制Bean的作用范围。@Scope的值即可配置为前面内容中提到的作用域。
使用Java类的配置信息启动Spring容器
Spring提供了一个AnnotationConfigApplicationContext类,它能够直接通过标注@Configuration的Java类启动Spring容器。如下代码所示:
1 | package cn.bytecollege.test; |
在代码注释1处,通过AnnotationConfigApplicationContext类的构造器注解传入标注了@Configuration的Java类,直接用该类中提供的Bean定义信息启动Spring容器。
因为AnnotationConfigApplicationContext构造器的参数是可变参数,可以传入多个配置类的class对象,除此之外可以调用容器的register()方法,传入需要使用的配置类。也可以使用@Import注解引入配置类,代码如下:
1 | package cn.bytecollege.conf; |