Spring 学习笔记(三)高级装配

2018-12-13  本文已影响2人  Theodore的技术站

主要内容

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

环境与 profile

在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另外一个环境。。数据库配置、加密算法以及与外部系统的集成是跨环境部署时会发生变化的几个典型例子。
比如,考虑一下数据库配置。在Spring配置类中,我们可能会在一个带有@Bean注解的方法上使用EmbeddedDatabaseBuilder:

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

在生产环境的配置中,你可能会希望使用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();
}

在QA环境中,你可以选择完全不同的DataSource配置,可以配置为Commons DBCP连接池,如下所示:

@Bean(destroyMethod="close")
public DataSource dataSource(){
    BasicDataSource dataSource = new BasicDataSource();
    dataSource.setUrl("jdbc:h2:tcp://dbserver/~/test");
    dataSource.setDriverClassName("org.h2.Driver");
    dataSource.setUsername("sa");
    dataSource.setPassword("password");
    dataSource.setInitialSize(20);
    dataSource.setMaxActive(30);
    return dataSource;
}

显然,这里展现的三个版本的dataSource()方法互不相同。虽然它们都会生成一个类型为javax.sql.DataSource的bean,但它们的相似点也仅限于此了。每个方法都使用了完全不同的策略来生成DataSource bean。
在 spring 中可以配置 profile bean
在Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile。例如,在配置类中,嵌入式数据库的DataSource可能会配置成如下所示:

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

Spring这个配置类中的bean只有在dev profile激活时才会创建。如果dev profile没有激活的话,那么带有@Bean注解的方法都会被忽略掉。
同时,你可能还需要有一个适用于生产环境的配置,如下所示:

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

在本例中,只有prod profile激活的时候,才会创建对应的bean。
在 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是激活的。但如果没有设置spring.profiles.active属性的话,那Spring将会查找spring.profiles.default的值。如果spring.profiles.active和spring.profiles.default均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在profile中的bean。
有多种方式来设置这两个属性:
-作为DispatcherServlet的初始化参数;
-作为Web应用的上下文参数;
-作为JNDI条目;
-作为环境变量;
-作为JVM的系统属性;
-在集成测试类上,使用@ActiveProfiles注解设置。

在Web应用的web.xml文件中设置默认的profile:

<?xml version="1.0" encoding="UTF-8" ?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLoaction="http://java.sun.com/xml/ns/javaee
            http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <context-param>
        <param-name>contextConfiguration</param-name>
        <param-value>/WEB-INF/spring/root-context.xml</param-value>
    </context-param>
    <context-param>
        <param-name>spring.profile.default</param-name>    //为上下文设置默认的 profile
        <param-value>dev</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoderListener</listener-class>
    </listener>
    <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>
    <servlet-mappring>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mappring>
</web-app>

按照这种方式设置spring.profiles.default,所有的开发人员都能从版本控制软件中获得应用程序源码,并使用开发环境的设置(如嵌入式数据库)运行代码,而不需要任何额外的配置。
当应用程序部署到QA、生产或其他环境之中时,负责部署的人根据情况使用系统属性、环境变量或JNDI设置spring.profiles.active即可。当设置spring.profiles.active以后,至于spring.profiles.default置成什么值就已经无所谓了;系统会优先使用spring.profiles.active中所设置的profile。
使用profile进行测试
当运行集成测试时,通常会希望采用与生产环境(或者是生产环境的部分子集)相同的配置进行测试。但是,如果配置中的bean定义在了profile中,那么在运行测试时,我们就需要有一种方式来启用合适的profile。
Spring提供了@ActiveProfiles注解,我们可以使用它来指定运行测试时要激活哪个profile。在集成测试时,通常想要激活的是开发环境的profile。例如,下面的测试类片段展现了使用@ActiveProfiles激活dev profile:

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

条件化的 bean

Spring 4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。
看下面三个代码实例

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

可以看到,@Conditional中给定了一个Class,它指明了条件——在本例中,也就是MagicExistsCondition。@Conditional将会通过Condition接口进行条件对比:

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

