Spring实战第三章:高级装配

一、环境与profile

问题:我们必须要有一种方法来配置DataSource,使其在每种环境下都会选择最为合适的配置。

方案1:在单独的配置类(或XML文件)中配置每个bean,然后在构建阶段(可能会使用Maven的profiles)确定要将哪一个配置编译到可部署的应用中。这种方式的问题在于要为每种环境重新构建应用。

方案2:spring在运行时才去根据环境决定创建哪个bean和不创建哪个bean,可以保证同一个部署单元能够适用于所有的环境,没有必要进行重新构建。

实现方式:

1、配置profile

(1)java方式

使用@Profile注解指定某个bean属于哪一个profile;

spring 3.1:@Profile只能定义在类级别;

Spring3.2:@Profile可以定义在方法级别;

没有指定@Profile的bean都会被创建,与激活哪个profile无关;

@Configuration
@Profile("dev") // 告诉此类中bean只有在dev profile激活时才去创建,没有没有激活,带有@Bean注解的的方法都会被忽略掉
public class DevelopmentProfileConfig {
  
  @Bean(destroyMethod = "shutdown")
  public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.H2)
        .addScript("classpath:schema.sql")
        .addScript("classpath:test-data.sql")
        .build();
  }
}
@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();
  }
}

(2)xml配置

所有配置文件都会放到部署单元(war文件),但只有profile属性与当前激活profile相匹配的配置文件才会被用到;

<?xml version="1.0" encoding="UTF-8"?>
<beans .....
    profile="dev">

    <jdbc:embedded-database id="dataSource"
      <jdbc:script location="classpath:schema.sql" />
      <jdbc:script location="classpath:test-data.sql" />
    </jdbc:embedded-database>
</beans>

可以为<beans>标签嵌套定义<beans>元素,即可以将所有profile bean定义放在一个文件

<beans .....">

  <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>

2、激活profile

(1)按属性顺序判断是否激活profile:spring.profiles.active  -> spring.profiles.default 

(2)激活方式:

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

如在web.xml中配置spring.profiles.default属性,开发人员可以通过spring.profiles.active配置不同环境的

Spring提供了@ActiveProfiles注解,我们可以使用它来指定运行测试时要激活哪个profile

  @RunWith(SpringJUnit4ClassRunner.class)
  @ContextConfiguration(classes=DataSourceConfig.class)
  @ActiveProfiles("dev")
  public static class DevDataSourceTest {
    ...
  }

二、条件化的bean

根据不同条件来判断是否创建:只有某个特定bean也声明了之后才会创建、只有某个特定环境变量设置后才会创建等。。。

Spring 4之后,可以使用@Conditional注解来标记条件化bean,它可以作用到带有@Bean注解的方法上,根据@Conditional(条件)中条件的计算结果来判断是否创建这个bean。

@Bean
// 只有满足MagicExistCondition中的条件才会创建MagicBean
@Conditional(MagicExistCondition.class)
public MagicBean magicBean(){
  return new MagicBean();
}
// 需要实现Condition接口
public class MagicExistCondition implements Condition{

  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata){
    // 这里添加代码判断是否match
    // 返回true则创建bean、false不创建
  }
}

三、处理自动装配的歧义性

装配bean时匹配的bean存在多个,此时的解决方法有:
1、标示首选的bean-@primary标记首选

@Component
@Primary // 可与@Component、@Bean配合使用
public class Iceream implements Dessert { ... }

@Bean
@Primary
public Dessert iceCream() {
  return new IceCream();
}

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

如果存在多个首选的bean,则又会存在歧义了,此时需要使用限定符了;

2、限定自动装配的bean-@Qualifier

@Qualifier可以与@Autowried、@Inject注解配合使用;

@Autowired
@Qualifier("iceCream")// bean的id
// 当iceCream代表的bean进行了重构,id变了就会出问题
public void setDessert(Dessert dessert) {
    this.dessert = dessert;
}

为了解决上面对id限定符强依赖问题,以下通过自定义限定符解决;

@Component // 与@Component配合使用
@Qualifier("cold")
public class IceCream implements Dessert { ... }


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

@Bean // 与@Qualifier配合使用
@Qualifier("cold")
public Dessert iceCream() {
    return new IceCream();
}

