高级装配

环境与profile

在开发软件的时候,将应用程序从开发环境,迁移到测试环境,或者是迁移到生产环境都是一项挑战。最常见的就是对于DataSource的配置,这三种环境我们会根据不同的策略来生成DataSource bean。我们需要有一种方式来配置DataSource,使其在每种环境下都会选择对应的配置。

配置profile bean

要使用profile,首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署到不同环境中,要确保对应的profile文件处于激活状态。

在Java配置中,使用@Profile注解指定某个bean属于哪一个profile。

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;

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

}

在这里@profile注解是应用在类级别了。它告诉Spring这个配置类中的bean只有在dev profile 激活时在会创建。如果dev profile没有激活的话,那么这个配置类下的bean会被忽略。

如果我们为每一个配置环境都去配置一个配置类,那无疑会增加配置类的个数,不利于维护。从Spring3.2开始,就可以在方法级别上使用@profile注解,与@Bean注解一同使用。这样的话,我们就可以将多个bean放在一个配置类中了。

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:scheam.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();
    }

}

只有在对应的profile激活时,相应的bean才会被创建,但是可能会有其他的bean并没有声明在一个给定的profile范围内。没有指定profile的bean始终会被创建,与激活哪个profile无关。

在XML配置中,我们可以使用<beans>元素的profile属性,在XML中配置profile bean。

在这里使用beans元素中嵌套定义beans元素的方式,而不是为每个环境都创建一个profile XML文件,这样将所有的profile bean定义在同一个XML文件中

profile.xml:

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

    <beans profile="dev">
        <jdbc:embedded-database id="datasource">
            <jdbc:script location="classpath:scheam.sql"/>
            <jdbc:script location="classpath:test-data.sql"/>
        </jdbc:embedded-database>
    </beans>
    
    <beans profile="qa">
        <bean id="datasource" class="org.apach.commons.dbcp.BesicDataSource" 
        destroy-method="close"
        p:url="jdbc:h2:tcp://dbserver/~/test"
        p:driverClassName="org.h2.Driver"
        p:username="sa"
        p:password="password"
        p:initialSize="20"
        p:maxActive="30"
        />
    </beans>
    
    <beans profile="prod">
        <jee:jndi-lookup id="datasource" jndi-name="jdbc/myDatabase"
        resource-ref="true"
        proxy-interface="javax.sql.DataSource"
        ></jee:jndi-lookup>
    </beans>

</beans>

激活profile

Spring在确定哪个profile处于激活状态时,需要依赖两个属性:spring.profiles.active和spring.profiles.default。如果设置了spring.profiles.active就会根据它的值确定哪个profile是激活的。但如果没有设置spring.profiles.active属性的话,Spring就会去找spring.profiles.default的值.如果这两个属性都没有设置,那就没有激活的profile。

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

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

我在这里使用前两种方式,前两种方式可以直接在web.xml中设置:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    version="2.5">

    <servlet>
        <servlet-name>springDispatcherServlet</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>
    <servlet-mapping>
        <servlet-name>springDispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/applicationContext.xml</param-value>
    </context-param>
    
    <context-param>
        <param-name>spring.profiles.defaule</param-name>
        <param-value>dev</param-value>
    </context-param>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>

当应用部署到其他环境时,我们根据情况再来设置spring.profiles.active即可.spring.profiles.active的设置优先使用。可以注意到spring.profiles.active和spring.profiles.default的profiles是复数形式,可以设置多个profile名称,并以逗号分隔,我们可以设置多个彼此不相关的profile。

使用profile进行测试

在运行集成测试的时候,Spring提供了@ActiveProfiles注解,我们可以死哦那个它来制定运行测试要激活哪个profile.

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

条件化创建bean

Spring的profile机制是根据哪个profile处于激活状态来条件化初始bean的一种体现。Spring4中可以使用一种更为通用的机制来实现条件化bean的定义,在这种机制下,条件完全由自己决定。Spring4使用的是@Conditional注解定义条件化bean。

@Conditional注解可以用在@Bean注解的方法上。如果给定条件为true,就会创建这个bean,否则的话,这个bean会被忽略。

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

@conditional注解给定了一个Class,它指明了条件——在这里是MagicCondition(实现了Condition接口)。给定@conditional注解的类可以是任意实现了Condition接口的类型。

public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

这个接口比较简单,只有一个matches()方法作为实现,如果该方法返回true就会创建提供了@Conditional注解的bean,否则就不会创建.

 matches()方法会得到ConditionContext和AnnotatedTypeMetadata两个入参。ConditionContext接口会有一些方法检查bean的定义或bean的属性,AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有其他注解,或是检查@Bean注解的方法上其他注解的属性.

