Spring in Action——高级装配

bean装配所设计的领域并不仅仅局限于上一章学习到的内容。Spring提供了很多技巧,实现更高级的bean装配功能。

环境与profile

在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另一个环境。开发阶段中,某些环境相关的做法可能并不适合迁移到生产环境中。数据库配置,加密算法以及外部系统的集成是跨环境部署时会发生变化的几个例子。
比如考虑一下数据库配置。在开发环境中,我们可能使用嵌入式数据库,并预先加载测试数据。例如,在Spring配置类中,我们可能会在一个带有@Bean注解的方法上使用EmbeddedDatabaseBuilder:

@Bean(destroyMethod="shutdown")
  public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .addScript("classpath:schema.sql")
        .addScript("classpath:test-data.sql")
        .build();
  }

这样会创建一个类型为javax.sql.DataSource的bean,这个bean是如何创建出来的才是最有意思的。使用EmbeddedDatabaseBuilder会搭建一个嵌入式的Hypersonic数据库,它的模式(schema)定义在schema.sql中,测试数据则是通过test-data.sql加载的。
当你在开发环境中运行集成测试或者启动应用进行手动测试的时候,这个DataSource是很有用的。每次启动它的时候,都能让数据库处于一个给定的状态。
但是对于生产环境中,你可能希望使用JNDI在容器中获取一个DataSource。在这种场景中使用如下的@Bean方法会更加合适:

  @Bean
  public DataSource dataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    jndiObjectFactoryBean.setJndiName("jdbc/myDS");
    jndiObjectFactoryBean.setResourceRef(true);
    jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
    return (DataSource) jndiObjectFactoryBean.getObject();
  }

显然这里展示的两个版本的dataSource()不相同,虽然都会生成javax.sql.DataSource的bean,但是它们的相似点也仅限于此了。
我们必须要有一种方法来配置它,使得它在各种情况(不止两种)下,都是合适的。
配置profile bean
在Java配置中,可以使用@Profile注解指定某个bean属于哪个profile。

既可以在类级别上,也可以在方法级别上使用。

下面是方法级别上的使用:

package com.myapp;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;

@Configuration
public class DataSourceConfig {
  
  @Bean(destroyMethod = "shutdown")
  @Profile("dev")
  public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.H2)
        .addScript("classpath:schema.sql")
        .addScript("classpath:test-data.sql")
        .build();
  }

  @Bean
  @Profile("prod")
  public DataSource jndiDataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    jndiObjectFactoryBean.setJndiName("jdbc/myDS");
    jndiObjectFactoryBean.setResourceRef(true);
    jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
    return (DataSource) jndiObjectFactoryBean.getObject();
  }
}

另外在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" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="
    http://www.springframework.org/schema/jee
    http://www.springframework.org/schema/jee/spring-jee.xsd
    http://www.springframework.org/schema/jdbc
    http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

  <beans profile="dev">
    <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">
    <jee:jndi-lookup id="dataSource"
      lazy-init="true"
      jndi-name="jdbc/myDatabase"
      resource-ref="true"
      proxy-interface="javax.sql.DataSource" />
  </beans>
</beans>

激活profile
Spring确定哪个profile处于激活状态时,需要依赖两个独立的属性:

  • spring.profiles.active
  • spring.profiles.default

如果设置了spring.profiles.active的话,它的值就会用来确定哪个profile是激活的。如果没有设置active,那么Spring将会查找default的值。如果均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在profile中的bean。
有多种方法来设置这两个属性:

  • 作为DispatcherServlet的初始化参数
  • 作为Web应用的上下文参数
  • 作为JNDI条目
  • 作为环境变量
  • 作为JVM的系统属性
  • 在集成测试类上,使用@ActiveProfiles注解设置。

我所喜欢的一种方式是使用DispatcherServlet的参数将spring.profiles.default设置为开发环境的profile,我会在Servlet上下文中设置,例如在Web应用中,web.xml文件如下所示:
略。P75

条件化的bean

假设你希望一个或多个bean只有在应用的类路径下包含特定的库时才创建,或者某个bean只有当另外一个特定的bean也声明后才创建。我们还可能要求只有某个特定的环境变量设置后,才会创建某个bean。
Spring4引入了@Conditional注解,它可以用到带有@Bean注解的方法上,如果给定条件为true则会创建这个bean,否则被忽略。

  @Bean
  @Conditional(MagicExistsCondition.class)
  public MagicBean magicBean() {
    return new MagicBean();
  }

