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标签中配置:

<?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类型来指定限定符,自定义的注解也更为类型安全。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,563评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,713评论 6 342
  • 本章内容: Spring profile 条件化的bean声明 自动装配与歧义性 bean的作用域 Spring表...
    谢随安阅读 1,167评论 0 5
  • 一 自古及今,不说洋鬼子们的历史了,单说中国古代,猛将满书上都是,随手一翻都能有个顶天立地的大将军提刀跨马、摆着迷...
    猫猫葵阅读 1,742评论 8 5
  • 这是一个金色的时季 有金黄的橘子和稻谷 还有金的耀眼和红的似火的叶子 啊,叶子 树上的合着风的歌唱 地上的装饰着泥...
    我曾有梦阅读 157评论 0 0