在Spring4开始,@profile注解就是根据@Conditional和Condition实现的,我们可以参照它的实现。

处理自动装配的歧义性

使用Spring的自动封装来装配bean真的是太方便了,但是,这是有且只有一个bean匹配所需条件时,自动装配才有效。如果有多个bean匹配结果的话,这种歧义性会阻碍Spring自动装配。例如使用@Autowired注解标注Dessert接口

    @Autowired
    private Dessert dessert;

但是这个接口有三个实现:

@Component
public class Cake implements Dessert {}

@Component
public class Cookies implements Dessert {}

@Component
public class IceCream implements Dessert {}

这三个实现类都用@Component注解,当组件扫描的时候,它们都会在Spring上下文中创建bean。然后当Spring试图自动装配Dessert的时候,它没有唯一,无歧义的值。Spring就会抛出NoUniqueBeanDefinitonException。

其实在使用中,自动装配的歧义性并不常见,因为一般情况是给定的类型只有一个实现类,自动装配可以很好地运行。但是,如果真的出现歧义性,Spring提供了:可以将某一个可选bean设为首选(primary)的bean,或者使用限定(qualifier)来帮助Spring将可选的bean'范围缩小到只有一个bean.

标示首选bean

当声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配的歧义性。当出现歧义性的时候会直接使用首选的bean。Spring使用的正是@Primary注解,这个注解能够与@Component组合在组件扫描的bean上,也可以与@Bean组合用在Java配置的bean声明上。

@Component
@Primary
public class Cake implements Dessert {}

或者使用的是JavaConfig则如下:

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

如果使用XML配置bean的话,同样可以实现这样的功能。<bean>元素有一个primary属性来指定首选的bean:

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

但是,如果你标记了多个首选bean,那么又会带来新的歧义性,事实上也就没有了首选,Spring也无法正常工作。解决歧义性使用限定符是一种更为强大的机制

限定自动装配的bean

使用首选bean只能标示一个优先的选择方案,当首选bean数量超过一个,我们就没有其他方法进一步缩小范围。而且,我们每次使用该类型,都会自动使用首选bean,如果在某处不想使用就不好处理了。使用限定可以缩小可选bean的范围直到只有一个bean满足条件,如果还存在歧义性,那么你还可以继续使用更多的限定来进一步缩小范围。@Qualifier注解就是限定,它可以与@Autowired协同使用,在注入时指定想要注入的是哪个bean。

    @Autowired
    @Qualifier("iceCream")
    private Dessert dessert;

我们已经知道了使用@Component注解声明的类,默认使用bean的ID是首字母小写的类名。因此@Qualifier("iceCream")指向的正是IceCream类的实例.这种情况是将限定与要注入的bean的名称是紧密耦合的,对类名的任何修改都会影响限定失败。这个时候,我们可以使用自定义的限定。

自定义的限定

我们可以自定义的设置限定,而不是依赖beanID作为限定,在这里@Qualifier可以与@Component或@Bean配合使用。

组件:

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

或是在JavaConfig方式中:

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

这样我们在注入的时候就可以使用"cold"这个名称了:

    @Autowired
    @Qualifier("cold")
    private Dessert dessert;

使用自定义的限定注解

当一个限定不能解决问题的时候,可以使用多个限定来缩小范围,也就是使用多个@Qualifier,但是在Java中不允许出现两个相同的注解(Java8可以,但也要求该注解本身实现@Repeatable注解,不过,Spring的@Qualifier没有实现),这个时候就需要我们创建一个注解了,该注解使用@Qualifier标注,就直接使用我们自定义的注解即可。

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

表明bean限定的时候就可以用:

@Component
@Clod
public class IceCream implements Dessert {}

我们在注入的时候直接使用这个@Cold注解即可:

    @Autowired
    @Cold
    private Dessert dessert;

我们可以自定义多个注解来进一步缩小范围

 bean的作用域

在默认情况下,Spring应用上下文的bean都是作为单例的形式被创建,不管给定的bean被注入到其他的bean多少次,每次注入的都是同一个实例。但有时我们使用的类是易变的,这个时候使用单例就不太好了,因为对象会被修改。

Spring提供了多种作用域,可以基于这些作用域创建bean:

  • 单例(singleton):这整个应用中只创建bean的一个实例
  • 原型(Prototype):每次注入的时候都会创建一个新的bean的实例(多例)
  • 会话(Session):在Web开发中,为每一个会话创建一个bean实例
  • 请求(Request):在Web开发中,为每一个请求创建一个bean实例

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

