9、高级装配1(spring笔记)

一、环境与profile

在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另一个环境,因为开发阶段中,某些环境相关的做法可能并不适合迁移到生产环境中,甚至即便迁移过去也无法正常工作。比如数据库的配置中,有可能在测试的时候使用的嵌入式的数据库,并且加载相关的测试数据,但是在生产环境中可能会使用JNDI获取一个DataSource,或者配置一个数据库连接池C3P0,每种取得DataSource的方式都不一样,以前可能会在XML中配置多种策略,然后在构建(比如在XML文件中选择某种策略)的时候选择不同的策略。下面看spring如何处理这个问题。

1.1 配置profile bean

其实spring提供的方案和构建解决方案没有太大的差别,但是spring并不是在构建时选择某种策略,而是在运行时再来确定。这样同一个部署单元能够适用于所有的环境,没必要重新构建。

3.1版本中,spring引入了bean profile的功能,要使用此功能,首先要将所有不同的bean定义整理到一个或多个profile中,在应用部署到每个环境时,要确保对应的profile处于激活(active)状态。

Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile,如配置一个嵌入式数据库DataSource

@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只有在dev profile激活时才会创建,这里可以表示是在开发环境下的bean。我们还可以配置一个生产环境下的bean

@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,但是只有被激活的那个profile对应的bean会被创建。
注意:以上的profile配置都可以在总的数据源配置类DataSourceConfig中进行配置。

注意:上面@Bean中配置了destroyMethod方法,一般情况下是会执行相关方法的,比如destroyMethod = "destroy"就表示此bean在销毁时会执行其destroy方法,但是会默认匹配找到close、shutdown方法(只要此类实现了java.lang.AutoCloseablejava.io.Closeable),具体信息请参看Spring指导手册的6.6.1小节。但是这里的DataSource类和EmbeddedDatabaseBuilder类中都没有shutdown方法,不清楚配置是什么意思。

1.1.1 在 XML 中配置 profile

如果要配置一个profile,可以向下面这样在beans标签中配置:




  
    
    
  

说明:但是如果需要配置多个profile,就不能这样了,我们可以重复使用元素来指定多个profile,如下所示:




  
    
      
      
    
  
  
  
    
  

1.2 激活 profile

spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.activespring.profiles.default。如果设置了前一个属性,那么它的值就用来确定哪个profile是激活的,但是如果没有,则去后一个属性中的值,这个值即一个默认值。如果两个属性都没有设置,则没有profile会被激活。有多种方式来设置这两个属性:

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

这里我们看使用DispatcherServlet的参数将spring.profiles.default设置为开发环境的profile,需要在servlet上下文进行设置(为了兼顾到ContextLoaderListener)。如下所示:
web.xml




    
        contextConfigLocation
        /WEB-INF/spring/root-context.xml
    
    
    
        spring.profiles.default
        dev
    

    
        org.springframework.web.context.ContextLoaderListener
    
    
    
        appServlet
        org.springframework.web.servlet.DispatcherServlet
        
            spring.profiles.default
            dev
        
        1
    
    
        appServlet
        /
    

说明:这里我们配置了默认的profile,如果今后还有其他的profile,则可以设置spring.profiles.active属性,这样就可以覆盖掉默认属性。同时我们也可以激活多个profile,使用逗号分隔,但是激活多个profile意义不大。

1.2.1 使用 profile 进行测试

配置好一个或多个profile之后,在测试或者实际运行的时候需要激活某个profile,此时我们可以使用@ActiveProfiles注解来将某个profile激活:

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

1.2.2 具体测试

这里由于书中例子不是很完整,所以这里我们使用《8、装配bean(补)(spring笔记)》这一节中的例子测试一下,首先我们对配置类做一下改动(使用@Bean配置方式):
Config.java

@Configuration
public class Config {
    
    @Bean
    @Profile("dev")
    public UserDao getUserDao4MySql(){
        return new UserDao4MySqlImpl();
    }
    
    @Bean
    @Profile("product")
    public UserDao getUserDao4Oracle(){
        return new UserDao4OracleImpl();
    }
    
    @Bean
    public UserManager getUserManager(UserDao userDao){
        return new UserManagerImpl(userDao);
    }
}

测试的时候我们可以选择激活哪一个数据库配置:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=win.iot4yj.spring.config.Config.class)
@ActiveProfiles("product")
public class IoCTest {
}

可以看到这里激活了Oracle的配置。对于XML配置方式,其实差不多,这里不再细说。

二、条件化的 bean

有时候我们希望某个bean在满足某些条件时才创建,否则就不创建。假设有一个MagicBean类,我们希望只有设置了magic环境属性的时候,Spring才会实例化这个类,否则就忽略此类:

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

说明:这里我们使用@Conditional注解指明条件为MagicExistsCondition@Conditional将会通过Condition接口进行条件对比:

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

说明:设置给@Conditional的类可以是任意实现了Condition接口的类型。可以看到我们只要实现matches方法即可:

package com.habuma.restfun;
import ...

public class MagicExistsCondition implements Condition {

  @Override
  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    Environment env = context.getEnvironment();
    return env.containsProperty("magic");
  }
}

