美文网首页
9、高级装配1(spring笔记)

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

作者: yjaal | 来源:发表于2017-03-19 11:51 被阅读63次

    一、环境与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标签中配置:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
     ....
      xsi:schemaLocation="
        http://www.springframework.org/schema/jee
         ...
        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>
    

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

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/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>
    

    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

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_2_5.xsd"
        id="WebApp_ID" version="2.5">
    
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring/root-context.xml</param-value>
        </context-param>
        
        <context-param>
            <param-name>spring.profiles.default</param-name>
            <param-value>dev</param-value><!--为上下文设置默认的profile-->
        </context-param>
    
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</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><!--为servlet设置默认的profile-->
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>appServlet</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    </web-app>
    

    说明:这里我们配置了默认的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<String, Object> getAnnotationAttributes(String annotationType);
        Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValueAsString);
        MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType);
        MultiValueMap<String, Object> 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<String, Object> 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方式配置:

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

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

    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笔记)

          本文链接:https://www.haomeiwen.com/subject/nokmnttx.html