@Component
@Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {}

使用ConfigurableBeanFactory.SCOPE_PROTOTYPE常量比较安全,当也可以使用字面量的方式:@Scope("prototype")

如果使用JavaConfig的方式:

@Bean
@Scop(value=ConfigurableBeanFacory.SCOPE_PROTOTYPE)
public Notepad notepad(){
  return Notepad();  
}

如果使用XML的方式,可以在<bean>元素的scope属性设置:

<bean id="notepad" class="cn.lynu.Notepad" scope="prototype"/>

使用会话或请求作用域的bean

使用会话或请求作用域,还是用@Scope注解,它的使用方式与原型作用域大致相同:

@Component
@Scope(value="session",proxyMode=ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {}

Spring会在Web应用的每一个会话中创建一个ShoppingCart实例,但是在对每一个的会话操作中,这个bean实际相当于单例. 注意这里使用了proxyMode属性,这个属性可以解决将会话或请求bean注入到单例bean中所遇到的问题:例如将ShoppingCart bean注入到单例的StoreService bean

@Component
public class StoreService{
    @AutoWired
    private ShoppingCart shoppingCart;    
}    

当扫描到StoreService 就会创建这个bean,并试图将ShoppingCart bean注入进去,但是ShoppingCart是会话作用域的,此时不存在,只有例如某个用户登录系统,创建会话之后,这个bean才存在;而且会话会有多个,ShoppingCart实例也会有多个,我们不想注入某个固定的实例,应该是当需要使用ShoppingCart实例恰好是当前会话中的那一个。

使用proxyMode就会先创建一个ShoppingCart bean的代理,将这个代理给StoreService ,当StoreService 真正使用的时候,代理会调用会话中真正的ShoppingCart bean。需要注意的是:如果ShoppingCart是一个接口,需要使用 ScopedProxyMode.INTERFACES JDK动态代理,但如果ShoppingCart是一个具体类,它必须使用CDLib来做代理,必须将proxyMode属性设置为ScopedProxyMode.TERGET_CLASS.表明要生成代理类的拓展类的方式创建代理。

在XML中声明作用域代理

如果使用XML的方式就不能使用@Scope注解及其proxyMode属性了。<bean>元素的scope属性能够设置bean的作用域,但是如何指定代理模式呢?

<bean id="cart" class="cn.lynu.ShoppingCart" scope="session">
    <aop:scoped-proxy />
</bean>

默认情况下,它使用CGLib常见目标类的代理,但是我们也可以将proxy-target-class属性设置为false,就是用JDK的代理

<bean id="cart" class="cn.lynu.ShoppingCart" scope="session">
    <aop:scoped-proxy proxy-target-class="false" />
</bean>

运行时值注入

有的时候,我们可能会希望避免硬编码,而且想让这些值在运行的时候再确定。为了实现这种功能,Spring提供了两种在运行时求值的方式:

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

注入外部的值

在Spring中,处理外部属性最简单的但是就是声明属性源并通过Spring的Environment来检索属性:

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

文件 app.properties 就是属性源,使用@propertySource注解声明,这个属性文件会加载到Spring的Environment,我们再注入Environment,就可以通过Environment的方法获得属性了。使用getPropperty()方法及其重载方法时,当属指定的属性不存在时,可以通过重载方法给一个默认值,如果没有默认值就会返回null。如果希望这个属性必须存在,那么可以使用getRequiredProperty()方法,Environment还有一些其他方法。

直接从Environment中获得属性的值,在JavaConfig的方式中非常方便。Spring还提供了占位符装配属性的方法,这些占位符的值来源于一个属性源。

使用属性占位符

Spring中可以使用属性占位符的方式将值插入到Spring bean中,在Spring装配中,占位符的形式为使用"${...}"包裹的属性名称。

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

这样XML文件中没有任何的硬编码,属性的值都是从属性源中获得。如果我们使用的是组件扫描和自动装配,没有使用XML的话,在这种情况下,我们使用@Value注解:

@Component
public class BlankDisc {
    
    private String title;
    private String artist;

    public BlankDisc() {}

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

最后,为了属性占位符可以使用,我们还需要配置一个PropertySourcePlaceholdConfigurer,它可以根据Spring的Environment及其属性源来解析占位符。

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

如果使用XML,需要context名称空间的<context:property-placeholder>元素,这个元素会生成PropertySourcePlaceholdConfigurer bean:

<context:property-placeholder/>

 

posted @ 2018-04-30 16:38  OverZeal  阅读(326)  评论(0编辑  收藏  举报