可以看到,如果MagicExistsCondition声明了,这个bean才创建。

@Conditional将会通过Condition接口进行条件对比。

package org.springframework.context.annotation;

import org.springframework.core.type.AnnotatedTypeMetadata;

public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

设置给@Conditional的类可以是任意实现了Condition接口的类型。可以看出来,只需要提供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。
我们这里实现这个接口:

package com.habuma.restfun;

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 MagicExistsCondition implements Condition {

  @Override
  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    Environment env = context.getEnvironment();
    return env.containsProperty("magic");//检查是不是有magic属性
  }
}

ConditionContext得到了Environment,实际上它是一个接口,可以做到如下几点:

  • getRegistry()返回BeanDefinitionRegistry检查bean定义。
  • getBeanFactory()返回ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性。
  • getEnvironment()返回Environment检查环境变量是否存在以及它的值是什么。
  • 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源。
  • 借助getClassLoader()返回的ClassLoader加载并检查类是否存在。

AnnotatedTypeMetadata则能让我检查@Bean注解的方法上还有什么其他的注解:借助isAnnotated()能够判断。

处理自动装配的歧义性

因为在自动装配中,如果不仅有一个bean能够匹配结果的话,这种歧义会出错。
比如:

  @Autowired
  public CDPlayer(CompactDisc cd) {
    this.cd = cd;
  }

以及:

@Component
public class SgtPeppers implements CompactDisc {
	...
}

@Component
public class BlankDisc implements CompactDisc {
	...
}

因为都实现了@Component注解,所以,Spring抛出NoUniqueBeanDefinitionException.
所以当歧义发生时,Spring提供了首选primary和限定quelifier来帮助Spring将可选的bean的范围缩小为只有一个bean。

标识首选的bean

@Component
@Primary
public class BlankDisc implements CompactDisc {
	...
}

或者

@Configuration
public class CDConfig {
    @Bean
    @Primary
    public CompactDisc compactDisc() {
        return new SgtPeppers();
    }
}

或者如果使用XML配置:

<bean id="iceCream" class="..IceCream" primary="true"/>

但是如果标识了两个PRIMARY,那就无法正常工作了。

限定自动装配的bean
@Qualifier注解是使用限定符的主要方式。它与@Autowired@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。例如:

@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
	this.dessert=dessert;
}

@Qualifier注解所设置的参数就是要注入的bean的ID。

创建自定义的限定符

@Component
@Qualifier("blankDisc")
public class BlankDisc implements CompactDisc {
	...
}

在这种情况下,在注入的地方,只要引用blankDisc就行:

	@Autowired
    @Qualifier(value = "blankDisc")
    public void setCompactDisc(CompactDisc compactDisc) {
        this.compactDisc= compactDisc;
    }

当通过显式配置bean的时候,也可以与@Bean一起使用:

@Configuration
public class SoundSystemConfig {
    @Bean
    @Qualifier("blankDisc")
    public CompactDisc compactDisc(){
        return new BlankDisc();
    }
}

使用自定义的限定符注解

创建自定义限定符注解:

package soundsystem;

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.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Blank {
}

怎么使用呢?

@Component
@Blank
public class BlankDisc implements CompactDisc {
	...
}

所以,假如我们有几个CompactDisc都符合Blank的标签,我们还可以创建一个Light(轻音乐)来区分。

@Component
@Blank
@HipHop
public class BlankDisc implements CompactDisc {
	...
}
@Component
@Blank
@Light
public class BlankDJsDisc implements CompactDisc {
	...
}

bean的作用域

@Scope来设置singleton或者prototype等等作用域的bean。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class BlankDisc implements CompactDisc {...}

或者在JavaConfig里使用:

	@Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public CompactDisc compactDisc(){
        return new BlankDisc();
    }

或者使用XML来配置bean:

<bean id="notepad" class="..NotePad" scope="prototype"/>

还有session和request等等,暂略。

在XML中声明作用域代理
暂略。

运行时注入

我们在BlankDisc的构造器中,构造器含参数的情况,我们在JavaConfig里通常会这么做:

@Bean
public CompactDisc sgtPeppers(){
	return new BlankDisc("title is xxx","author A");}

或者使用XML这样做:

<bean id="sgtPeppers" class="..BlankDisc" 
c:_title="title is xxx" 
c:_artist="author A"/>

