第三章 高级装配
标签(空格分隔): 未分类
[TOC]
环境与profile
配置profile bean
在开发过程中,不同开发环境可能会出现代码不同的情况,比方说:一般开发和测试会有两个不同的环境和数据库等等。Spring引入了Profile
的功能,要使用Profile
,你需要将不同的Bean
整理到一个或者多个Profile
中,将应用部署到每个环境的时候,需确定对应的Profile
处于激活状态(active
)
在Java中,可以使用@Profile
注解指定某个bean
属于哪个profile
ProfileConfig.class
@Configuration
public class ProfileConfig {
@Bean
@Profile("dev") // 指定下面的bean属于dev profile
public DemoBean devBean(){
return new DemoBean("这是测试环境");
}
@Bean
@Profile("prod") // 指定下面的bean属于prod profile
public DemoBean prodBean(){
return new DemoBean("这是生产环境");
}
}
MainTest.java
public class MainTest {
public static void main(String []args){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.getEnvironment().setActiveProfiles("dev"); // 激活某些profile状态
context.register(ProfileConfig.class);
context.refresh();
DemoBean demoBean = context.getBean(DemoBean.class);
System.out.println(demoBean.getContent());
context.close();
}
}
@Profile
注解不仅可以加到@Bean
的下方,还可以加到类级别上(@Configuration
):
@Configuration
@Profile(value="prod")
public class ProductionProfileConfig{
// ...
}
表示仅有当@Profile
中的环境激活时(也就是例子中的prod,dev...),才可以创建相应的bean
除了在Java文件中加上注解之外,我们还可以通过XML配置文件的方法来配置相应的profile:
spring-config.xml
当prod的profile被激活时,spring-config.xml才会被用到。
我们还可以在一个XML文件中通过
标签嵌套定义多个profile
重复使用
元素来指定多个profile
spring-config.xml
...
....
激活profile
激活profile需要依赖两个独立的属性:
spring.profiles.active // 优先级要比default高
spring.profiles.default // 如果active没有设置,才会查找default中的值
系统会优先使用spring.profiles.active
中所设置的profile
如果spring.profiles.active
和spring.profiles.default
均没有设置的话,那就没有激活的profile,因此,只会创建那些没有定义在profile中的bean 。
有多种方式来设置这两个属性:
- 作为
DispatcherServlet
的初始化参数 - 作为Web应用的上下文参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试上,使用
@ActiveProfiles
注解设置
在web应用中的web.xml中设置默认的profile
web.xml
index.jsp
spring.profiles.default
dev
org.springframework.web.context.ContextLoaderListener
appServlet
org.springframework.web.servlet.DispatcherServlet
spring.profiles.default
dev
1
appServlet
/
使用profile进行测试
Spring提供了 @ActiveProfiles
注解,来指定运行测试时需要激活哪个profile
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-config.xml")
@ActiveProfiles("prod")
public class UserDaoTest {
// ...
}
条件化的bean
假如我们希望某个bean只有当另一个特定的bean声明了之后才会创建,或者需要某个特定的环境变量才会创建,就需要用到 @Conditional
注解
它可以用在带有 @Bean
注解的方法上,如果给定的条件计算结果为true,就创建这个bean,否则就不创建。
例如,假设有一个MagicBean
的类,当设置了magic环境属性的时候,Spring才会实例化这个类:
MagicBeanConfig.class
@Configuration
public class MagicBeanConfig {
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean(){
return new MagicBean();
}
}
@Conditional
给定了一个class ,它指明了一个条件 —— 在本例中,也就是 MagicExistsCondition
类 。MagicExistsCondition
需要实现Condition
接口。
@Conditional
将会通过 Condition 接口进行对比:
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
设置给 @Conditional
的类可以是任意实现了Condition
接口的类型 ,实现 Condition
接口需要实现matches
方法。如果 matches() 方法返回 true , 那么就会创建带有 @Conditional
注解的bean,否则就不会创建这些 bean
MagicExistsCondition.class
public class MagicExistsCondition implements Condition {
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Environment environment = conditionContext.getEnvironment();
// 检查环境中是否存在magic的环境属性
return environment.containsProperty("magic");
}
}
MainTest.class —— 测试类
public class MainTest {
public static void main(String []args){
System.setProperty("magic","true");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MagicBeanConfig.class);
// 打印 ture
System.out.println(context.containsBean("magicBean"));
context.close();
}
}
Condition
实现的考量要比本例中的更多,mathes()
方法会得到ConditionContext
和AnnotatedTypeMetadata
对象用来做出决策 。
ConditionContext
是一个接口,大致如下所示:
public interface ConditionContext {
// 检查bean定义
BeanDefinitionRegistry getRegistry();
// 检查bean是否存在,甚至探查bean的属性
ConfigurableListableBeanFactory getBeanFactory();
// 检查环境变量是否存在以及它的值是什么
Environment getEnvironment();
// 返回ResourceLoader所加载的资源
ResourceLoader getResourceLoader();
// 加载并检查类是否存在
ClassLoader getClassLoader();
}
AnnotatedTypeMetadata
能让我们检查带有 @Bean
注解的方法上还有什么其他的注解 , AnnotatedTypeMetadata
也是一个接口,他如下所示 :
public interface AnnotatedTypeMetadata {
// 判断方法是否还有其他注解的属性
boolean isAnnotated(String var1);
@Nullable
Map getAnnotationAttributes(String var1);
@Nullable
Map getAnnotationAttributes(String var1, boolean var2);
@Nullable
MultiValueMap getAllAnnotationAttributes(String var1);
@Nullable
MultiValueMap getAllAnnotationAttributes(String var1, boolean var2);
}
返回到这篇笔记刚开始的地方,我们看下 @Profile
是如何实现的:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
String[] value();
}
@Profile
本身也是用了 @Conditional
注解 ,并引用ProfileCondition
作为 Condition 的实现 。
ProfileCondition 检测某个 bean profile 是否可用
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 获取用于@Profile注解的所有属性
MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
// 获取value属性
for (Object value : attrs.get("value")) {
// 检查value属性中的profile是否处于激活状态
if (context.getEnvironment().acceptsProfiles((String[]) value)) {
return true;
}
}
return false;
}
return true;
}
}
自动处理装配的歧义性
仅有一个bean匹配所需的结果时 ,自动装配才是有效的 。如果不仅有一个bean能够匹配结果的话 ,这种歧义性会阻碍 Spring 自动装配属性 、构造器参数或者方法参数 。
在发生歧义时,可以选择bean中的某一个设为首选(Primary)的bean 。或者使用限定符(Quailfier)来帮助Spring将可选的bean的范围缩小到只有一个bean 。
标志首选的bean --- @Primary
甜点的例子
public interface Dessert {
void play();
}
@Component
public class Cake implements Dessert {
// ...
}
@Component
public class Cookie implements Dessert {
// ...
}
@Component
public class IceCream implements Dessert {
// ...
}
在本例中, Dessert
是一个接口 ,并且有三个类实现了这个接口 。因为这三个类均使用了 @Component
注解 , 在组件扫描时 ,能够发现他们并且将其创建为Spring上下文参数的bean 。
@Component
public class DessertMachine implements Machine {
private Dessert dessert;
@Autowired
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
public void play(){
dessert.play();
}
}
当Spring试图装配 setDessert()
中的 Dessert
参数时 ,它并没有唯一、无歧义的可选值 。Spring会抛出相应的异常 。
在Spring中,可以使用 @Primary
来表达最喜欢的方案, @Primary
和@Component
组合用在组件扫描的bean上,例如:
@Primary
@Component
public class IceCream implements Dessert {
// ...
}
@Primary
也可以与 @Bean
组合用在Java 配置的bean 声明中。
如果你使用XML配置bean的话,
元素有一个primary
属性来指定首选的bean 。
当时,如果你标识了两个或者更多的首选bean ,那么它就无法正常工作了。
限定自动装配的bean
@Qualifier
注解是使用限定符的主要方式 , 可以和 @Autowired
协同使用,在注入的时候指定想要注入哪一个bean 。
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
为@Qualifier
注解所设置的参数就是想要注入的bean的ID 。因为所有使用 @Component
注解的声明的类都会创建为bean ,并且bean的ID为首字母变为小写的类名,所以,@Qualifier("iceCream")
指向的是组件扫描时所创建的bean,并且这个bean是 IceCream
的实例 。
这里的问题是如果重构了IceCream类,bean的ID和默认的限定符就会变,这里就无法匹配setDessert()
中指定的限定符 。自动装配会失败 。
@Qualifier
可以自定义名称,做到与类名(或者说bean的ID)解耦 :
@Component
@Qualifier("cold")
public class IceCream implements Dessert {
public void play() {
System.out.println("这是 IceCream");
}
}
这样在装配bean的时候就需要引用cold
限定符了 :
@Autowired
@Qualifier(value = "cold")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
当程序员自定义@Qualifier
值时,最佳实践是为bean选择特征性或者描述性的术语。在IceCream
中,我们将Qualifier
自定义为cold
,假设另外一个Dessert
的实现类也是cold
的话 :
@Component
@Qualifier(value = "cold")
public class Popsicle implements Dessert {
public void play() {
System.out.println("这是 水果冰棒");
}
}
程序再次出现了歧义性的问题 。
Java8允许出现重复的注解,只要这个注解本身在定义的时候带有 @Repeatable
注解就可以。不过,Spring的@Qualifier
注解并没有在定义时添加@Repeatable
注解。所以,下面这种使用方法是错误的:
@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
那么,我们应该如何区分IceCream
和Popsicle
类呢 ?
创建自定义的限定符注解:
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {
// ...
}
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy {
// ...
}
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Fruit {
// ...
}
现在,我们可以重新看下 IceCream
,并为其添加以下注解:
@Component
@Cold
@Creamy
public class IceCream implements Dessert {
// ...
}
// ...
@Component
@Cold
@Fruit
public class Popsicle implements Dessert {
// ...
}
最终,在注入点,我们使用必要的限定符注解进行任意组合,从而将可选范围缩小到只有一个bean满足需求 。
@Autowired
@Cold
@Fruit
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
为了创建自定义的条件化注解,我们创建了一个新的注解并在这个注解上添加了@Conditional
。为了创建自定义的限定符注解,我们创建一个新的注解并在这个注解上加上 @Qualifier
自定义@Conditional注解示例
ConditionalOnMyProperties.class
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnMyPropertiesCondition.class)
public @interface ConditionalOnMyProperties {
String name();
}
OnMyPropertiesCondition.class
public class OnMyPropertiesCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 获取注解上的name属性
Object propertiesName = metadata.getAnnotationAttributes(ConditionalOnMyProperties.class.getName()).get("name");
if (propertiesName != null){
// 检查环境中是否存在该属性的值
boolean value = context.getEnvironment().containsProperty(propertiesName.toString());
if(value){
return true;
}
}
return false;
}
}
HelloWorld.class
public class HelloWorld {
public void print() {
System.out.println("hello world");
}
}
ConditionClass.class
@Configuration
@ConditionalOnMyProperties(name = "message")
public class ConditionClass {
@Bean
public HelloWorld helloWorld(){
return new HelloWorld();
}
}
public static void main(String[] args) {
System.setProperty("message","something");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConditionClass.class);
try {
context.getBean(HelloWorld.class).print();
} catch (Exception e) {
e.printStackTrace();
}
}
bean的作用域
在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多少次,每次所注入的都是同一个实例。
Spring定义了多种作用域,可以基于这些作用域创建bean
- 单例(Singleton):在整个应用中,只创建bean的一个实例。
- 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
- 会话(Session):在Web应用中,为每个会话创建一个bean实例。
- 请求(Request):在Web应用中,为每个请求创建一个bean实例。
单例是默认的作用域,但对于易变(mutable)的类型来说,这种作用域并不合适,如果选择其他的作用域,要使用@Scope
注解,他可以和@Component
或者@Bean
一起使用。
例如:
如果你使用组件扫描来发现bean,那么你可以在bean的类上使用 @Scope
注解,将其声明为原型bean 。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class NotePad{
// ...
}
如果你想在Java配置(显示配置)中将Notepad声明为原型bean,那么可以组合使用 @Scope
和 @Bean
来指定所需要的作用域。
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad(){
return new Notepad();
}
你还可以在XML中设置作用域:
使用会话和请求作用域
在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事情。就购物车bean来说,每一个用户都需要有不同的购物车,所以会话作用域是最为合适的,因为它与给定的用户关联性最大。
要指定会话作用域,我们可以使用@Scope
注解:
@Component
@Scope(value= WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.INTERFACES)
)
// 当ShoppingCart是一个接口时
public ShoppingCart cart(){
// ...
}
在这里,我们将value设置为了WebApplicationContext
中的SCOPE_SESSION
常量,这表明Spring为web应用中的每个会话创建一个实例。
要注意的是,@Scope
还有一个proxyMode
属性,它被设置为了ScopedProxyMode.INTERFACES
。这个属性解决了将会话或者请求作用域的bean注入到单例bean中所遇到的问题 :
假设我们需要将ShoppingCart
的bean注入到单例StoreService
bean的setter方法中:
@Component
public class StoreService{
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart){
this.shoppingCart = shoppingCart;
}
}
因为StoreService
是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCart
bean注入到setShoppingCart()
方法中。但是ShoppingCart
bean 是会话作用域的,此时并不存在。直到某个用户进入了系统,创建了会话之后,才会出现ShoppingCart
的实例。
另外,系统中将会出现多个ShoppingCart
实例,我们并不希望将特定的ShoppingCart
注入到StoreService
中,我们希望的是当StoreService
处理购物车功能时,它所使用的ShoppingCart
恰恰是当前会话所对应的那个。
Spring并不会将实际的ShoppingCart
bean注入到StoreService
中,Spring会注入一个到ShoppingCart
bean的代理:
[图片上传失败...(image-19b5ab-1533698521996)]
这个代理会暴露与ShoppingCart
相同的方法,所以StoreService
会认为它就是一个购物车。但是,当StoreService
调用购物车方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart
bean 。
如配置所示,proxyMode
被设置成为了ScopedProxyMode.INTERFACES
,这表明这个代理要实现ShoppingCart
接口,并将调用委托给实现bean。
如果ShoppingCart
是一个接口而不是一个类,这是可以的。但是如果ShoppingCart
是一个具体的类,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体的类的话,我们必须要将proxyMode
属性设置为ScopedProxyMode.TARGET_CLASS
请求作用域和会话作用域一样,也需要以作用域的方式进行注入。
在XML中使用作用域代理
中的scope属性能够设置bean的作用域,
是和@Scope
注解中的proxyMode
属性功能相同的Spring XML配置元素。它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLib创建目标类的代理。但是,我们可以将proxy-target-class
属性设置为false,进而告诉它生成基于接口的代理。
运行时值注入
在讨论依赖注入的时候,我们通常所讨论的是将一个bean引用注入到另一个bean的属性或者构造器参数中。它通常来讲是将一个对象与另一个对象进行关联。
然而还有另一个方面是将一个值注入到bean的属性或者构造器中。
例如:我们将专辑的名字装配到BlanckDisc
bean 的构造器或者title属性中:
BlankDisc.class
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
public BlankDisc(String title, String artist) {
this.title = title;
this.artist = artist;
}
@Override
public void play() {
System.out.print("Playing " + title + " by " + artist);
}
}
使用XML装配:
我们还可能使用Java的方式进行装配:
@Bean
public CompactDisc sgtPeppers(){
return new BlankDisc("Sgt. Pepper's Lonely Hearts Club Band","The Beatles");
}
但是,以上两种方式都是将值以固定编码的形式注入到bean中的。Spring提供了两种方式让这些值在运行时注入:
- 属性占位符
- Spring表达式语言
注入外部的值
在Spring中,获取外部值最简单的方式是声明数据源并通过Spring的Enviroment
来获取属性值 :
@Configuraion
@PropertyResource(value="classpath:/app.properties")
public class ExpressiveConfig{
@Autowired
private Enviroment enviroment;
@Autowired
private BlankDisc disc(){
return new BlankDisc(enviroment.getProperty("disc.title"),
enviroment.getProperty("disc.artist")
);
}
}
app.properties
disc.title = 寻宝游戏
disc.artist = vae
public interface CompactDisc {
void play();
}
// ...
public class BlankDisc implements CompactDisc{
private String title;
private String artist;
public BlankDisc(String title , String artist) {
this.title = title;
this.artist = artist;
}
public void play() {
System.out.println("title : " + title + " artist : " + artist);
}
}
在Enviroment
类中还有其他一系列的方法,其中包括getProperty()
方法的重载以及其他方法 :
public interface PropertyResolver {
// 检查某个属性是否存在
boolean containsProperty(String var1);
String getProperty(String var1);
// 当指定属性var1不存在时候,返回默认值var2
String getProperty(String var1, String var2);
@Nullable
// 返回非字符串类型T
T getProperty(String var1, Class var2);
// 当指定属性var1不存在时候,返回指定类型T var3
T getProperty(String var1, Class var2, T var3);
// 当指定属性不存在时候报错
String getRequiredProperty(String var1) throws IllegalStateException;
T getRequiredProperty(String var1, Class var2) throws IllegalStateException;
String resolvePlaceholders(String var1);
String resolveRequiredPlaceholders(String var1) throws IllegalArgumentException;
}
除了属性相关功能之外,Enviroment
对象还提供了一些方法来检查哪些profile
处于激活状态 :
public interface Environment extends PropertyResolver {
// 获取到profile为active的数组
String[] getActiveProfiles();
// 获取到profile为default的数组
String[] getDefaultProfiles();
// 如果environment支持给定的profile 就返回true
boolean acceptsProfiles(String... var1);
}
解析属性占位符
使用XML方式
使用Java配置方式
@Configuration
@ComponentScan(basePackages = "com.springdemo")
@PropertySource(value = "classpath:/app.properties")
public class ExpressiveConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcesPlaceholderConfigurer();
}
}
@Component
public class BlankDisc implements CompactDisc {
// ...
public BlankDisc(
@Value("${disc.title}") String title ,
@Value("${disc.artist}") String artist) {
this.title = title;
this.artist = artist;
}
}
总结
在这章中我们主要学习了以下内容 :
-
@Profile
注解 : 主要用来区别不同开发或者运行环境问题,例如是mysql还是oracle数据库等等 - 激活
profile
的几种方法 : 作为WEB应用上下文、作为JNDI条目、作为环境变量、作为JVM系统属性、在集成测试中使用@ActiveProfiles
注解设置 -
@Conditional
注解 :@Profile
的“升级版”,设置条件化的Bean,@Conditional
注解中的类必须要实现Conditional
接口中的matches()
方法 -
@Primary
注解 : 标示首选的bean(不建议使用) -
@Qualifier
注解 : 消除装配时的歧义性 。也可以通过@Qualifier
自定义限定符注解 。 -
bean
的四种作用域 :单例、原型 、会话 、请求 -
@Scope
注解修改bean的作用域 :@Scope(ConfigurationBeanFactory.SCOPE_PROTOTYPE)
,如果是会话/请求作用域的话,除了设置@Scope(value = WebApplicationContext.SCOPE_SESSION)
之外,还需要设置proxyMode=ScopedProxyMode.INTERFACE
或者proxyMode=ScopedProxyMode.TARGET_CLASS