Spring学习笔记-第三章-高级装配

第三章:高级装配

本章内容:

  • Spring profile
  • 条件化的bean声明
  • 自动装配与歧义性
  • bean的作用域
  • Spring表达式语言

3.1 环境与profile

在软件开发的不同阶段需要不同的环境和配置。

@Bean(destroyMethod = "shutdown")
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .addScript("classpath:ch3.sql")
        .addScript("classpath:ch3.1.sql")
        .build();
}

为了适应环境更换的需求,可以将所需要的所有的配置类配置到每个bean中,然后在构建阶段选择需要使用的bean,但是从开发环境切换到生产环境时可能会发生问题。

3.1.1 配置profile bean

Spring为此种场景提供了profile功能。

使用profile注解来声明在合适的阶段使用合适的bean。将所有的bean整理到一个profile中,确保在需要的时候active相应的bean。

package com.ch3;

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;

import javax.sql.DataSource;

/**
 * @author [email protected]
 * @since 2018/11/24 下午1:00
 */
@Configuration
public class DataSourceConfig {
    @Bean(destroyMethod = "shutdown")
    @Profile("dev")
    public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:test.sql")
                .addScript("classpath:test1.sql")
                .build();
    }

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

虽然所有的bean都被声明在一个profile里,但是只有当指定的profile被激活时,相应的bean才会被创建,没有指定profile的bean始终都会被创建,与激活的profile没有关系。

在XML中配置profile:

可以通过beans元素的profile属性,在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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"
        profile="dev">

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

只有profile属性与当前激活的profile相匹配的配置文件才会被用到。

重复使用beans属性指定多个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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">

    <beans profile="dev">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:test.sql"/>
            <jdbc:script location="classpath:test1.sql"/>
        </jdbc:embedded-database>
    </beans>
    
    <beans profile="prod">
        <jee:jndi-lookup jndi-name="jdbc/MyDatabase" id="dataSource" resource-ref="true" proxy-interface="javax.sql.DataSource"/>
    </beans>
</beans>

虽然id都一样,类型都是javax.sql.dataSource,但是只会创建指定profile的bean。

3.1.2 激活profile

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

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

优先级从上到下,如果spring.profiles.active没有设置,则看spring.profiles.default,否则只会创建没有定义在profiles中的bean。

有多种方式设置这两个属性:

  • 作为DispatcherServlet的初始化参数
  • 作为web应用的上下文参数
  • 作为JNDI条目
  • 作为环境变量
  • 作为JVM属性
  • 在集成测试类上使用@ActiveProfiles属性

在web.xml配置文件中设置默认的profile:

<context-param>
    <param-name>spring.profiles.default</param-name>
    <param-value>dev</param-value>
</context-param>

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

可以同时激活多个profile,以逗号分隔。

使用profile进行测试:

Spring提供了@ActiveProfiles注解,用来指定测试时使用的profile。

@RunWith(SpringJunit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest {
    
}

3.2 条件化的bean

需求:

  1. 希望一个或多个bean只有在类路径下包含某个特定的库时才创建
  2. 希望某个bean在特定的bean声明之后再创建

Spring 4引入了@Conditional注解,只有条件计算结果为true才会创建bean,否则不创建。

package com.ch3;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * @author [email protected]
 * @since 2018/11/24 下午2:07
 */
public class MagicExistsCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Environment environment = conditionContext.getEnvironment();
        return environment.containsProperty("magic");
    }
}
@Bean
@Conditional(MagicExistsCondition.class)   //条件化创建bean
public MagicBean magicBean() {
    return new MagicBean();
}

ConditionContext接口:

public interface ConditionContext {
    BeanDefinitionRegistry getRegistry();

    ConfigurableListableBeanFactory getBeanFactory();

    Environment getEnvironment();

    ResourceLoader getResourceLoader();

    ClassLoader getClassLoader();
}
  • getRegistry:根据返回值可以检查bean定义
  • getEnvirnment:检查环境变量
  • getResourceLoader:读取加载的资源
  • getClassLoader:加载并检查类是否存在

AnnotatedTypeMetadata接口:

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);
}

3.3 处理启动装配的歧义性

仅有一个bean匹配所需结果时,自动装配才是有效的,如果有多个bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器参数和方法参数。

Spring提供的解决方案:

  • 将可选bean中的其中一个声明为首选(primary)
  • 使用限定符(qualifier)缩小可选范围

