Spring基础(一)_控制反转(IOC)
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>
3.1.2 激活Profile
Spring 在确定哪个 Profile 处于激活状态时,需要依赖两个独立的属性:spring.profiles.active
和 spring.profiles.default
。前者会根据指定值来确定哪个 Profile 是激活的;后者是当没有指定 active
属性的值时,默认激活的 Profile。Spring 中设置这两个属性的方式
- 作为 DispatcherServlet 的初始化参数;
- 作为 Web 应用的上下文参数;
- 作为 JNDI 条目;
- 作为环境变量;
- 作为 JVM 的系统属性;
- 在记成测试类上,使用
@ActiveProfiles
注解设置;
下面的例子中,使用 DispatcherServlet 的参数将 spring.profiles.default
设置 profile。在 Web 应用中,设置 spring.profiles.default
的 web.xml 文件如下
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--为上下文设置默认的 profile-->
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>cn.book.main.servlet.DispatcherServlet</servlet-class>
<!--为Servlet设置默认的 profile-->
<init-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/appServlet</url-pattern>
</servlet-mapping>
</web-app>
注意
spring.profiles.active
和 spring.profiles.default
属性中,profile 使用的是复数形式,可以同时激活多个 Profile——通过列出多个 profile 名称,并以逗号分隔。
3.2 条件化 Bean
如果我们定义的 bean ,但不希望它们被 Spring 容器即刻被创建,而是希望当类路径下包含某个库,或者是创建了其它 Bean,亦或者要求设置了某个特定环境变量后,该 Bean 才被创建。此时,我们就需要使用条件化配置。
要实现一个条件化 Bean,在装配 Bean 的方法上( 使用@Bean
),引用另一个注解 @Conditional(*.class)
,注意:括号内给定的是一个类文件。该注解会根据括号内给定类的返回结果判断是否创建 Bean,如果为true,会创建 Bean,否则不创建。
但是,这只是定义了一个要条件化的 Bean,该 Bean 需要满足怎样的条件,需要自己实现。上面说到,@Conditional
注解需要传入一个类文件,该类在创建时,要实现 Condition
接口,并重写 matches()
方法。下面是一个简单的例子
package cn.book.main.pojo;
//Bean 类
public class TestCondition {
public TestCondition() {
System.out.println("Bean 被创建了");
}
}
该类实现 Condition
接口,并重写 matches()
方法,在方法内可以编写判断代码,并返回 boolean 值。
package cn.book.main.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class IfCreatCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return false;
}
}
配置类,装配 Bean。
package cn.book.resource;
import cn.book.main.condition.IfCreatCondition;
import cn.book.main.pojo.TestCondition;
import org.springframework.context.annotation.*;
@Configuration
public class HumanJobConfig {
@Bean
@Conditional(IfCreatCondition.class)
public TestCondition getCondition(){
return new TestCondition();
}
}
测试类,如果 IfCreatCondition 类返回 true,则 Bean 被创建;否则不会被创建。
package cn.book.test;
import cn.book.resource.HumanJobConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=cn.book.resource.HumanJobConfig.class)
public class HumanJobTest {
@Test
public void Test(){
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(HumanJobConfig.class);
}
}
上面只是演示了实现条件化 Bean 的流程,我们的条件可以再复杂。大家应该注意到了,matches()
中有两个参数:ConditionContext
和 AnnotatedTypeMetadata
。通过这两个对象,我们可以实现符合 IOC 和 DI 的条件。接下来,就来了解这两个对象:
ConditionContext
是一个接口,它有以下方法
方法 | 描述 |
---|---|
getRegistry | 返回 BeanDefinitionRegistry 检查 bean 定义; |
getBeanFactory | 返回 ConfigurableListableBeanFactory 检查 bean 是否存在,甚至 检查 bean 的属性; |
getEnvironment | 返回 Environment 检查环境变量是否存在以及它的值是什么; |
getResourceLoader | 返回 ResourceLoader 所加载的资源; |
getClassLoader | 返回 ClassLoader 加载并检查类是否存在; |
AnnotatedTypeMetadata
也是一个接口,能够检查带有 @Bean
注解的方法上还有什么注解。它有以下方法:
方法 | 描述 |
---|---|
boolean isAnnotated(String annotationType) | 检查带 @Bean 的方法上是否存在其它特定的注解 |
Map<String,Object> getAnnotationAttributes(String annotationType) | 获得指定注解的 Bean |
Map<String,Object> getAnnotationAttributes(String annotationType, boolean classValueAsString) | ==未了解== |
MultiValueMap<String,Object> getAllAnnotationAttributes(String annotationType) | 获得指定注解的所有 Bean |
MultiValueMap<String,Object> getAllAnnotationAttributes(String annotationType, boolean classValueAsString) | ==未了解== |
3.3 处理自动装配的歧义性
自动化装配中,仅当只有一个 Bean 满足时,才能装配成功。当多个 bean 满足装配时,Spring 会产生异常:NoUniqueBeanDefinitionException
。最常见的情况是:==当一个接口有多个实现类,调用时使用接口对象引用子类==。
比如:Human接口有两个实现类:Man类和 Woman类
package cn.book.main.entity;
public interface Human {
}
package cn.book.main.entity;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Component
public class Man implements Human {
public Man() {
System.out.println("I am man");
}
}
package cn.book.main.entity;
import org.springframework.stereotype.Component;
@Component
public class Woman implements Human {
public Woman() {
System.out.println("I am woman");
}
}
配置类
package cn.book.resource;
import cn.book.main.pojo.TestCondition;
import org.springframework.context.annotation.*;
@Configuration
@ComponentScan("cn.book.main.entity")
public class HumanConfig {
}
测试类,自动注入一个Human接口。此时,spring会产生:NoUniqueBeanDefinitionException
。
package cn.book.test;
import cn.book.main.entity.Human;
import cn.book.resource.HumanJobConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=cn.book.resource.HumanJobConfig.class)
public class HumanJobTest {
@Autowired
private Human human;
@Test
public void Test(){
System.out.println(human.getClass());
}
}
当确实发生装配的歧义性时,Spring 提供了以下方案:
- 将可选 Bean 中的某一个设为首选(primary) 的 Bean;
- 使用限定符(qualifier)限定到符合的、唯一的 Bean;
3.3.1 标示首选 Bean
标示首选需要使用关键字 primary,它在 JavaConfig 中是注解 @Primary
,在 XML 是 bean
元素中的属性 primary。
JavaConfig 配置
@Primary
注解配合 @Component
和 @Bean
注解组合使用,在需要设置为首选的组件类和 Bean 对象上。
与 @Component
注解配合使用
@Component
@Primary
public class Man{
}
或者,与 @Bean
注解配合使用
@Configuration
public class JavaConfig{
@Bean
@Primary
public Human getMan(){
return new Man();
}
}
在 XML 中设置 Bean 为首选项的配置为:
<bean id="man" class="Man" primary="true"/>
缺点
- 不能设置多个首选 Bean;
- 不够灵活,存在歧义性时,只能装配使用设置首选的Bean;
3.3.2 限定符限定装配
限定符 @qualifier
注解,主要作用是在可选的 Bean 进行缩小范围选择,直到找到满足的 Bean。它的有两个作用:
-
与
@Autowired
和@Inject
协同使用,在注入的时候指定想要注入的是哪个 Bean;@qualifier("")
括号内所设置的参数时要注入 Bean 的 ID 值。 -
与
@Component
和@Bean
协同使用,为 Bean 设定限定符;@qualifier("")
括号内是为 Bean 设置的限定符,在注入时使用qualifier
中引用。
3.3.3 限定符注解
如果使用注解 @qualifier
限定符依旧无法解决 bean 的装配歧义性问题时,而且,在 Spring 中无法重复使用相同的 @qualiifer
注解,在这种情况下,可以自定义注解来区分 bean。那么,如何自定义注解呢?
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,
ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface 注解名 {
}
注意:
- 自定义注解不能使用在类上;
- 使用自定义注解时,需要同时放在 声明Bean的地方 和 注入 Bean 的地方;
4、Bean 作用域
在默认情况下,Spring 应用上下文中所有的 Bean 都是以单例形式创建的。也就是,不管一个 Bean 被注入多少次,每次注入的 Bean 都是同一个实例。
如果一个实例需要保持无状态并在应用中重复使用,单例作用域是不可行且不安全的。在 Spring 定义了多种作用域,Spring 会基于这些作用域创建 Bean,这些作用域包括:
- 单例(Singleton):在整个应用,只会创建 Bean 的一个实例;
- 原型(Prototype):每次注入或通过 Spring 应用上下文获取时,都会创建一个新的 Bean 实例;
- 会话(Session):在 Web 应用中,为每个会话创建一个 Bean 实例;
- 请求(Request):在 Web 应用中,为每个请求创建一个 Bean 实例;
单例是默认的作用。如果想要选择其它作用域,要使用 @Scope
注解。注解内使用以下表示作用域的参数:
-
ConfigurableBeanFactory.SCOPE_PROTUTYPE
或者"prototype"
; -
ConfigurableBeanFactory.SCOPE_SESSION
或者"session"
; -
ConfigurableBeanFactory.SCOPE_REQUEST
或者"request"
;
如果使用 XML 配置,在 <bean>
元素中的属性 scope
设置 bean 的作用域。
4.1 会话和请求作用域
==学习到 Web 部分内容再深入学习==
5、运行时值注入
前面在装配 Bean,讲到在创建 Bean 时,将常量(比如int类型、String类型)直接给定,这是将值硬编码到 Bean 中。有时,为了避免硬编码值,想让这些值在运行时在确定,Spring 提供了两种在运行时求值的方式:
- 属性占位符
- Spring 表达式语言
5.1 注入外部值
回顾一下,在我们使用 JDBC 时,会创建一个属性文件 *.properties
文件放置连接数据库所需的配置参数。假设,在 Spring 中该文件依旧存在,我们如何在配置类或配置文件中解析并取值?
JavaConfig 配置类
- 通过注解
@PropertySource
中的value属性设置属性文件路径; - 自动注入 Environment 对象;
- 通过 Environment 对象获取属性值;
@Configuration
@PropertySource(value = "classpath:/JDBC.properties")
public class JdbcConfig {
@Autowired
Environment env;
@Bean
public JdbcParams getJdbc(){
return new JdbcParams(
env.getProperty("jdbc.driver"),
env.getProperty("jdbc.url"),
env.getProperty("jdbc.username"),
env.getProperty("jdbc.password")
);
}
}
public class JdbcParams {
private String driver;
private String url;
private String username;
private String password;
public JdbcParams() {
}
public JdbcParams(String driver, String url, String username, String password) {
this.driver = driver;
this.url = url;
this.username = username;
this.password = password;
}
}
Environmen 接口的用法,通过 Environment 接口可以调用以下方法:
方法 | 描述 |
---|---|
String getProperty(String key) |
根据指定值获取属性,属性没有定义返回null; |
String getProperty(String key, String defaultValue) |
根据指定值获取属性,如果没有属性值,则返回defaultValue; |
T getProperty(String key, Class<T> type) |
返回指定类型的属性值;type为指定类型的.class |
T getProperty(String key, Class<T> type,T defaultValue) |
返回指定类型的属性值;type为指定类型的.class,如果没有属性值,则返回defaultValue; |
getRequiredProperty(String key) |
根据指定值获取属性,属性没有定义抛出异常; |
containProperty(String key) |
检查属性文件是否存在某个属性; |
T getPropertyAsClass(String key,Class<T> type) |
将属性文件解析为指定的类文件; |
String[] getActiveProfiles() |
返回激活 profile 名称的数组; |
String[] getDefaultProfiles() |
返回默认 profile 名称的数组; |
boolean acceptsProfiles(String... profiles) |
如果 environment 支持给定的 profile 的话,就返回 true; |
5.2 占位符注入值
Spring 支持将属性定义到外部的属性文件中,并使用占位符将值插入到 Bean 中。在 Spring 装配中,占位符的形式为使用 ${...}
包装的属性名称。
为了使用占位符,需要配置一个 PropertySourcePlaceholderConfigurer
Bean,它能够基于 Environment 及其属性源来解析占位符。下面来看看,JavaConfig 配置和 XMl 配置中使用占位符的用法
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
@Configuration
//声明属性源,并将属性文件加载到Spring
@PropertySource(value = "classpath:/JDBC.properties")
public class StudentCongif {
二、
//(1)使用占位符解析属性
@Bean
public JdbcParams getJdbc(
@Value("${jdbc.driver}") String driver,
@Value("${jdbc.url}") String url,
@Value("${jdbc.username}") String username,
@Value("${jdbc.password}") String password){
return new JdbcParams(driver,url,username,password);
}
//(2)还需要配置一个PropertySourcesPlaceholderConfigurer 的 bean
@Bean
public PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcesPlaceholderConfigurer();
}
}
<!--创建 PropertySourceHolderConfigurer -->
<context:property-placeholder location="classpath:/JDBC.properties"/>
<!-- 使用占位符进行值注入-->
<bean id="jdbc" class="cn.book.main.valueInject.JdbcParams"
c:driver="${jdbc.driver}"
c:url="${jdbc.url}"
c:username="${jdbc.username"
c:password="${jdbc.password}"/>
解析外部属性能够将值的处理推迟到运行时,但它的关注点在于根据名称解析来自 Spring Environment 和属性源的属性。