13、IOC 之环境抽象

13、IOC 之环境抽象

Environment 接口是集成在容器中的抽象,用于对应用程序环境的两个关键方面进行建模:配置文件属性

配置文件是一个命名的逻辑Bean定义组,只有当给定的概要文件处于活动状态时,它才能向容器注册。Bean可以分配给配置文件,无论该配置文件是用XML定义的还是用注释定义的。与配置文件相关的 Environment 对象的作用是确定哪些配置文件(如果有的话)当前是活动的,以及哪些配置文件(如果有的话)在默认情况下应该是活动的。

属性在几乎所有的应用程序中都扮演着重要的角色,并且可能起源于各种各样的来源:属性文件、JVM系统属性、系统环境变量、JNDI、servlet上下文参数、特别的 Properties 对象、Map 对象,等等。与属性相关的 Environment 对象的角色是为用户提供一个方便的服务接口,用于配置属性源并从中解析属性。

13.1、Bean 定义配置文件

Bean 定义配置文件在核心容器中提供了一种机制,允许在不同的环境中注册不同的 Bean。“环境”这个词对不同的用户来说可能意味着不同的东西,这个功能可以帮助许多用例,包括:

  • 在开发中处理内存中数据源,而不是在 QA 或生产中从 JNDI 查找相同的数据源
  • 仅当将应用程序部署到性能环境中时才注册监视基础结构
  • 为客户 A 部署与客户 B 部署注册 Bean 的自定义实现

考虑需要 DataSource 的实际应用程序中的第一个用例。在测试环境中,配置可能类似于以下内容:

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}

现在考虑如何将此应用程序部署到 QA 或生产环境中,假设应用程序的数据源已注册到生产应用程序服务器的 JNDI 目录中。我们的 dataSource Bean现在看起来像下面的清单:

@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

问题是如何根据当前环境在使用这两种变体之间进行切换。随着时间的推移,Spring用户已经设计了许多方法来完成这一任务,通常依赖于系统环境变量和XML 语句的组合,这些语句包含 ${placeholder} ,根据环境变量的值解析到正确的配置文件路径。Bean定义配置文件是一个核心容器特性,它为这个问题提供了一个解决方案。

如果我们泛化前面环境特定的Bean定义示例中显示的用例,我们最终需要在某些上下文中注册某些Bean定义,而在其他上下文中则不需要。可以说,想要在情况A中注册Bean定义的某个配置文件,而在情况B中注册不同的配置文件。我们通过更新配置来反映这种需求。

使用 @Profile

当一个或多个指定的概要文件处于活动状态时,@Profile 注释允许你指示组件符合注册资格。使用前面的示例,我们可以如下重写 dataSource 配置:

@Configuration
@Profile("development")
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}
@Configuration
@Profile("production")
public class JndiDataConfig {

    @Bean(destroyMethod="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

如前所述,使用 @Bean 方法,通常选择使用编程式 JNDI查找,通过使用 Spring的 JndiTemplate / JndiLocatorDelegate助手或前面显示的直接JNDI InitialContext用法,但不使用 JndiObjectFactoryBean变体,这将强制你将返回类型声明为 FactoryBean类型。

配置文件字符串可能包含一个简单的配置文件名称(例如,production)或一个配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,production & us-east)。配置文件表达式中支持以下操作符:

  • !:配置文件的逻辑“非”
  • &:配置文件的逻辑“和”
  • |:配置文件的逻辑“或”

如果不使用圆括号,就不能混合使用 &|操作符。例如,production & us-east | eu-central不是一个有效的表达式。它必须表示为 production & (us-east | eu-central)

你可以使用 @Profile 作为元注释来创建自定义组合注释。下面的例子定义了一个自定义的 @Production 注释,你可以用它来代替 @Profile("production"):

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

如果 @Configuration类用 @Profile标记,那么与该类关联的所有 @Bean方法和 @Import注释都会被忽略,除非一个或多个指定的概要文件处于活动状态。如果 @Component@Configuration类被标记为 @Profile({"p1", "p2"}),除非 profile 'p1’或’p2’被激活,否则该类不会被注册或处理。如果给定的概要文件以 !操作符作为前缀,那么只有当概要文件不是活动的时候,才会注册注释的元素。例如,给定 @Profile({"p1", "!p2"}),如果 profile 'p1’激活或 profile 'p2’未激活,就会发生注册。

@Profile也可以在方法级别声明为仅包含配置类的一个特定 Bean(例如,对于特定 Bean 的替代变体),如以下示例所示:

@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("development") // standaloneDataSource 方法仅在 development 配置文件中可用
    public DataSource standaloneDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }

    @Bean("dataSource")
    @Profile("production") // jndiDataSource 方法仅在 production 配置文件中可用
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