这样的硬编码虽然可以,但是我们可能会希望避免硬编码值,而是想要这些值在运行时才确定。为了实现这种功能,Spring提供了两种方式:

  • 属性占位符
  • Spring表达式语言(SpEL)

注入外部的值
在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。

@Configuration
@PropertySource("classpath:/soundsystem/app.properties")
public class ExpressiveConfig {
    @Autowired
    Environment environment;
    @Bean
    public CompactDisc disc(){
        return new BlankDisc(environment.getProperty("disc.title"),
                environment.getProperty("disc.artist"));
    }
}

以及app.properties文件:

disc.title=This is title
disc.artist=Author A

这里的getProperty()方法,有四种重载的形式:

  • String getProperty(String key)
  • String getProperty(String key, String defaultValue)
  • T getProperty(String key, Class<T> type)
  • T getProperty(String key, Class<T> type,T defaultValue)

很容易理解,假如我们从属性文件想要的到的是数字,需要这样写:
int connectionCount= env.getProperty("db.connection.count", Integer.class, 30)
如果想检查getProperty里的属性是不是没有定义:
boolean exists=env.containsProperty("disc.title")
getRequiredProperty()如果属性没有定义,则会抛出异常,比起getProperty()返回Null更加安全。

当然,有时候我们不希望直接先写死赋值,这就需要解析属性占位符
占位符的形式使用:${...}
在XML中,按照如下方式解析BlankDisc的构造器参数:

<bean id="sgtPeppers" class="soundsystem.BlankDisc" 
	c:_title="${disc.title}"
	c:_artist="${disc.artist}">

而如果我们使用组件扫描和自动装配来创建和初始化应用组件,我们使用@Value注解:

    public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}") String artist) {
        this.title = title;
        this.artist = artist;
    }

为了使用占位符,我们必须要配置一个PropertyPlaceholderConfigurer bean或者PropertySourcesPlaceholderConfigurer bean。推荐后面一种,因为它能基于Spring Enviroment及其属性源来解析占位符。

@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
	return new PropertySourcesPlaceholderConfigurer();
}

使用SpEL进行装配
SpEL它可以以强大简洁的方式装配到bean属性和构造器参数中,它有很多特性:

  • 使用bean的ID来引用bean
  • 调用方法和访问对象的属性
  • 对值进行算数、关系和逻辑运算
  • 正则表达式匹配
  • 集合操作

需要了解的第一件事就是SpEL表达式要放到#{...}中。
这个属性占位符的${...}相似。
比如
#{1},表示的就是1。
#{T(System).currentTimeMillis()}它的最终结果就是计算表达式的那一刻当前时间的毫秒数。T()表达式将java.lang.System视为Java中对应的类型,因此可以调用static修饰的currentTimeMills()方法。
#{sgtPeppers.artist}这个表达式会得到ID为sgtPeppers的bean的artist属性。
#{systemProperties{'disc.title'}}通过systemProperties对象引用系统属性。
所以上面的内容,我们使用SpEL:

    public BlankDisc(@Value("#{systemProperties{'disc.title'}}") String title, @Value("#{systemProperties{'disc.artist'}}") String artist) {
        this.title = title;
        this.artist = artist;
    }

或者

<bean id="sgtPeppers" class="soundsystem.BlankDisc" 
	c:_title="#{systemProperties{'disc.title'}}"
	c:_artist="#{systemProperties{'disc.artist'}}">

当然SpEL还可以引用bean、属性和方法
比如:
之前的#{sgtPeppers.artist}引用属性除外,
还有,
#{sgtPeppers}引用bean
#{artistSelector.selectArtist()}引用方法
#{artistSelector.selectArtist().toUpperCase()}引用方法
#{artistSelector.selectArtist()?.toUpperCase()}当然我们要判断selectArtist是否返回为空,我们使用?.运算符,用于确保非空。

SpEL 在表达式中使用类型
如果要在SpEL中访问类作用域的方法和常量的话,要依赖T()这个关键的运算符。例如:
#{T(java.lang.Math)}
#{T(java.lang.Math).PI}使用pi的值
#{T(java.lang.Math).random()}获得随机数
#{T(java.lang.Math).random()*circle.radius},要使用运算符:乘法

SpEL应该写的尽量简洁,因为它可能测试起来很困难,以上介绍的SpEL只是冰山一角。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值