《Spring实战》学习笔记(3)——高级装配
上一章学习的是Spring核心的bean装配技术,本章学习一些更高级的装配功能,虽然不一定经常使用,但依然有价值学习。
本章知识点:
- Spring profile
- 条件化的bean声明
- 自动装配与歧义性
- bean的作用域
- Spring表达式语言
一、环境与profile
我们可能会这样获取一个数据源:
@Bean
public DataSource dataSource(){
// 此处省略。。。
}
但在开发环境和生产环境中使用的数据库不相同,dataSource()方法自然也不同。也就是说,在不同的环境下,某个bean会有所不同。那我们用什么方法能够保证在每种环境下都能选择最适合的bean呢。有一个方法是在不同的配置类或者XML文件中配置不同的bean,然后在构建阶段再选择某一个配置编译,缺点是要为每个环境都重新构建应用。
如果不想重新构建怎么办呢,可以使用Spring profile。
1. 配置profile bean
Spring并不是在构建阶段决定创建哪个bean,不创建哪个bean,而是等到运行时再来确定。因此可以做到只构建一次就适用于所有环境。
(1). 在JavaConfig中配置profile
稍微修改一下装配bean使用的例子:
+---java
| +---broadcast
| | Broadcast.java 广播
| |
| +---enemy
| | ArtilleryCorps.java 炮车兵
| | Enemy.java 敌人接口
| | MeleeCreeps.java 近战兵
| | RemoteSoldier.java 远程兵
| | EnemyConfig.java 敌人配置类
| |
| \---hero
| ADCHero.java ADC英雄
| APHero.java AP英雄
| Hero.java 英雄接口
| TankHero.java 坦克英雄
|
\---resources
applicationContext.xml
我们将开发环境,生产环境,测试环境看作是游戏中的不同路线。上路(soluTop)对应TankHero,中路(mid)对应APHero,下路(bot)对应ADHero。于是我们可以创建soluTop的配置类:
@Configuration
@Profile("soloTop")
public class SoloTopProfileConfig {
@Bean
public Enemy remoteSoldier() {
return new RemoteSoldier();
}
@Bean
public Hero hero() {
return new TankHero(remoteSoldier());
}
}
@Profile注解使用在了类级别上,Spring会在soloTop profile激活的时候,才会创建配置类中的bean,如果没有激活,那么配置类中带有@Bean注解的方法都会被忽略。
我们再来写一个mid的配置类:
@Configuration
@Profile("mid")
public class MidProfileConfig {
@Bean
public Enemy remoteSoldier() {
return new RemoteSoldier();
}
@Bean
public Hero hero() {
return new APHero(remoteSoldier());
}
}
我们也可以将以上连个例子合在一起写入一个配置类中,代码如下:
@Configuration
public class HeroConfig {
@Bean
public Enemy remoteSoldier() {
return new RemoteSoldier();
}
@Bean
@Profile("soloTop")
public Hero tankHero() {
return new TankHero(remoteSoldier());
}
@Bean
@Profile("mid")
public Hero apHero() {
return new APHero(remoteSoldier());
}
}
虽然两个Hero的bean都声明在了profile中,只有相应profile被激活时才会创建,但Enemy bean没有声明在profile中,所以不论何时它都将被创建。
(2). 在XML中配置profile
我们通过
<?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">
<bean id="artilleryCorps" class="chapterthree.enemy.ArtilleryCorps"/>
<beans profile="soloTop">
<bean id="tankHero" class="chapterthree.hero.TankHero">
<constructor-arg ref="artilleryCorps"/>
</bean>
</beans>
<beans profile="mid">
<bean id="apHero" class="chapterthree.hero.APHero">
<constructor-arg ref="artilleryCorps"/>
</bean>
</beans>
</beans>
当然我们也可以为每一种环境单独写一个XML配置文件。
2. 激活profile
Spring依靠两个独立的属性判断来哪个profile处于激活状态:
- spring.profiles.active
- spring.profiles.default
1.如果设置了spring.profiles.active,那么它的值就用来确定激活哪个profile。
2.如果没设置spring.profiles.active,spring就会找spring.profiles.default的值。
3.如果以上两个都没设置,那就没有激活的profile,只创建未在profile中定义的bean。
设置两个属性的方式有如下几种:
- 作为DispatcherServlet的初始化参数
- 作为Web应用上下文参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统属性
- 集成测试类上使用@ActiveProfiles注解
比如我们可以在web.xml中使用这种方式为上下文设置默认的profile
<context-param>
<param-name>spring.profile.default</param-name>
<param-value>soloTop</param-value>
</context-param>
二、条件化的bean
当我们想让某个bean在另一个特定的bean声明后才创建,或者某个环境变量设置后才创建某个bean,可以使用@Conditional注解来实现。
package chapterthree.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class EnemyExistCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("soloTop"); // 检查soloTop属性
}
}
创建一个Condition类的实现,matches()方法判断环境中是否存在soloTop属性。
@Configuration
public class HeroConfig {
@Bean
@Conditional(EnemyExistCondition.class)
public Hero tankHero() {
return new TankHero(remoteSoldier());
}
}
如果matches()返回true,则Spring就会创建这个bean,否则不会创建。
实际上,我们利用ConditionContext这个接口可以做很多事:
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
- 借助getRegistry()方法可以检查bean定义
- 借助getBeanFactory()方法可以检查bean是否存在,检查bean的属性等
- 借助getEnvironment()方法可以知道环境变量是否存在,以及值是什么
- 借助getResourceLoader()方法可以读取ResourceLoader加载的资源
- 借助getClassLoader()加载并检查类是否存在
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String annotationName);
Map<String, Object> getAnnotationAttributes(String annotationName);
Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString);
}
AnnotatedTypeMetadata接口可以让我们知道有@Bean注解的方法上还有什么其他注解。
我们再看一下@Profile注解是如何实现的:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
/**
* The set of profiles for which the annotated component should be registered.
*/
String[] value();
}
实际上,@Profile注解也使用了@Conditon注解,并使用ProfileCondition作为Condition的实现。
三、处理自动装配的歧义性
上一章已经感受到了自动装配的强大之处,它可以减少装配时需要的显式配置数量。可是,如果仅有一个bean匹配所需结果时自动装配可以生效,但如果不止一个bean匹配,就会出现歧义。
比如我们有三个带有@Component注解的Enemy实现:
@Component
public class MeleeCreeps implements Enemy {
public void dead() {
System.out.println("近战兵被击杀");
}
}
@Component
public class RemoteSoldier implements Enemy {
public void dead() {
System.out.println("远程兵被击杀");
}
}
@Component
public class ArtilleryCorps implements Enemy {
public void dead() {
System.out.println("炮车兵被击杀");
}
}
我们用@Autowired标注set方法:
public class ADCHero implements Hero {
private Enemy enemy;
@Autowired
public void setEnemy(Enemy enemy) {
this.enemy = enemy;
}
public void killEnemy() {
enemy.dead();
}
}
这时,在装配enemy时,并没有唯一值,会抛出异常。看一下Spring是如何解决这种问题的。
1. 标示首选的bean
@Component
@Primary
public class ArtilleryCorps implements Enemy {
public void dead() {
System.out.println("炮车兵被击杀");
}
}
@Primary注解表明在Spring遇到歧义性的时候,首要选择@Primary标注的bean进行装配。
显式装配的写法:
@Bean
@Primary
public class Enemy artilleryCorps{
return new ArtilleryCorps();
}
XML中的写法
<bean id="artilleryCorps" class="chapterthree.enemy.ArtilleryCorps" primary="true" />
使用@Primary这种方式,如果两个bean上都加了@Primary,那么又会产生歧义性问题。限定符是一种更强大的机制。
2. 限定自动装配的bean
@Primary的缺点在于无法将可选方案限定在唯一选项中,而限定符则可以。
public class ADCHero implements Hero {
private Enemy enemy;
@Autowired
@Qualifier("artilleryCorps")
public void setEnemy(Enemy enemy) {
this.enemy = enemy;
}
public void killEnemy() {
enemy.dead();
}
}
@Qualifier注解设置的参数为想要注入bean的ID,他与@Autowired或@Inject共同使用,指定注入时想要注入的bean的ID。
准确的说,@Qualifier("artilleryCorps")意思是要注入限定符为artilleryCorps的bean,限定符默认与beanID一致。这个例子中存在一个问题,如果ArtilleryCorps类重构了,重命名为其他名字的话,就无法匹配@Qualifier("artilleryCorps")中的限定符了。想要解决这个问题可以自定义限定符。
创建自定义限定符:
@Component
@Qualifier("artilleryCorps")
public class ArtilleryCorps implements Enemy {
public void dead() {
System.out.println("炮车兵被击杀");
}
}
artilleryCorps限定符分配给了ArtilleryCorps,所以即使重构ArtilleryCorps,限定名也不会改变。
显式配置的写法:
@Bean
@Qualifier("artilleryCorps")
public class Enemy artilleryCorps{
return new ArtilleryCorps();
}
如果有两个bean都标注了@Qualifier("artilleryCorps")也会产生歧义性,我们可以自定义注解的方式。
声明自定义限定符注解:
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface ArtilleryCorps {
}
然后我们就可以使用@ArtilleryCorps注解来替代@Qualifier("artilleryCorps")。
四、bean的作用域
默认情况下,Spring上下文中的bean都是单例的,无论一个bean被注入到其他bean多少次,注入的都是同一个实例。但有时候,使用的类是易变的,会保持一些状态,重用是不安全的。
Spring定义了多种作用域:
- 单例(Singleton):整个应用中只创建bean的一个实例。
- 原型(Prototype):每次注入或通过Spring上下文获取时都创建新的bean实例。
- 会话(Session):在Web应用中为每个会话创建一个bean实例。
- 请求(Rquest):在Web应用中为每个请求创建一个bean实例。
配置方式:
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Enemy remoteSoldier() {
return new RemoteSoldier();
}
或:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class RemoteSoldier implements Enemy {
public void dead() {
System.out.println("远程兵被击杀");
}
}
或:
<bean id="remoteSoldier" class="chapterthree.enemy.RemoteSoldier" scope="prototype" />
1. 使用会话和请求作用域
如果将一个bean代表网购平台中的购物车,那作用域选择哪一个比较合适呢。如果是单例的话,所有用户的商品都会添加到一个购物车里。如果是原型的,某一处添加的商品在另一处就没有了。所以选用会话作用域最合适。
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,proxyMode = ScopedProxyMode.INTERFACES)
public class ShoppingCart {
// 内容省略
}
WebApplicationContext.SCOPE_SESSION会让Spring为每个会话创建一个bean。
ScopedProxyMode.INTERFACES解决了如下问题:
@Component
public class StoreService {
@Autowired
private ShoppingCart shoppingCart;
// 内容省略
}
首先,StoreService是单例的,Spring应用上下文加载时,他就会被创建,同时Spring会试图注入ShoppingCart bean,而由于ShoppingCart bean是会话作用域,此时不存在,只有用户进入了系统创建会话时才创建。
其次,系统中有多个ShoppingCart bean,我们不想同一个ShoppingCart bean到StoreService中。
使用ScopedProxyMode.INTERFACES,Spring不会将实际的ShoppingCart bean注入进StoreService,而是注入ShoppingCart bean的代理并暴露ShoppingCart相同的方法。此时StoreService认为注入的代理就是一个ShoppingCart bean,当它调用ShoppingCart bean的方法时,代理就会解析并调用真正的ShoppingCart bean。
2. 在XML中声明作用域代理
<bean id="shoppingCart" class="chapterthree.shop.ShoppingCart" scope="session" >
<aop:scoped-proxy />
</bean>
五、运行时注入
有时候我们希望避免硬编码,让某些值再运行时再确定,Spring提供了两种在运行时求值的方式:
- 属性占位符
- Spring表达式语言(SpEL)
1. 注入外部的值
(1). 通过Spring的Environment的方式。
public class ADCHero implements Hero {
private String heroName;
private String enemyHeroName;
public ADCHero(String heroName, String enemyHeroName) {
this.heroName = heroName;
this.enemyHeroName = enemyHeroName;
}
public void killEnemy() {
System.out.println("我方英雄" + heroName + "击杀了敌方英雄" + enemyHeroName);
}
}
@Configuration
@PropertySource("classpath:chapterthree/hero/hero.properties") // 声明属性源
public class HeroConfig {
@Autowired
Environment environment;
@Bean
public Hero adcHero() {
return new ADCHero(environment.getProperty("ADCHero.heroName"), environment.getProperty("ADCHero.enemyHeroName")); // 检索属性值
}
}
hero.properties文件内容
ADCHero.heroName=EZ
ADCHero.enemyHeroName=Kaisa
(2). 使用占位符方式在XML中进行注入:
<!-- 解析占位符,引入property文件 -->
<context:property-placeholder location="chapterthree/hero/hero.properties"/>
<bean id="adcHero" class="chapterthree.hero.ADCHero" c:heroName="${ADCHero.heroName}" c:enemyHeroName="${ADCHero.enemyHeroName}"/>
使用组件扫描和自动装配时,使用占位符的方式如下:
@Component
public class ADCHero implements Hero {
private String heroName;
private String enemyHeroName;
public ADCHero(@Value("${ADCHero.heroName}") String heroName, @Value("${ADCHero.enemyHeroName}") String enemyHeroName) {
this.heroName = heroName;
this.enemyHeroName = enemyHeroName;
}
public void killEnemy() {
System.out.println("我方英雄" + heroName + "击杀了敌方英雄" + enemyHeroName);
}
}
同时我们要创建一个bean来解析占位符
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
解析外部属性能够将值得处理推迟到运行时。
1. 使用Spring表达式装配
使用SpEL有很多好处:
- 使用beanID来引用bean
- 调用方法和访问对象的属性
- 对值进行算数、关系和逻辑运算
- 正则表达式匹配
- 集合操作
与属性占位符不同,SpEL使用 #{ ... } 的方式。
如之前的例子使用SpEL将变成这样:
public ADCHero(@Value("#{ADCHero.heroName}") String heroName, @Value("#{ADCHero.enemyHeroName}") String enemyHeroName) {
this.heroName = heroName;
this.enemyHeroName = enemyHeroName;
}
<!-- 解析占位符,引入property文件 -->
<context:property-placeholder location="chapterthree/hero/hero.properties"/>
<bean id="adcHero" class="chapterthree.hero.ADCHero" c:heroName="#{ADCHero.heroName}" c:enemyHeroName="#{ADCHero.enemyHeroName}"/>
总结一下SpEL所支持的表达式:
表示字面值:
例如:
浮点类型:
#{3.1415926}
字符串类型:
#{'Hero'}
布尔类型:
#{true}
引用bean、bean属性、方法:
将bean装配到另一个bean的属性中,使用beanID,例如:
#{adcHero}
引用bean属性:
#{adcHero.heroName}
引用bean方法:
#{adcHero.killEnemy()}
在表达式中使用类型:
T(java.lang.Math).random()
SpEL运算符:
#{2 * T(java.lang.Math).PI * circle.radius}
计算正则表达式:
#{adcHero.heroName matches '[a-zA-Z0-9_]'}
计算集合:
#{assistHero.equipList[1]}