《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

我们通过元素的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]}
posted @ 2020-08-23 17:31  当代艺术家  阅读(168)  评论(0)    收藏  举报