对于 @Bean方法上的 @Profile,可能适用于一种特殊的场景:对于具有相同 Java方法名的重载 @Bean方法(类似于构造函数重载),需要在所有重载方法上一致声明 @Profile条件。如果条件不一致,则只有重载方法中第一个声明的条件才重要。因此,不能使用 @Profile选择具有特定参数签名的重载方法而不是其他方法。同一个 Bean的所有工厂方法之间的解析在创建时遵循 Spring构造函数解析算法。

如果你希望定义具有不同概要条件的替代 Bean,那么可以使用 @Bean name属性使用指向相同 Bean名的不同 Java方法名称,如上例所示。如果参数签名都是相同的(例如,所有变体都有无参数的工厂方法),这是首先在有效的 Java类中表示这种安排的唯一方法(因为特定名称和参数签名只能有一个方法)。

XML Bean 定义配置文件

对应的XML是<beans>元素的profile文件属性。我们前面的示例配置可以重写为两个XML文件,如下所示:

<beans profile="development"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">

    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>
<beans profile="development"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">

    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>

还可以避免在同一个文件中分割和嵌套<beans/>元素,如下面的示例所示:

<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"
    xsi:schemaLocation="...">

    <!-- other bean definitions -->

    <beans profile="development">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>

    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>

spring-bean.xsd被限制只允许这些元素作为文件中的最后一个元素。这应该有助于提供灵活性,而不会导致XML文件混乱。

XML对应项不支持前面描述的配置文件表达式。但是,也可以使用 ! 操作符。通过嵌套概要文件,也可以应用一个逻辑的 “and”,如下面的例子所示:

<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"
    xsi:schemaLocation="...">

    <!-- other bean definitions -->

    <beans profile="production">
        <beans profile="us-east">
            <jee:jndi-lookup id="dataSource" 
                             jndi-name="java:comp/env/jdbc/datasource"/>
        </beans>
    </beans>
</beans>

在上面的例子中,如果 productionus-east配置文件都处于活动状态,那么 dataSource bean就会被公开。

激活一个概要文件

现在我们已经更新了配置,我们仍然需要告诉 Spring 哪个配置文件是活动的。如果我们现在启动示例应用程序,我们将看到抛出一个 NoSuchBeanDefinitionException,因为容器无法找到名为 dataSource 的Spring Bean。

激活一个配置文件可以通过几种方式来完成,但最直接的方式是对通过 ApplicationContext 可用的 Environment API进行编程。下面的例子展示了如何这样做:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

另外,你还可以通过 spring.profiles.active 属性来声明性地激活概要文件,它可以通过 web.xml 中的系统环境变量、JVM系统属性、servlet上下文参数来指定,甚至可以作为 JNDI中的一个条目(请参阅 抽象 PropertySource)。在集成测试中,可以使用 spring-test 模块中的 @ActiveProfiles 注释来声明活动概要文件(参见使用环境概要文件进行上下文配置)。

注意!!!概要文件不是一个“非此即彼”的命题。你可以一次激活多个配置文件。通过编程,你可以为 setActiveProfiles() 方法提供多个配置文件名称,该方法接受 String… 参数。下面的例子激活多个配置文件:

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

声明性地,spring.profiles.active 可以接受一个逗号分隔的配置文件名称列表,如下所示:

-Dspring.profiles.active="profile1,profile2"

默认配置文件

默认配置文件表示默认情况下启用的配置文件。请考虑以下示例:

@Configuration
@Profile("default")
public class DefaultDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}

