1 IOC 理解
Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。
IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
- 谁控制谁,控制什么:传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对 象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
- 为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
用一句话来讲解什么是IOC:IOC中文名称是控制反转。主要目的是为了实现程序的解耦,由程序员主动实例化对象的过程转交给Spring容器。IOC将本来是由程序员创建对象以及对象的管理和销毁的控制权交给了Spring IOC容器中,Spring IOC将这一块操作从应用程序中解耦出来,降低程序的耦合程度,简化程序设计。
2. DI 理解
Dependency Injection,即“依赖注入”。DI是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。当一个类(A)需要依赖另一个类对象(B)时,我们把另一个对象赋值给一个对象的过程。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:
- 谁依赖于谁:当然是应用程序依赖于IoC容器;
- 为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;
- 谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
- 注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
用一句话来讲解什么是依赖注入:当我们一个类需要依赖于另一个对象,我们把另一个对象实例化后注入到这个对象的过程,我们就称为DI。由于Spring IOC掌控对象的创建、管理以及销毁工作,如果一个类需要依赖另外一个对象,Spring IOC容器需要将另外一个对象注入到应用程序某个对象中来,应用程序需要依赖某个外部对象。所有依赖注入提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。
3. Spring Bean装配
在Spring中,对象无需自己查找或者创建与其所关联的其他对象。相反容器负责把需要相互协作的对象引用赋予各个对象。创建应用对象之间协作关系的行为通常被称为装配(wiring),这也是依赖注入(DI)的本质。
3.1 Spring Bean装配的可选方案
当描述bean如何装配时,Spring具有非常大的灵活性,它提供了三种主要的装配机制:
- 在XML中进行显示配置
- 在Java中进行显示配置
- 隐式的Bean发现机制和自动装配
我们需要尽可能使用在Java中进行显示配置和Bean自动装配机制,极少推荐使用XML中进行显示配置,因为XML中配置需要大量的XML配置工作。
3.2 隐式的Bean发现机制和自动装配
Spring从两个角度来实现自动化装配:
- 组件扫描(component scanning):Spring会发现应用上下文中所创建的Bean
- 组件装配(autowiring):Spring自动满足Bean之间的依赖
实现自动化装配有两种方式:(组件扫描默认是不启动的)
1)通过XML启用组件扫描
首先在mvc-config.xml中启动组件扫描功能,并用base-package属性指定扫描范围
<context:component-scan base-package="com.hust.edu"/>
再通过在需要注解的class上添加@Controller、@Service、@Repository、@Component等注解,比如:
@Controller
public class UserController {// ......}
2)通过@ComponentScan注解启用组件扫描
首先在class上使用@ComponentScan启用组件扫描,例如:
@ComponentScan
public class AppConfig {// ......}
此外:@ComponentScan(basePackages=“conf”)等同于@ComponentScan(“conf”),然后通过在需要注解的class上添加@Controller、@Service、@Repository、@Component等注解,例如:
对于@ComponentScan可以通过basePackages或者basePackageClasses指定扫描范围,等同于XML注解中的base-package属性;如果不指定扫描范围,则默认扫描当前类所在包以及子包的所有类。当然Craig Walls建议使用basePackageClasses,因为如果代码重构的话这种方式会立马发现错误,下面是basePackageClasses的使用方式(指定基础扫描类):
@ComponentScan(basePackageClasses={
UserController.class
// ......
})
public class AppConfig {}
使用@Autowired可以为bean实现自动装配,@Autowired可以使用在构造函数、Setter方法、普通方法和成员变量上。比如下面的用法:
// 用法一:
@Autowired
MessageSend messageSend;
// 用法二:(构造函数也一样,主要是函数参数的依赖)
@Autowired
public void setMessageSend(MessageSend messageSend) {
this.messageSend = messageSend;
}
设置@Autowired(required=false)时,Spring尝试执行自动装配,但是如果没有匹配的bean则忽略,但是这种情况故意出现空指针异常NullPointerException。@Autowired注解可以使用@Inject替换,@component可以使用@Named注解替换,后者是源于Java依赖注入规范。
3.3. 通过Java代码装配
大部分的场景自动化装配Bean是满足要求的,但是在一些特殊场景下自动化装配Bean是无法满足要求的,比如说要将第三方的组件装配到自己的应用中,因为没有方法将@component或者@Autowired注解放置在它们的类上。但是你仍然可以采用显示装配方式:Java代码装配和XML配置。
首先需要创建配置类,创建配置类的关键是使用@Configuration注解来表明这个类是一个配置类,该类包涵Spring上下文中如何创建Bean的细节。
声明一个简单Bean:在Java的配置类中编写一个带有@Bean注解的方法,比如下面:
@Bean
public UserServiceImpl userService(){
return new UserServiceImpl();
}
默认情况下Bean的ID是方法名,也可以指定Bean的ID:@Bean(name=“userService”),如果有依赖可以使用下面这些的方式来实现:
@Bean
public UserServiceImpl userService(){
return new UserServiceImpl(userDao());
}
@Bean
public UserDaoImpl userDao(){
return new UserDaoImpl();
}
上面看起来是调用UserDaoImpl(),其实在配置类中Spring会拦截对这个方法的引用,并返回该方法所创建的bean,而不是每次都对其进行实际调用。当然下面这种方式也是可以的,userService()方法需要userDao作为参数,Spring创建Bean的时候会自动装配一个UserDaoImpl到方法中(我猜测应该和@Autowired意思差不多,当Spring Context下只有一个UserDaoImpl就可以通过匹配原则进行装配),这种方式是被推荐的,如果UserDaoImpl不是在本配置类下配置,任然可以正常使用(比如XML默认的组件扫描等)。
@Bean
public UserServiceImpl userService(UserDaoImpl userDao){
return new UserServiceImpl(userDao);
}
上面我们通过构造函数的方式实现依赖注入(DI),当然我们也可以用一种更好的方式来实现依赖注入,就是用Setter方法注入,如下所示:
@Bean
public UserServiceImpl userService(UserDaoImpl userDao){
UserServiceImpl userService = new UserServiceImpl();
userService.setDao(userDao);
return userService;
}
3.4 通过XML装配Bean
XML装配Bean的方式,虽然已经不推荐了,但是还是我们最早使用的一种Bean装配的方式,还是需要学习的(很多古老的项目还在用,我目前的公司也是)。下面是一个简单Bean的声明:
<!--index表示获取到对象的标识 clsss创建哪个对象-->
<bean id="peo" class="com.hust.edu.pojo.People">
<!--ref表示引用另一个bean vaule表示基本数据类型或者String-->
<constructor-arg index="1" type="int" value="1"></constructor-arg>
<constructor-arg index="0" type="java.lang.String" value="AAA"></constructor-arg>
</bean>
通过构造器创建Bean
- 通过无参数构造创建:默认情况
- 有参数构造创建:需要明确配置
在applicationContext.xml中设置调用哪个构造方法创建对象
- 如果设定的条件匹配多个构造方法执行最后的构造方法
- index:参数的索引,从0开始
- name:参数名字
- type:类型(区分关键字和非封装类 int 和 Integer)
public class People {
private int id;
private String name;
public People(int id, String name) {
this.id = id;
this.name = name;
System.out.println("有参数构造方法1 name:"+name+" id:"+id);
}
public People(String name, int id) {
this.id = id;
this.name = name;
System.out.println("有参数构造方法2 name:"+name+" id:"+id);
}
public People() {
System.out.println("执行默认的构造方法");
}
public int getId() {
return id;
}
public void setId(int id) {
System.out.println("setter id: "+ id);
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("setter name: "+ name);
this.name = name;
}
@Override
public String toString() {
return "People{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
<!--index表示获取到对象的标识 clsss创建哪个对象-->
<bean id="peo" class="com.hust.edu.pojo.People">
<!--ref表示引用另一个bean vaule表示基本数据类型或者String-->
<constructor-arg index="1" type="int" value="1"></constructor-arg>
<constructor-arg index="0" type="java.lang.String" value="AAA"></constructor-arg>
</bean>
通过实例工厂方法创建Bean
工厂设计模式:帮助创建对象,一个工厂可以参数多个对象。
实例工厂:需要创建工厂,才能创建对象。
实现步骤:
- 必须要有一个实例工厂
- 在applicationContext.xml 中配置工厂对象和需要创建的对象
public class PeopleFactory {
public People createPeople(String type){
switch (type){
case "A":
return new PeopleA();
case "B":
return new PeopleB();
default:
return null;
}
}
}
public class PeopleA extends People{
@Override
public String toString() {
System.out.println("A");
return super.toString();
}
}
public class PeopleB extends People{
@Override
public String toString() {
System.out.println("B");
return super.toString();
}
}
<bean id="factory" class="com.hust.edu.constructor.factory.PeopleFactory"></bean>
<bean id="peo1" factory-bean="factory" factory-method="createPeople" >
<constructor-arg name="type" value="A"/>
</bean>
<bean id="peo2" factory-bean="factory" factory-method="createPeople" >
<constructor-arg name="type" value="B"/>
</bean>
通过静态工厂创建Bean
静态工厂:不需要创建工厂, 可以快速创建对象
实现步骤:
- 必须需要有一个静态工厂,在方法上添加static
- 在applicationContext.xml 中创建对象
public class PeopleStaticFactory {
public static People getPeopleAInstance(){
return new PeopleA();
}
public static People getPeopleBInstance(){
return new PeopleB();
}
}
<bean id="peo3" class="com.hust.edu.constructor.factory.PeopleStaticFactory" factory-method="getPeopleAInstance"></bean>
<bean id="peo4" class="com.hust.edu.constructor.factory.PeopleStaticFactory" factory-method="getPeopleBInstance"></bean>
设置属性
- 如果属性是基本数据类型或者String类型,注入就比较简单
- 如果属性是Set、List、Map、Array, 注入不同的数据类型
<bean id="peo" class="com.hust.edu.pojo.People">
<property name="id" value="1"></property>
<property name="name" value="zhangsan"></property>
</bean>
<bean id="peoSet" class="com.hust.edu.pojo.PeopleSet">
<property name="id" value="1"></property>
<property name="name" value="zhangsan"></property>
<property name="set" >
<set>
<value>1</value>
<value>2</value>
<value>3</value>
<value>4</value>
</set>
</property>
</bean>
<bean id="peoList" class="com.hust.edu.pojo.PeopleList">
<property name="id" value="1"></property>
<property name="name" value="zhangsan"></property>
<property name="list" >
<set>
<value>1</value>
<value>2</value>
<value>3</value>
<value>4</value>
</set>
</property>
</bean>
<bean id="peoArray" class="com.hust.edu.pojo.PeopleArray">
<property name="id" value="1"></property>
<property name="name" value="zhangsan"></property>
<property name="strings" >
<array>
<value>1</value>
<value>2</value>
<value>3</value>
<value>4</value>
</array>
</property>
</bean>
<bean id="peoMap" class="com.hust.edu.pojo.PeopleMap">
<property name="id" value="1"></property>
<property name="name" value="zhangsan"></property>
<property name="map" >
<map>
<entry key="a" value="123"></entry>
<entry key="b" value="456"></entry>
<entry key="c" value="789"></entry>
</map>
</property>
</bean>
4 处理自动装配的歧义性
虽然在实际编写代码中,很少有情况会遇到Bean装配的歧义性,更多的情况是给定的类只有一个实现,这样自动装配就会很好的实现。但是当发生歧义性的时候,Spring提供了多种的可选解决方案。比如People父类,有三个实现类分别是PeopleA、PeopleB和PeopleC。那么在自动装配的时候具体是装配哪个类呢?所以Spring必须提供表选自动装配的Bean。
public class PeopleAutowired {
@Autowired
private People people;
@Override
public String toString() {
return "PeopleAutowired{" +
"people=" + people +
'}';
}
}
@Component
public class PeopleA extends People {
@Override
public String toString() {
System.out.println("A");
return super.toString();
}
}
@Component
public class PeopleB extends People {
@Override
public String toString() {
System.out.println("B");
return super.toString();
}
}
@Component
public class PeopleC extends People {
@Override
public String toString() {
System.out.println("C");
return super.toString();
}
}
4.1 表示首选的Bean
注意,对于多可选择项,只能有一个可以加上@Primary。通过@Primary
注解来标识首选Bean。
@Component
@Primary
public class PeopleA extends People {
@Override
public String toString() {
System.out.println("A");
return super.toString();
}
}
4.2 限定自动装配的Bean
@Qualifier(“peopleA”)指向的是扫描组件时创建的Bean,并且这个Bean是IceCream类的实例。事实上如果所有的Bean都没有自己指定一个限定符(Qualifier),则会有一个默认的限定符(与Bean ID相同),我们可以在Bean的类上添加@Qualifier注解来自定义限定符,如下所示:
@Component
@Primary
@Qualifier("peopleA")
public class PeopleA extends People {
@Override
public String toString() {
System.out.println("A");
return super.toString();
}
}
@Component
public class PeopleAutowired {
@Autowired
@Qualifier("peopleB")
private People people;
@Override
public String toString() {
return "PeopleAutowired{" +
"people=" + people +
'}';
}
}
5. Bean作用域
默认情况下,Spring应用上下文中所有的Bean都是单例模式。在大多数情况下单例模式都是非常理想的方案。但是如果,你要注入或者装配的Bean是易变的,他们会有一些特有的状态。这种情况下单例模式就会容易被污染。Spring为此定义了很多作用域,可以基于这些作用域创建Bean,包括:
- 单例(Singleton):在整个应用中,只创建Bean的一个实例
- 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的Bean实例。这个相当于new的操作
- 会话(Session):在Web应用中,为每个会话创建一个Bean实例。对于同一个接口的请求,如果使用不同的浏览器,将会得到不同的实例(Session不同)
- 请求(Request):在Web应用中,为每个请求创建一个Bean实例
@Component
@Scope("prototype")
public class Car {
// 。。。。。。
}
// ——————或者——————
@Bean
@Scope("prototype")
public LinuxConfig getLinux() {
LinuxConfig config = new LinuxConfig();
return config;
}
5.1 使用会话和请求作用域
我们常用@Scope来定义Bean的作用域。如用户的购物车信息,如果将购物车类声明为单例(Singleton),那么每个用户都向同一个购物车中添加商品,这样势必会造成混乱;你也许会想到使用原型模式声明购物车,但这样同一用户在不同请求时,所获得的购物车信息是不同的,这也是一种混乱。如下所示:
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,proxyMode = ScopedProxyMode.INTERFACES)
public class ShoppingCart {
}
在这里我们要注意一下,属性proxyMode。这个属性解决了将会话或者请求作用域的Bean注入到单例Bean中所遇到的问题。假设我们要将Cart bean注入到单例StoreService bean的Setter方法中:
StoreService是一个单例bean,会在Spring应用上下文加载的时候创建。 当它创建的时候, Spring会试图将Cart bean注入到setCart()方法中。 但是Cart bean是会话作用域的, 此时并不存在。 直到某个用户进入系统,创建了会话之后,才会出现Cart实例。系统中将会有多个Cart实例: 每个用户一个。 我们并不想让Spring注入某个固定的Cart实例到StoreService中。 我们希望的是当StoreService处理购物车功能时, 它所使用的Cart实例恰好是当前会话所对应的那一个。Spring并不会将实际的Cart bean注入到StoreService中,Spring会注入一个到Cart bean的代理。这个代理会暴露与Cart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用Cart的方法时, 代理会对其进行懒解析并将调用委托给会话作用域内真正的Cart bean。
proxyMode属性被设置成了ScopedProxyMode.INTERFACES, 这表明这个代理要实现Cart接口,并将调用委托给实现bean。如果Cart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果Cart是一个具体的类的话,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。
6.运行时值注入
Bean的属性注入的时候有时候硬编码是可以的,但是有时候我们希望避免硬编码值,而是想让这些在运行时再确定。为了实现这些功能,Spring提供了两种在运行时的求值方式:
- 属性占位符
${}
- Spring表示式语言(SpEL)
#{}
6.1 属性占位符
<!-- 加载配置文件,支持注解的方法 -->
<bean id="prop" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="locations">
<array>
</array>
</property>
</bean>
<!-- 加载配置文件,支持xml的方式-->
<bean class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
<property name="locations">
<array>
</array>
</property>
</bean>
通过配置PreferencesPlaceholderConfigurer和PropertiesFactoryBean用来在Java代码中可以使用属性占位符来方位外部资源属性。
6.2 Spring表示式语言(SpEL)
Spring表达式语言(Spring Expression Language),它能够以一种强大的和简洁的方式将值装配到Bean属性和构造参数中,这个过程中所使用到的表达式会在运行时计算得到。
SPEL有很多特性:
- 使用Bean ID来引用Bean
- 调用方法和访问对象的属性
- 对值进行算式、关系和逻辑运算
- 正在表达式匹配
- 集合操作
SPEL的使用方法
- 表达字面值
- 引用Bean、属性和方法
- 在表达式中使用类型
- SPEL运算符
- 计算正则表达式
- 计算集合