说明:

  • 上述matches方法通过给定的ConditionContext对象进而得到Environment对象,并使用此对象检查环境中是否存在名为magic的环境属性,这里属性的值是什么无所谓。如果属性满足则条件满足,bean就能被创建出来,否则,就忽略。如果考虑的因素更多,matches方法则可能需要使用ConditionContextAnnotatedTypeMetadata 对象来做出决策。

  • 其中ConditionContext是一个接口,大致如下:

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

使用此接口可以做到如下几点:

  • 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义

  • 借助getBeanFactory()返回的ConfigurableListableBeanFactory 检查bean是否存在,甚至检查bean的属性

  • 借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么

  • 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源

  • 借助getClassLoader()返回的ClassLoader 加载并检查类是否存在

  • AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解,也是一个接口:

public class AnnotatedTypeMetadata{
    boolean isAnnotated(String annotationType);
    Map getAnnotationAttributes(String annotationType);
    Map getAnnotationAttributes(String annotationType, boolean classValueAsString);
    MultiValueMap getAllAnnotationAttributes(String annotationType);
    MultiValueMap getAllAnnotationAttributes(String annotationType, boolean classValueAsString);
}

说明:借助isAnnotated()方法能够判断带有@Bean注解的方法是不是还有其他特定的注解;借助其他方法,能够检查@Bean注解方法上其他注解的属性。

Spring 4开始,@Profile注解进行了重构,使其基于@ConditionalCondition实现:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
    String[] value();
}

说明:可以看到@Profile本身也使用了@Conditional注解,并且引用ProfileCondition作为Condition实现,实现中考虑到了ConditionContextAnnotatedTypeMetadata中的多个因素:

class ProfileCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (context.getEnvironment() != null) {
            MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
            if (attrs != null) {
                for (Object value : attrs.get("value")) {
                    if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                        return true;
                    }
                }
                return false;
            }
        }
        return true;
    }
}

说明:首先是得到了@Profile注解的所有属性。借助该信息,会明确地检查value属性,该属性包含了beanprofile名称,然后通过Environment来检查[借助acceptsProfiles()方法]该profile是否处于激活状态。就是比较环境中的value值和profile中的value值是不是一致的。

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

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

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

说明:上例中Dessert是一个接口,有如下三个实现:

@Component
public class Cake implements Dessert{...}

@Component
public class Cookies implements Dessert{...}

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

说明:此时如果要自动装配setDessert方法,那么有三个可以匹配的bean,这样就会造成歧义。下面看如何解决这种歧义。

3.1 标示首选的 bean

对于上面三个可选的bean,我们可以标识一个为首选的bean,这样就不会出现歧义了:

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

当然也可以使用XML方式配置:


说明:但是如果我们配置多个首选,那么又会出现歧义。就解决歧义性的问题,限定符是一种更为强大的机制。

3.2 限定自动装配的 bean

之前的@Primary只能标识一个优选方案,但是并不能解决歧义性问题。而限定符能够在所有可选的bean上进行缩小分为的操作,最终能够达到只有一个bean满足所有要求。如果依然存在歧义性,那么可以继续使用更多的限定符来缩小范围。

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

说明:这是使用限定符最简单的例子。为@Qualifier注解所设置的参数就是想要注入的beanID。但是要注意:这个"iceCream"要和实际beanID一致。但是如果重构了IceCream类,将其重命名为Gelato的,同时有是使用默认ID,那么就会出现问题。于是我们可以创建自定义的限定符来解决此问题。

3.2.1 创建自定义的限定符

我们可以为bean设置自己的限定符,而不是依赖于将bean ID作为限定符:

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

说明:这里我们还是使用@Qualifier注解来为bean创建了一个自定义的限定符"cold",而且不依赖bean的类名或ID,于是方法上可以这样使用:

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

说明:更为重要的是通过Java配置显示定义bean的时候,@Qualifier可以和@Bean一起使用:

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

说明:在使用自定义限定符的时候,最佳实践是为bean选择特征性或描述性的术语。

3.2.2 使用自定义的限定符注解

如果此时我们有引入了一个新的Dessert bean

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

此时就有两个实现了Dessert接口的bean,而且使用相同的自定义限定符,这样会显然会造成歧义,于是我们可以再加上一层限定:

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

于是此时我们可以这样定义方法setDessert

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

说明:

  • 低版本的Java不与许在同一个条目上重复出现相同类型的多个注解,但是Java8允许,只要这个注解本身定义的时候带有@Repeatable注解就可以,不过Spring@Qualifier注解并没有在定义时添加@Repeatable注解。仅仅使用@Qualifier并没有办法将可选的bean缩小到仅有一个可选的bean。这里我们可以使用自定义的限定符注解解决。

  • 这里所需要做的就是创建一个注解,它本身要使用@Qualifier注解来标注:

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

这样便定义了一个注解,可以这样使用:

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

说明:通过声明自定义的限定符注解,可以同时使用多个限定符,不会再有其他问题。同时,相对于使用原始的@Qualifier并借助String类型来指定限定符,自定义的注解也更为类型安全。

你可能感兴趣的:(9、高级装配1(spring笔记))