如果针对多个相同限定符的bean,则歧义问题又会出现,可以使用自定义限定符注解

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold { }

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy { }


@Component
@Cold
@Creamy
public class IceCream implements Dessert() { ... }

@Component
@Cold
@Fruity
public class Popsicle implements Dessert() { ... }

// 最终使用
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert) {
    this.dessert = dessert;
}

四、bean的作用域

1、Spring定义了多种作用域,可以基于这些作用域创建bean:

  • 单例(Singleton):在整个应用中,只创建bean的一个实例(默认)。
  • 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
  • 会话(Session):在Web应用中,为每个会话创建一个bean实例。
  • 请求(Rquest):在Web应用中,为每个请求创建一个bean实例。

选择除单例的其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 原型bean
// 或 @Scope("prototype")
public class NotePad { ... }


@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTYPE)
public Notepad notepad() {
    return new Notepad();
}


<bean id="notepad" class="com.myapp.Notepad"
    scope="prototype" />

2、使用会话和请求作用域:P87

@Bean
@Scope(
    value=WebApplicationContext.SCOPE_SESSION,
    proxyMode=ScopedProxyMode.INTERFACES) 
   // 表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean
public ShoppingCart cart() { ... }


@Component
public class StoreService {
    @Autowired
    public void setShoppingCart(ShoppingCart shoppingCart) {
        this.shoppingCart = shoppingCart;
    }
    ...
}

ScopedProxyMode.TARGET_CLASS:表明要以生成目标类扩展的方式创建代理。

3、xml中声明作用域代理

<!-- 
scope :设置bean作用域
aop:scope-proxy:设置作用域代理,与@Scope注解的proxyMode属性功能相同
默认情况下,会使用CGLib创建目标类的代理。但是也可以将proxy-target-class属性设置为false,进而要求它生成基于接口的代理
-->
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
     <aop:scope-proxy />
</bean>


<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
     <aop:scope-proxy proxy-target-class="false" />
</bean>

五、运行时注入

避免将属性值等字面量信息写死在代码中或xml中,可以使用spring提供的2种运行时求值方式:

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

1、注入外部值

最简单方式是声明属性源并通过Spring的Environment来检索属性。

@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
// 属性文件会被加载到Environment中
public class EnvironmentConfig {

  @Autowired
  Environment env;
  
  @Bean
  public BlankDisc blankDisc() {
    return new BlankDisc(
        env.getProperty("disc.title"), // 在app.properties中声明
        env.getProperty("disc.artist"));
  }
}

2、属性占位符

(1)配置:

配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean。从Spring 3.1开始,推荐使用PropertySourcesPlaceholderConfigurer,因为它能够基于Spring Environment及其属性源来解析占位符。

a、java方式

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

b、xml方式

<?xml version="1.0" encoding="UTF-8"?>
<beans .....">

  <context:property-placeholder />
  <!-- 可以生成PropertySourcesPlaceholderConfigurer bean-->
</beans>

(2)使用:

xml中使用${disc.title} 形式,disc.title具体值定义在外部文件中;

java代码中使用@value注解,如@Value("${disc.title}") String title

 

3、Spring表达式语言

(1)SpEL拥有很多特性,包括:

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

(2)SpEL表达式要放到#{...}之中,属性占位符是放在${...}

(3)更多见课本 P96

1、#{1}: 结果为1
2、#{T(System).currentTimeMillis()} :那一刻当前时间的毫秒数
   T()表达式会将java.lang.System视为Java中对应的类型,因此可以调用其static修饰的
   currentTimeMillis()方法。
3、#{sgtPeppers.artist}: 计算得到ID为sgtPeppers的bean的artist属性
4、#{systemProperties['disc.title']}:通过systemProperties对象引用系统属性
5、#{3.14159}:浮点值
6、#{9.87E4}:科学计数法
7、#{'Hello'}:字符串
8、#{true}:Boolean类型
9、#{sgtPeppers}:引用bean
10、#{artistSelector.selectArtist()}:引用bean的方法
11、#{artistSelector.selectArtist().toUpperCase()}:
12、#{artistSelector.selectArtist()?.toUpperCase()}:
如果artistSelector.selectArtist()返回为null,不会调toUpperCase(),表达式结果也为null

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值