3.3.1 标示首选的bean

将其中一个可选的bean声明为首选可以避免自动装配的歧义性。

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

@Component
@Primary
public class IceCream implements Dessert {
    //...
}

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

xml配置:

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

3.3.2 限定自动装配的bean

设置首选bean的局限性在于 @Primary无法将可选方案范围限定到一个无歧义性的选项中 ,当首选bean的数量超过一个时,无法进一步缩小限定范围。

@Qualifier注解是使用限定符的主要方式,与@Autowired协同使用,在注入时指定要注入的bean。

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

@Qualifier注解的参数就是想要注入的bean的id,所有使用@Component注解的类都会创建为bean,且id为首字母小写的类名。

基于默认id作为限定符是简单的,但是当类名被更改之后会使限定符失效。

创建自定义的限定符:

可以设置自己的限定符,而不依赖于bean id作为限定符。

@Component
@Qualifier("cold")
public class IceCream implements Dessert {
    
}

此时cold限定符分配给了IceCream bean,只需要在合适的地方引入cold限定符即可自动装配。

@Bean
@Qualifier("cold")
public Dessert iceCream() {
    return new IceCream();
}

此时类限定名的变更不会影响到自动装配。但是当应用中出现同名的注解@Qualifier(“cold”)时,歧义性又会再次出现。

这时需要多个@Qualifier注解来进一步缩小限定范围。


3.4 bean的作用域

默认情况下,Spring应用上下文中的所有bean都是以单例模式创建的。不管给定的bean被注入到其他bean多少次,每次注入的都是同一个实例。

如果一个类是可变(mutable)的,那么对其进行重用时可能会遇到意想不到的问题。

Spring定义的bean作用域:

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

@Scope注解:

用来指定bean的作用域:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
//或者 @Scope("prototype")
public class Notepad {
    //something
}

XML配置:

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

3.5 运行时值注入

Spring提供了两种运行时求值的方式:

  • 属性占位符(Property placeholder);
  • Spring表达式语言(S片EL)。
@Configuration
@PropertySource("classpath:/com/soundsys/app.properties")
public class ExpressiveConfig {
    @Autowired
    Environment env;
    
    @Bean
    public BlankDisc disc() {
        return new BlankDisc(env.getProperty("disc.title"), env.getProperty("disc.artist"));
    }
}

Spring的Environment:

getProperty()方法的四种重载方式:

  • String getProperty(String key);
  • String getProperty(String key, String defaultValue);
  • T getProperty(String key, Class type);
  • T getProperty(String key, Class type, T defaultValue);

使用重载形式的getProperty()方法可以避免类型转换:

int connectionCount = env.getProperty("db.connection.count", Integer.class, 10);

Environment常见方法:

  • boolean containsProperty(String property);
  • String[] getActiveProfiles();
  • String[] getDefaultProfiles();
  • boolean acceptsProfiles(String… profiles)。

解析属性占位符:

Spring支持将属性定义到外部的属性文件中,并使用占位符将其值插入到Spring bean中。在Spring装配中,占位符的形式为使用 “${…}” 的形式包装的属性名称。

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

使用组件扫描和自动装配时:

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

SpEL表达式语言:

将表达式语言放到 “#{…}” 之中。

  • “#{1 + 1}”
  • “#{T(System).currentMillis()}”
  • “#{sgtPeppers.artist}”
  • “#{false}”
  • “#{artistSelector.selectArtists().toUpperCase()}”

SpEL运算符:

运算符类型 运算符
算术运算符 +、-、*、/、%、
比较运算符 <、>、==、<=、>=、lt、gt、eq、le、ge
逻辑运算符 and、or、not、|
条件运算符 ?:(ternary)、?:()
正则表达式 matches

计算正则表达式:

#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}

计算集合:

#{jukebox.songs[4].title}
#{jukebox.songs[T(java.lang.Math).random()*jukebox.songs.size()].title}
#{jukebox.songs.?[artist eq 'Aerosmith']}  //.?[]得到集合的一个子集
#{jukebox.songs.^[artist eq 'Areosmith']}  //.^[]查询集合中的第一个匹配项
#{jukebox.songs.$[artist eq 'Areosmith'].![title]}  //.$[]查询集合中的最后一个匹配项,.![]从集合的每个成员中选择特定的属性放到另外一个集合中