如果没有激活配置文件,则创建 dataSource。你可以将此视为为一个或多个Bean提供默认定义的一种方法。如果启用了配置文件,则默认配置文件不适用。

你可以通过在 Environment 上使用 setDefaultProfiles() 来更改默认概要文件的名称,或者以声明的方式使用 spring.profiles.default 属性。

13.2、抽象 PropertySource

Spring的 Environment 抽象在可配置的属性源层次结构上提供搜索操作。考虑以下清单:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + 
                   containsMyProperty);

在前面的代码片段中,我们看到一种高级的方式来询问 Spring 是否为当前环境定义了my-property 属性。为了回答这个问题,Environment 对象对一组 PropertySource 对象执行搜索。PropertySource 是对任何键-值对源的简单抽象,Spring的 StandardEnvironment 配置了两个PropertySource对象——一个表示JVM系统属性集(System.getProperties()),一个表示系统环境变量集(System.getenv())。

这些默认属性源用于 StandardEnvironment,用于在独立应用程序中使用。StandardServletEnvironment填充了额外的默认属性源,包括 servlet配置和 servlet上下文参数。它可以选择性地启用 JndiPropertySource

具体地说,当使用 StandardEnvironment 时,如果运行时存在 my-property 系统属性或 my-property 环境变量,对 env.containsProperty("my-property") 的调用将返回 true。

执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用 env.getProperty("my-property")期间,在两个位置都设置了 my-property属性,则系统属性值 " 胜出 " 并被返回。请注意,属性值并没有被合并,而是完全被前面的条目覆盖。

对于一个常见的 StandardServletEnvironment,完整的层次结构如下,最高优先级的条目在顶部 :

  • ServletConfig 参数 (如果适用的话——例如,在 DispatcherServlet上下文的情况下 )
  • ServletContext 参数 ( web.xml上下文参数条目 )
  • JNDI环境变量 ( java:comp/env/ 条目 )
  • JVM系统属性 ( -D命令行参数 )
  • JVM系统环境 ( 操作系统环境变量 )

最重要的是,整个机制是可配置的。也许你有一个自定义属性源,希望将其集成到此搜索中。为此,实现并实例化你自己的 PropertySource,并将其添加到当前EnvironmentPropertySource 集合中。下面的例子展示了如何这样做:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

在上面的代码中,搜索中以最高优先级添加了MyPropertySource。如果它包含 my-property 属性,则检测并返回该属性,这有利于任何其他 PropertySource 中的 my-property 属性。MutablePropertySources API 公开了许多方法,这些方法允许对属性源集进行精确操作。

13.3、使用 @PropertySource

@PropertySource 注释为向Spring环境添加 PropertySource 提供了一种方便且声明性的机制。

给定一个名为 app.properties 的文件,该文件包含键值对 testbean.name=myTestBean,下面的 @Configuration 类以这样的方式使用 @PropertySource,调用 testBean.getName() 返回 myTestBean:

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

@PropertySource 资源位置中的任何 ${…} 占位符都将根据已经在环境中注册的一组属性源进行解析,如下所示:

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

假设 my.placeholder 存在于已注册的一个属性源(例如,系统属性或环境变量)中,占位符将解析为相应的值。如果不是,则使用 default/path 作为默认值。如果未指定默认值且无法解析属性,则会引发 IllegalArgumentException

根据 Java 8约定,@PropertySource 注释是可重复的。但是,所有这类 @PropertySource 注释都需要在相同的级别上声明,要么直接在配置类上声明,要么作为同一个自定义注释中的元注释声明。不建议混合使用直接注释和元注释,因为直接注释会有效地覆盖元注释。

13.4、语句中的占位符解析

过去,元素中占位符的值只能根据 JVM 系统属性或环境变量进行解析。现在情况已不再如此。由于 Environment 抽象集成在整个容器中,因此很容易通过它路由占位符的分辨率。这意味着你可以按照自己喜欢的任何方式配置解析过程。你可以更改搜索系统属性和环境变量的优先级,也可以完全删除它们。你还可以根据需要将自己的属性源添加到组合中。

具体而言,无论在何处定义属性,只要 customer 属性在 Environment 中可用,以下语句都有效:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纯纯的小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值