新闻资讯
Spring基础——控制反转(IOC)(一)
1、简述
1.1 依赖注入DI
现实开发中,每一个应用都会由两个或多个类组成,这些类之间相互协作完成特定的业务逻辑。根据传统做法,每个对象负责管理与自己协作的对象的引用(也就是,每个对象中使用new实例化对象的方式创建协作的对象)——这将导致==高度耦合和难以测试的代码==。
public class ClassA{ private ClassB b;//B类的依赖 public ClassA(){ this.b=new ClassB();//A与B紧耦合 }
} public class ClassB{}
DI 的出现就是为了解决对象之间的依赖关系所带来的高耦合问题。【依赖注入 (DI,Dependency Injection)】:将所依赖的关系自动交给目标对象,而不是让对象本身去获取依赖。依赖注入所关注的是已经创建好的对象如何实现它们之间的依赖关系;至于这些对象怎么被创建和管理,稍后会讲述。
public class ClassA{ private ClassB b; public ClassA(ClassB b){ this.b=b;//B是被注入进来的 }
} public class ClassB{}
DI 的实现所带来的好处是:和面向接口实现松耦合。一个对象通过接口来表明依赖关系,这样就可以在对象不确定的情况下,使用不同的具体实现进行替换——【松耦合】。
1.2 Bean
在 Spring 应用中,一个 Bean 对象对应一个对象,并存储于 Spring 容器中,Spring 容器负责创建对象,装配、配置对象,以及管理整个对象的生命周期,从生存到死亡。
1.2.1 Spring容器
容器是 Spring 框架的核心。Spring 容器使用 DI 管理构成应用的组件,它会创建相互协作的组件之间的关联。Spring 自带多个容器实现,主要分为两种类型:
- bean 工厂:由org.springframework.beans.factory.BeanFactory接口定义,是最简单的容器,提供基本的 DI 支持;
- 应用上下文:由org.springframework.context.ApplicationContext接口定义,基于 BeanFactory 构建,并提供应用框架级别的服务;
A. 使用应用上下文
Spring 自带了多种类型的应用上下文。
类型 | 描述 |
---|---|
AnnotationConfigApplication | 从一个或多个基于 java 的配置类中加载 Spring 应用上下文 |
AnnotationConfigWebApplicationContext | 从一个或多个基于 Java 的配置类中加载 Spring Web 应用上下文 |
ClasssPathXmlApplicationContext | 从类路径下的一个或多个 XML 配置文件中加载上下文定义,把应用上下文的定义文件作为类资源 |
FileSystemXmlApplicationContext | 从文件系统下的一个或多个XML配置文件中加载上下文定义 |
XmlWebApplicationContext | 从 Web 应用下的一个或多个 XML 配置文件中加载上下文定义 |
B. Bean的生命周期
Java 中通过 new 实例化的对象,其生命周期是从被创建开始,直到不再被调用,该对象就由 Java 自动进行垃圾回收。
在 Spring 中,Bean 对象的生命周期相对复杂,其包含了以下过程:
- Spring 对 bean 进行实例化;
- Spring 将值和 bean 的引用注入到 bean 对应的属性中;
-
如果 bean 实现了以下对象,会进行相应的操作:
- 实现BeanNameAware接口,Spring 将 bean 的 ID 传递给setBeanName()方法;
- 实现BeanFactoryAware接口,Spring 将调用setBeanFactory()方法,将 BeanFactory 容器传入;
- 实现BeanPostProcessor接口,Spring 将调用postProcessBeforeInitialization()方法;
- 实现InitializingBean接口,Spring 将调用afterPropertiesSet()方法。如果 bean 使用 init-method 声明初始化方法,该方法也会被调用;
- bean 创建完毕,可被应用使用;此时,它们一直驻留在应用上下文,直到该应用上下文被销毁;
- 如果 bean 实现了DisposableBean接口,Spring 将调用destory()方法。同样,如果 bean 使用destory-method声明了销毁方法,该方法也会被调用;
2、装配Bean
在 Spring 中,对象无需自己查找或创建与其所关联的其他对象。相反,容器负责把需要相互协作的对象引用赋予各个对象。创建应用对象之间协作关系的行为称为【装配 (wiring)】。
装配 bean 的三种机制
- 隐式的 bean 发现机制和自动装配;
- 在 Java 中进行显示配置;
- 在 XML 中进行显示配置
尽管,Spring 中提供了多种方案来配置 bean,我们在配置时可视情况进行选择合适的方式进行装配我们的 bean 对象。建议是:尽可能使用自动配置机制;显示配置越少越好。而且,使用选择显示配置时,JavaConfig 配置会比 XML 配置更加强大,类型更安全。
2.1 自动化装配
Spring 是从两个方面实现自动装配:
- 组件扫描 (component Scan):Spring 会自动发现应用上下文中所创建的 bean;
- 自动装配 (autowiring):Spring 自动满足 bean 之间到依赖;
2.1.1 创建组件和自动装配
创建组件类时,常用的注解有:
-
@Component:创建一个组件类,用于被 Spring 扫描并创建 Bean 对象;
该注解可以为当前类设定 ID 值,@Component("ID_value")。没有设定 ID 值时,默认为类名的首字母为小写。
-
@Autowire:自动装配,为 Bean 的属性注入对象。
Autowire可以用在定义属性的语句上、有参构造方法以及 set()方法上。使用注解,会在 Spring 应用上下文中寻找匹配的 bean 对象。
在使用@Autowire注解时,需要注意两个问题:
- 如果当前 Bean 对象的依赖关系,==没有匹配的其它 Bean==,Spring 应用上下文在创建该 Bean 时,会抛出异常;使用注解的属性 required=false,如果找不到匹配的 Bean,会处于未装配状态:null。
- 如果当前 Bean 对象的依赖关系,==存在多个满足匹配的其它 Bean==,Spring 也将抛出异常;这涉及到 装配的歧义性 。
2.1.2 组件扫描
上节简单讲述了如何创建一个组件类,以及如何实现自动装配依赖关系。但这并不代表:在Spring容器中创建了一个 Bean 对象。要想创建一个 Bean 对象,需要配置 Spring 的组件扫描,命令 Spring 寻找带@Component注解的类,并创建 Bean,因为 Spring 中组件扫描功能默认是不启用。那么,如何启用组件扫描呢?——有两种方式:
-
基于 Java 的配置
需要创建一个配置类,该类与普通类的区别在于:使用注解@Configuration修饰。开启组件扫描,需要使用另一个注解@ComponentScan。
package soundsystem; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class CDPlayerConfig { }
-
XML 文件配置
在 XML 中配置启用组件扫描,需要使用 Spring context 命名空间 的<context:component-scan>元素。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:Context ="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <Context:component-scan base-package="soundsystem"/> </beans>
细心的小伙伴可能发现了,在 XML 文件配置中,base-package属性是必须给定的。该属性是指定组件扫描的基础包,也就是指定哪些包是需要使用组件扫描。
在 Java 配置中,@ComponentScan注解中可使用 basePackages 属性和 basePackageClasses 属性来指定组件扫描的基础包。前者给定值是包路径的 String 类型,后者是.class类文件(类文件所在的包会作为基础包)。它们的值可以是单一值,也可以是复数形式。
@ComponentScan(basePackages={"package1","package2",...})//使用String类型表示,是类型不安全的;当重构代码时,容易发生错误 //@ComponentScan(basePackageClasses={Xxx1.class,Xxx2.class,...}) public class CDPlayerConfig{
}
2.2 显式装配
大多数情况下,通过组件扫描和自动装配实现 Spring 的自动化配置更为推荐。但有些情况,比如:将第三方库中的组件装配到应用中,使用@Component和@Autowired无法进行注解,这就必须采用显式装配。显式装配的方案有:Java 和 XML。
2.2.1 Java 配置
在 2.1.2 组件扫描 中,已经提及如何创建一个 Java 配置类,就不在重复讲述。在配置类中,通过方法形式和注解@Bean创建 Bean 对象。Java 配置的好处是:在创建 Bean 的过程中,可以使用 Java 代码。
在 Java 配置类中声明 Bean,需要编写一个方法,这个方法会返回 创建所需类型的实例,然后给这个方法添加@Bean注解;默认情况下,@Bean注解会设定与方法名一样的 ID 值,可以使用 name 属性指定不同的名字。
package cn.book.main; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean; @Configuration public class StudentConfig { @Bean //@Bean(name="stu") public Student getStu(){ return new Student();
}
}
package cn.book.main; public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age;
} public Student() {
} public String getName() { return name;
} public void setName(String name) { this.name = name;
} public int getAge() { return age;
} public void setAge(int age) { this.age = age;
}
}
这个简单的例子中,通过无参构造方法创建实例,是声明 Bean 最简单的方法,因为没有为 Bean 注入依赖关系。在配置类中,实现依赖注入的方式都是通过有参构造方法创建,只是获取要注入的 Bean 的形式有两种:
-
引用配置类中创建 Bean 的方法;
@Bean public String getStuName(){ return "Tom"; } @Bean public int getStuAge(){ return 18; } @Bean public Student getStu(){ return new Student(getStuName(),getStuAge()); }
注意:
- 只能注入配置类中的 Bean 对象;
- Bean 对象是单例的。方法被调用时,spring 会拦截调用的方法,如果容器中已创建该方法返回的 Bean 对象,则直接赋予,而不会再执行方法内的操作。
-
通过方法参数传递;
Java 或 XML 配置中创建的 Bean 对象、组件扫描发现的 Bean 对象,都可以通过方法参数传递并注入。
@Bean public Student getStu(String name,int age){ return new Student(name,age); }
2.2.2 XML 配置
在使用 XML 装配 Bean 之前,需要创建一个新的配置规范,这意味着要创建一个 XML 文件,并且以<beans>元素为根。下面是最为简单的 Spring XML 配置:
<?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
http://www.springframework.org/schema/context "> </beans>
A、声明 Bean
无参构造器声明 bean
XML 配置中使用<bean>元素来声明一个 bean,该元素类似于 Java 配置中的@Bean注解。
<bean id="" class="" /> <!--这个元素将会调用类的默认构造器来创建 bean-->
- id:bean 的 ID 值;可以不指定,会默认为:包名.类名#0。0为计数值,用来区分相同的 bean ,如果有相同的 bean ,计数值 + 1;
- class:指定创建 bean 的类,使用全限定类名;
有参构造器声明 bean
要使用有参构造器创建 bean ,需要使用<bean>的<constructor-arg>元素,此方式在声明 bean 的同时,并注入其它依赖关系。该元素中有五个属性:
- name:指定参数名称,与构造方法中的参数名一致
- value:赋予参数的值;注入常量值,可以是基本类型和String;
- type:参数的类型
- index:指定参数在构造方法中的顺序号(从0开始)
- ref:要注入的 bean 的 ID 值;
<bean id="" class=""> <constructor-arg name="" value="" type="" index=""/> <constructor-arg name="" type="" index="" ref="" /> </bean>
注意
- bean元素中的参数名称要与构造方法中的参数名一致;
- bean元素中的参数顺序可以与构造方法中的参数顺序不一致;可以使用 index 属性指定在构造方法的顺序;
- 使用 type 属性时,对于引用类型需要使用包名+类名;基本类型可以不用该属性;
B、注入
在 XML 配置文件中,注入 bean 的方式有三种:有参构造器注入<constructor-arg>、属性注入<property>以及自动注入<autowire>。
属性注入
属性注入的实质是:调用set()方法。在声明 bean 的元素中,使用<property>元素。
<bean id="" class=" "> <property name="" value=""/> <property name="" ref=""/> </bean>
自动注入
自动注入方式使用bean元素中的属性autowire,该属性有三个值byName、byType、constructor,根据提供的值进行自动匹配注入。
-
byName:在当前 XML 文件中,查找 bean 元素的 id 值与需要注入 bean 的属性名相同的对象,进行匹配。
-
byType:在当前 XML 文件中,查找 bean 标签的对象类型与需要注入 bean 的属性类型相同的对象,进行匹配;此时,不需要关注 bean 标签的 id 值是否与需要注入的属性名一致。
-
constructor:【1】根据需要注入对象的有参构造器的形参名进行查找 ,找到匹配 bean 的 id 值则注入;否则,【2】根据需要注入对象的有参构造器的形参类型进行查找,找到类型匹配的 bean 标签则注入。
- byName 和 byType实际上是调用set()方法赋值;constructor则是调用有参构造方法;
-
byName 和 byType可以结合property标签使用;可以结合constructor-org标签使用,相当于调用多参的有参构造方法;
C、集合装配
Spring 中实现了对集合的装配,包括:Array、List、Set以及Map,它们对应的元素为:<array>、<list>、<set>以及<map>,集合配置方式比较接近,这里举例 List 和 Map 集合的配置方式
<list value-type=""><!--创建List,并声明存储值的类型--> <value type=""></value><!--集合包含的值,可声明数据类型--> <ref bean=""/><!--引用bean,使用bean的ID--> </list> <map key-type="" value-type=""><!--创建Map,并声明存储键-值的类型--> <entry key="" value=""/><!--集合包含的值--> <entry key-ref="" value-ref=""/><!--引用到键或值的bean,使用bean的ID--> </map>
2.2.3 混合配置
当我们在装配 bean 时,如果同时采用 JavaConfig 和 XML 配置 bean 时,而它们的 bean 相互关联,这时,就需要将不同的配置文件组合在一起。
A、JavaConfig 中引用 XML 配置
多个 Java 配置组合
使用注解@Import可以将其它 JavaConfig 配置类引入,
//在配置类中引用另一个配置类 @Configuration @Import(XxxConfig1.class) public class Xxxconfig2{
} //当然,也可以创建一个新的配置类,只用于组合配置类 @Configuration @Import(XxxConfig1.class,XxxConfig2.class) public class Config{
}
JavaConfig 配置中引用 XML 配置
@Configuration @ImportResource("classpath:*/*/*.xml") public class Config{
}
B、XML 配置中引用 JavaConfig 配置
<bean class="*.*.Config" /><!--引入 JavaConfig 配置--> <import resource="*/*/*.xml" /><!--引入 XML 配置-->
3、高级装配
3.1 环境与profile
应用中存在不同的环境,应用在不同的环境中需要配置不一样的 Bean,如果需要切换环境时,原环境的 Bean 在新环境中不一定可用,这时需要在新环境中配置新的 Bean,在 Spring 中,可以根据环境创建 Bean 或者不创建 Bean,这个就是 Profile 配置 。
跨环境配置的几个例子:数据库配置、加密算法以及外部系统的集成。
在这里,我们不讨论如何配置不同的环境,只关注如何使用Profile决定 Bean 的创建。现假设,我们应用中存在下面三个环境,环境名称为:dev、qa、prod。现在,我们要为指定的环境装配 Bean。
3.1.1 配置Profile
JavaConfig 中配置
@Profile("Envionment_name")注解,括号内指定环境名称,指定某个 Bean 属于哪一个 Profile。当指定的环境为激活状态时,该 Bean 被创建,否则不创建。
@Configuration //@Profile("dev") //profile应用在类上,当环境激活时,该配置类才会被创建 public class ProfileConfig{ @Bean(destroyMethod="shutdown") //使用在方法级别上,可以将不同环境的 Bean 放在同一配置类中 @Profile("dev") public DataSource dataSource(){ return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
}
XML 中配置
在 XML 配置中,可以通过<Beans>元素的profile属性,在 XML 配置 Bean。下面是一个例子
<beans profile="dev"> <!--为dev配置一个 Bean--> <jdbc:embedded-database id="dataSource" type="H2"> <jdbc:script location="classpath:schema.sql" /> <jdbc:script location="classpath:test-data.sql" /> </jdbc:embedded-database> </beans> <beans profile="prod"> <!--为prod配置一个 Bean--> <jee:jndi-lookup id="dataSource" lazy-init="true" jndi-name="jdbc/myDatabase" resource-ref="true" proxy-interface="javax.sql.DataSource" /> </beans>
回复列表