设置给@Conditional的类可以是任意实现了Condition接口的类型。可以看出来,这个接口实现起来很简单直接,只需提供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。如果matches()方法返回false,将不会创建这些bean。

package com.habuma.restfun;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;
public class MagicExistCondition implements Condition{
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata){
        Environment env = context.getEnvironment();
        return Env.containsProperty("magic");
    }
}

在上面的程序清单中,matches()方法很简单但功能强大。它通过给定的ConditionContext对象进而得到Environment对象,并使用这个对象检查环境中是否存在名为magic的环境属性。
MagicExistsCondition中只是使用了ConditionContext得到的Environment,但Condition实现的考量因素可能会比这更多。matches()方法会得到ConditionContext和AnnotatedTypeMetadata对象用来做出决策。ConditionContext是一个接口,大致如下所示:

public interface ConditionContext{
    BeanDefinitionRegistry getRegistry();
    ConfigurableListableBeanFactory getBeanFactory();
    Environment getEnvironment();
    ResourceLoader getResourceLoader();
    ClassLoader getClassLoader();
}

通过ConditionContext,我们可以做到如下几点:
-借助getRegistry()返回的BeanDefinitionRegistry检查bean定义;
-借助getBeanFactory()返回的
ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性;
-借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么;
-读取并探查getResourceLoader()返回的ResourceLoader所加载的资源;
-借助getClassLoader()返回的ClassLoader加载并检查类是否存在。

AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。像ConditionContext一样,AnnotatedTypeMetadata也是一个接口。它如下所示:

public interface AnnotatedTypeMetadata{
    boolean isAnnotated(String annotationType);
    Map<String, Object> getAnnotationAttributes(String annotationType);
    Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString);
    MultiValueMap<String, Object> getAllAnnotationAttributes(String annotatonType);
    MultiValueMap<String, Object> getAllAnnotaionAttributes(String annotationType, boolean classValuesAsString);
}

借助isAnnotated()方法,我们能够判断带有@Bean注解的方法是不是还有其他特定的注解。借助其他的那些方法,我们能够检查@Bean注解的方法上其他注解的属性。
从Spring 4开始,@Profile注解进行了重构,使其基于@Conditional和Condition实现。

//程序清单3.6 ProfileCondition检查某个bean profile是否可用
class ProfileCondition implements Condition{
    public boolean matches(ConditionContext context,AnnotatedTypeMetadata metadata){
        if(context.getEnvironment() != null){
            MultiValueMap<String, Object> attrs 
            = metadata.getAllAnnotationAttributes(Profile.class.getName());
            if(attrs != null){
                for(Object value:attrs.get("value")){
                    if(context.getEnvironment().acceptsProfile(((String[]) value))){
                        return true;
                    }
                }
                return false;
            }
        }
        return true;
    }
}

我们可以看到,ProfileCondition通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性。借助该信息,它会明确地检查value属性,该属性包含了bean的profile名称。然后,它根据通过ConditionContext得到的Environment来检查[借助acceptsProfiles()方法]该profile是否处于激活状态。

处理自动装配的歧义性

假设我们使用@Autowired注解标注了setDessert()方法:

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

但是如果 Dessert 是一个接口,然后三个类实现了这个接口,三个类都使用了 @Component 注解,组件扫描的时候,spring 无法做出选择,就会抛出 NoUniqueBeanDefinitionException
有几种方法可以结果这种歧义性问题:

标示首选的 bean

在声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将会使用首选的bean,而不是其他可选的bean。
下面的代码展现了如何将@Component注解的IceCream bean声
明为首选的bean:

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

下面的代码展现了如何将@Component注解的IceCream bean声明为首选的bean:

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

XML 配置

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

限定自动装配的 bean

@Qualifier注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。例如,我们想要确保要将IceCream注入到setDessert()之中:

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

创建自定义的限定符
我们可以为bean设置自己的限定符,而不是依赖于将bean ID作为限定符。在这里所需要做的就是在bean声明上添加@Qualifier注解。例如,它可以与@Component组合使用,如下所示:

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

还可以添加多个注解

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

还可以自己创建注解:

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD,ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qulifier
public @interface Cold{}
上一篇下一篇

猜你喜欢

热点阅读