一、环境与profile
配置Profile bean
环境多样导致配置多样:
在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另外一个环境。开发阶段中,某些环境相关做法可能并不适合迁移到生产环境中,甚至即便迁移过去也无法正常工作。数据库配置、加密算法以及与外部系统的集成是跨环境部署时会发生变化的几个典型例子。适配多环境配置常见处理方案:
- maven的profile:
在单独的配置类(或XML文件)中配置每个bean,然后在构建阶段(可能会使用Maven的profiles)确定要将哪一个配置编译到可部署的应用中。这种方式的问题在于要为每种环境重新构建应用。(构建阶段指定环境对应的配置) - spring的profile:
Spring为环境相关的bean所提供的解决方案其实与构建时的方案没有太大的差别。当然,在这个过程中需要根据环境决定该创建哪个bean和不创建哪个bean。不过Spring并不是在构建的时候做出这样的决策,而是等到运行时再来确定。这样的结果就是同一个部署单元(可能会是WAR文件)能够适用于所有的环境,没有必要进行重新构建。(运行时决策对应配置,不必重新构建)
- maven的profile:
springprofile使用的配置:
Spring3.1版本只适用于类级别上:
@Configuration @Profile("dev") public Class Bean1Config{ @Bean ... }
这个配置类中的bean只有在dev profile激活时才会创建。如果dev profile没有激活的话,那么带有@Bean注解的方法都会被忽略掉。
Spring3.2版本支持方法级别上的profile配置,与@Bean注解一同使用:
@Configuration public class SystemBeanConfig{ @Bean @Profile("dev") public DataSource emebeddedDataSource(){ return new EmebeddedDatabaseBuilder() .setType(EmebeddedDatabaseType.H2) .addScript("classpath:init.sql") .addScript("classpath:tes-data.sql") .builer(); } @Bean @Profile("prod") public DataSource jndiDataSource(){ JndiObjectFactory jndiFactory=new JndiObjectFactory(); jndiFactory.setJndiName(""); jndiFactory.setResourceRef(true); jndiFactory.setProxyInterface("javax.sql.DataSource.class"); return (DRSource)jndiFactory.getObject(); } }
尽管每个DataSource bean都被声明在一个profile中,并且只有当规定的profile激活时,相应的bean才会被创建,但是可能会有其他的bean并没有声明在一个给定的profile范围内。没有指定profile的bean始终都会被创建,与激活哪个profile没有关系。
适用于XML配置-为每个环境配置对应的profilebean:
<?xml version="1.0" encoding="utf-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ... profile="dev"> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:init.sql"/> <jdbc:script location="classpath:tes-data.sql"/> </jdbc:embeded-database> ... </beans>
同样,可以创建基于连接池定义的DataSource bean,将其放在另外一个XML文件中,并标注为qaprofile。所有的配置文件都会放到部署单元之中(如WAR文件),但是只有profile属性与当前激活profile相匹配的配置文件才会被用到。
- xml中重复使用元素来指定多个profile
```
<?xml version="1.0" encoding="utf-8"?>
<beans ...>
<beans profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:init.sql"/>
<jdbc:script location="classpath:tes-data.sql"/>
</jdbc:embeded-database>
</beans>
<beans profile="prod">
<jee: jndi-lookup id="datasource"
jndi-name="jndi/myDatabase"
resource-ref="true"
proxy-interface="javax.sql.DataSource"/>
</beans>
</beans>
```
除了所有的bean定义到了同一个XML文件之中,这种配置方式与定义在单独的XML文件中的实际效果是一样的。这里有三个bean,类型都是javax.sql.DataSource,并且ID都是dataSource。但是在运行时,只会创建一个bean,这取决于处于激活状态的是哪个profile。
激活profile
使用属性激活profile:
- spring.profiles.active
- spring.profiles.default
如果设置了spring.profiles.active属性的话,那么它的值就会用来确定哪个profile是激活的。但如果没有设置spring.profiles.active属性的话,那Spring将会查找spring.profiles.default的值。如果spring.profiles.active和spring.profiles.default均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在profile中的bean。
多种方式设置属性激活profile:
- 作为DispatcherServlet的初始化参数
- 作为Web应用的上下文参数
- 作为JNDI的条目
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试类上,使用@ActiveProfiles设置
spring.profiles.active和spring.profiles.default中,profile使用的都是复数形式。这意味着你可以同时激活多个profile,这可以通过列出多个profile名称,并以逗号分隔来实现。当然,同时启用dev和prod profile可能也没有太大的意义,不过你可以同时设置多个彼此不相关的profile。
二、条件化创建bean
假设你希望一个或多个bean只有在应用的类路径下包含特定的库时才创建。或者我们希望某个bean只有当另外某个特定的bean也声明了之后才会创建。我们还可能要求只有某个特定的环境变量设置之后,才会创建某个bean。
在Spring 4之前,很难实现这种级别的条件化配置,但是Spring4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。
(http://wiselyman.iteye.com/blog/2002449)
使用@Conditional:
- 类级别可用在@Component(包含@Configuration)的类上
- 作为meta-annotation,组成自定义注解
- 方法级别可放到标记@Bean注解的方法上
如果一个@Configuration的类标记了@Conditional,所有标识了@Bean的方法和@Import注解导入的相关类将遵从这些条件。
案例:public class LinuxCondition implements Condition{ @Override public boolean match(ConditionContext context,AnnotatedTypeMetadata metaddata){ return context.getEnvironment().getProperty("os.name").contains("Linux"); } }
public class WindowsCondition implements Condition{ @Override public boolean match(ConditionContext context,AnnotatedTypeMetadata metaddata){ return context.getEnvironment().getProperty("os.name").contains("Windows"); } }
@Configuration public class SystemEmailConfig{ @Bean(name="emailService") @Conditional(WindowsCondition.class) public EmailService windowsEmailerService(){ return new WindwosEmailService(); } @Bean(name="emailService") @Conditional(LinuxCondition.class) public EmailService linuxEmailerService(){ return new LinuxEmailService(); } }
当符合条件时,@Bean才会被初始化
@Conditional注解简单解析:
@Condtional注解接口
@Conditional将会通过Condition接口进行条件对比:package org.springframework.context.annotation; import ... @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { Class<? extends Condition>[] value(); }
- Condition接口
设置给@Conditional的类,需要实现Condition方法,重写matches实现。
public interface Condition{ boolean matches( ConditionContext ctxt,AnnotatedTypeMetadata metadata); }
当条件满足时创建该@Bean标示的类,否则不创建
ConditionContext 接口
ConditionContext源码如下:public interface ConditionContext { BeanDefinitionRegistry getRegistry(); ConfigurableListableBeanFactory getBeanFactory(); Environment getEnvironment(); ResourceLoader getResourceLoader(); ClassLoader getClassLoader(); }
- 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义;
- 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性;
- 借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么;
- 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源;
- 借助getClassLoader()返回的ClassLoader加载并检查类是否存在。
AnnotatedTypeMetadata接口
AnnotatedTypeMetadata接口源码如下:package org.springframework.core.type; import ... public interface AnnotatedTypeMetadata { boolean isAnnotated(String var1); Map<String, Object> getAnnotationAttributes(String var1); Map<String, Object> getAnnotationAttributes(String var1, boolean var2); MultiValueMap<String, Object> getAllAnnotationAttributes(String var1); MultiValueMap<String, Object> getAllAnnotationAttributes(String var1, boolean var2); }
借助isAnnotated()方法,我们能够判断带有@Bean注解的方法是不是还有其他特定的注解。借助其他的那些方法,我们能够检查@Bean注解的方法上其他注解的属性。
@Conditional重要使用案例:
Spring4开始,@Profile注解进行了重构,使其基于@Conditional和Condition实现。ProfileCondition实现了Condition接口,并且在做出决策的过程中,考虑到了ConditionContext和AnnotatedTypeMetadata中的多个因素。package org.springframework.context.annotation; import ... @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Documented @Conditional({ProfileCondition.class}) public @interface Profile { String[] value(); }
ProfileCondition类检查某个beanProfile是否可用
class ProfileCondition implements Condition{ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { if(context.getEnvironment() != null) { MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if(attrs != null) { Iterator var4 = ((List)attrs.get("value")).iterator(); Object value; do { if(!var4.hasNext()) { return false; } value = var4.next(); } while(!context.getEnvironment().acceptsProfiles((String[])((String[])value))); return true; } } return true; } }
自动化装配的歧义
自动装配让Spring完全负责将bean引用注入到构造参数和
属性中。自动装配能够提供很大的帮助,因为它会减少装配应用程序组件时所需要的显式配置的数量。
不过,仅有一个bean匹配所需的结果时,自动装配才是有效的。如果不仅有一个bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器参数或方法参数。
例如,当多个类都实现了都实现了同一个接口,@Autowired标记需要注入该接口类型的对象,当Spring视图自动装配时无法找到唯一无歧义的对应值,spring无法做出选择,宣告失败抛出异常:NoUniqueBeanDefinitionException。
因此当出现这种歧义时,Spring提供了多种方法来解决:设置首选或者限定bean。
标示首选的bean
@Primary
@Component @Primary public class bean1 implements IBean{...}
或者:
@Bean @Promary public Bean1 getBean1(){ return new Bean1(); }
XML中primary=true属性
<bean id="bean1" class="com.xx.Bean1" primary="true"/>
无论使用哪种方法来标示首选bean,都是高速Spring当发生歧义时选择标为首选的bean。
如果同一类bean标示了多于一个的首选bean,那么歧义就人存在,所以首选bean要确定唯一无歧义。
限定自动装配的bean
设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义性的选项中。它只能标示一个优先的可选方案。当首选bean的数量超过一个时,我们并没有其他的方法进一步缩小可选范围。
与之相反,Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的限定符都用上后依然存在歧义性,那么你可以继续使用更多的限定符来缩小选择范围。
@Qualifier+默认BeanId
@Qualifier注解可以与@Autowired和@Inject注解协同使用,在注入时制定特定的beanID:@Component public class Bean1 implements IBean{...} @Component public class Bean2 implements IBean{...}
当需要制定Bean1为某个注入的对象时,使用@Qualifier与@Autowired,注入bean1的id即可让bean1作为特定注入值:
@Autowired @Qualifier("bean1") private IBean bean1;
@Component标记的对象,默认给定的id是类名首字母小写
基于默认的bean ID作为限定符是非常简单的,但会引入一些问题。如果你重构了类,将其重命名的话,bean的ID和默认的限定符会改变,这就无法匹配到方法中的限定符。自动装配会失败。
方法上所指定的限定符与要注入的bean的名称是紧耦合的。对类名称的任意改动都会导致限定符失效。自定义限定符:
自定义限定符:
由于默认的id重命名后改变导致限定符失效,所以为bean设置自己的限定符,而不依赖于bean ID作为限定符可以解决这个问题:@Qualifier+@Component:
@Component @Qualifier("cold") @Qualifier("beautiful") public class Bean1{..}
@Qualifier+@Bean
@Bean @Qualifier("cold") public IBean bean2(){ return new Bean2(); }
@Qualifier限定注入的不足,创建使用自定义限定注解
面向特性的限定符要比基于bean ID的限定符更好一些。但是,如果多个bean都具备相同特性的话,这种做法也会出现问题,所以可以使用多个@Qualifier来缩小限定bean。在注入点中也回使用多个@Qualifier来限定bean的范围;
@Autowired @Qualifier("cold") @Qualifier("beautiful") private IBean bean;
但是Java不允许在同一个条目上重复出现相同类型的多个注解,所以以上代码编译会报错,@Qualifier注解在有多个限定条件时无法直接将注入的bean限定到一个唯一有效的范围。
因此,我们可以创建自定义的限定符注解,借助这样的注解来表达bean所希望限定的特性。这里所需要做的就是创建一个注解,它本身要使用@Qualifier注解来标注。这样我们将不再使用@Qualifier(“cold”),而是使用自定义的@Cold注解:@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Cold{};
为Bean1添加这个注解
@Component @Cold @Beautiful public class Bean1{...}
注入点,我们使用必要的限定符注解进行任意组合,从而将可选范围缩小到只有一个bean满足需求:
@Autowired @Cold @Beautiful private IBean bean;
bean的作用域
默认情况下,Spring上下文中所有的bean都是当你形式创建的,也就是说,不管给定的一个bean被注入到其他bean多少次,每次所注入的都是同一个实例。
在大多数情况下,单例bean是很理想的方案。初始化和垃圾回收对象实例所带来的成本只留给一些小规模任务,在这些任务中,让对象保持无状态并且在应用中反复重用这些对象可能并不合理。当所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。在这种情况下,将class声明为单例的bean就不是什么好主意了,因为对象会被污染,稍后重用的时候会出现意想不到的问题。
因此Spring定义了多种bean的作用域,基于这些作用域创建bean:
- 单例(Singleton): 在整个应用中只创建一个实例
- 原型(Prototype):每次注入或者从应用上下文中获取时都创建一个新的实例
- 会话(Session):在web应用中为每个会话创建一个实例
- 请求(Request):在web应用中为每个请求创建一个实例
单例是默认的作用域,但是正如之前所述,对于易变的类型,这并不合适。如果选择其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。
1.使用会话作用域与请求作用域:
@Component
@Scope(value=WebApplicationContext.SESSION_SCOPE)
public class ShopCar {...};
将value设置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。这会告诉Spring为Web应用中的每个会话创建一个ShoppingCart。对于当前应用来说这会创建多个ShoppingCart bean的实例,但是对于给定的会话只会创建一个实例,在当前会话相关的操作中,这个bean实际上相当于单例的。
假设要将ShopCar注入到ShopService(默认单例)的setter中,如下:
@Component
public class ShopService{
...
@Autowired
public ShopCar setterShopCar(ShopCar car){
this.shopCar=car;
}
...
}
因为StoreService是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCart bean注入到setShoppingCart()方法中。但是ShoppingCart bean是会话作用域的,此时并不存在。直到某个用户进入系统,创建了会话之后,才会出现ShoppingCart实例。
另外,系统中将会有多个ShoppingCart实例:每个用户一个。我们并不想让Spring注入某个固定的ShoppingCart实例到StoreService中。我们希望的是当StoreService处理购物车功能时,它所使用的ShoppingCart实例恰好是当前会话所对应的那一个。
Spring并不会将实际的ShoppingCart bean注入到StoreService中,Spring会注入一个到ShoppingCart bean的代理,这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean,如下:
@Component
@Scope(value=WebApplicationContext.SESSION_SCOPE,proxyMode=ScopeProxyMode.INTERFACES)
public class ShopCar {...};
proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart 接口,并将调用委托给实现bean。
如果ShoppingCart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果ShoppingCart 是一个具体的类的话,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。
2.XML中使用作用域
使用步骤是,引入aop命名空间,使用aop-scope标签和标签相关属性给bean设置作用域:
<?xml vesion="1.0" encoding="utf-8"?>
<beans ....
xmlns:aop="https://www.springframework.org/schema/aop"
....
xsi:schemaLocation=...
https://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
...
>
<bean id="cart"
class="XX.XX.Car"
scope="session"/>
<aop:scoped-proxy proxy-target-class="false"/>
</beans>
< aop:scoped-proxy >标签的作用跟@Scope注解的proxy-Mode属性功能相同,默认情况下会CGLib创建目标类的代理,但是proxy-target-class属性设置为false时也可以要求生成基于接口的代理。