Environment abstraction

允我心安 提交于 2019-12-05 07:32:34

##Environment abstraction (环境抽象) Environment是容器重要的抽象,它集成了应用两个重要的方面:profiles和properties;

一个Profile是一组已命名的有逻辑的bean定义,只有当特定的profile启动时,它们才在容器里注册.Beans可以通过xml或者注解来指定其profile值.Environment对象在profiles中的角色决定目前哪个profile会被启动,哪个profile是默认启动.

Properties在所有的应用中都起重要作用,它可以从以下资源中产生:properties文件,JVM系统属性,系统环境变量,JNDI,servlet上下文参数,特定的Properties对象,Maps,等.Environment对象与属性的关系是提供使用者一个合适的服务接口来配置属性资源,并从中释放属性. ###7.13.1 Bean definition profiles bean定义中的Profiles

bean定义的profiles是一个核心容器的机制,允许在不同的环境里注册不同的bean.这个单词"environment"对不同的用户代表不同的事物,这个功能在很多场景下给你提供帮助,包括:

  • 开发中使用内存数据库 VS 在QA或production中使用从JNDI中查找相同的数据源.
  • 在一个复杂的环境部署项目如何注册监控组件.
  • 部署时,对消费者A或B注册不同的beans的自定义实现

让我讨论第一个例子要求一个特定的数据源.在一个测试环境中,这个配置可能如下:

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

现在让我们思考一下如何在一个QA或生产环境部署应用,说明这个应用的数据源将会注册到生产应用服务器的JNDI目录里.我们的数据源可能如下:

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

难点在于如何根据当前环境来使用这两种Bean.长久以来,pring用户已经有很多方式来实现它,一般而言是一大堆系统环境变量和包含了${placeholder}令牌的xml的 <import/>元素,这样就可以根据环境变量的值来释放正确的配置文件路径.Bean定义的profile是核心容器的功能,可以为该问题提供解决方案.

如果我们概括上面的而特定环境bean定义的使用情况,我们最终需要在特定的上下文中使注册特定的bean定义,而不是其他的.你可以说你要在A环境中注册一个bean定义的profile,并在B环境中注册另一个profile.让我看看如何更新我们的配置来反应这个需求.

####@Profile Profile注解允许你表明一个逐渐有资格在一个或多个特定的profile下被启动.使用上面的例子,我们可以如下重写dataSource配置:

@Configuration
@Profile("dev")
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");
    }
}

像以前提到的,你可以使用编程的JNDI查找:可以使用JndiTemplate/JndiLocatorDelegate 帮助者或直接使用上文中展示的JNDI InitialContext,但不要使用JndiObjectFactoryBean,因为它强制要求你将返回类型设置为FactoryBean.

@Profile可以在创建一个自定义组合注解时作为元注解使用.下面的例子就是定义一个@Production注解可以用来替代 @Profile("production");

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

@Profile可以在方法级别上声明,可以只引用配置类中的几个特定的bean:

@Configuration
public class AppConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        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
    @Profile("production")
    public DataSource productionDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

如果一个@Configuration类标志为@Profile,这个类中的所有的@Bean方法和@Import注解都会被绕过除非一个或多个特定的profile启动.如果一个@Component或@Configuration类被标志为@Profile({"p1","p2"}),那么除非是'p1'或'p2'已经激活,否则这个类不会被注册或处理.如果一个特定profile加上了非操作符(!),那么只要这个特定的profile不激活,该注解下的bean都会被注册.例如,特定的@Profile({"p1","p2"}),当profile'p1'激活或profile'p2'未激活时都会发生注册.

###7.13.2 XML bean definition profiles

在xml部分中profile是<Beans>元素的属性.上面的简单配置可以用以下xml文件重写:

<beans profile="dev"
    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="production"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</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="dev">
        <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已经改造,当出现一个以上的<beans>元素时,是允许这么构造的.这可以在避免xml文件的混乱性并提高其灵活性; ####Activing a profile 启动一个profille 现在我们已经更新了配置,我们只需要确定spring中哪个profile要启动.如果我们现在启动我们的实例应用,我们会发现一个NoSuchBeanDefinitionException的异常抛出,因为容器无法找到名为dataSource的bean. 可以用好几种方法启动一个profile,最直接的方式就是通过一个ApplicationContext得到的Environment对象的API方法来动态实现:

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

另外,profiles可以通过spring.profiles.active属性来申明式启动,它可以通过环境变量,JVM系统属性,web.xml里的servlet上下文参数,或者JNDI里的一个键值.在集成测试里,可以通过@ActiveProfiles来显示的设置一个spring-test模块的激活的profile.

记住,配置文件不是一个是或不是的命题;它可以一次启动多个混合profiles.编程式的,简单的向setActiveProfiles()方法提供混合profile的名称,该方法可以接受任意string值.

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

声明式的,spring.profiles.active也可以接受一个profile名称的标点分割集合.

spring.profiles.active="profile1,profile2"

####Default profile 默认profile default profile表示这个profile会被默认启动.思考如下:

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

如果没有一个profile被启动,那么上面那个dataSource会被创建.这被认为是提供一个或多个bean的默认定义的方式.如果有任意一个profile是可用的,那么默认的profile将不会被启用.

默认profile的名字可以使用setDefaultProfiles()来替换,或者直接使用spring.profiles.default属性来申明.

##@7.13.3 PropertySource abstraction 属性资源抽象 spring的环境抽象通过属性资源的配置结构提供了搜索操作.要详细解释,思考以下例子:

ApplicationContext ctx=new GenericAppliactionContext();
Environment env =ctx.getEnvironment();
boolean containsFoo=env.containsProperty("foo");
System.out.println("does my environment contain the  'foo' property"+ containsFoo);

在上面的片段中,我们可以看到向spring询问当前环境是否定义foo属性的更高明的方式.要回答这个问题,Environment对象在一堆PropertySource对象里执行查询.一个propertySource是所有键值对的简单抽象,并且spring的StandardEnvironment是由两个PropertySource对象决定的,一个是JVM系统属性(如 System.getProperties()),另一个是系统环境变量(例如 System.getenv()).

对StandardEnvironment展现的这些属性资源,可以用于单独的应用中.StandardServletEnvironment可以操作其他额外的默认属性资源,包括servlet配置和servlet上下文参数.StandardPortletEnvironment同样可以向访问配置资源一样访问portlet配置和portlet上下文参数.二者都可以访问JndiPropertySource.查看文档细节.

具体而言,当使用StandardEnvironment,如果一个foo的系统属性或foo 的 环境变量在运行时存在那么调用env.containsProperty("foo")将返回true;

这个搜索动作有层次性.一般,系统属性的优先级高于环境变量,所以当foo属性在二者都已设置且调用env.getProperty("foo")方法时,这个系统变量值将会获胜并在返回是覆盖掉环境变量.记住属性值不会被合并,只会更加优先级来重写.

对于一个普通的StandardServletEnvironment来说,以下所有的层次都会查找,高优先级的实体在上面:

  1. ServletConfig parameters (if applicable,e.g. in case of a DispatcherServlet context)
  2. ServletContext parameters(web.xml context-param entries)
  3. JNDI environment variables("java:comp/env/" entries)
  4. JVM system properties ("-D" command-line arguments)
  5. JVM system environment(operating system environment varibales)

最重要的是,整个机制是可配置的.或许你有个自定义属性资源,你打算集成到搜索中.没有任何问题,简单的实现并实例化你自己的PropertySource,并将它加入当前Environment的PropertySource集合中.

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

在上面的代码中,MyPropertySource被加入为搜索中的更高优先级.如果他含有一个foo属性,它将会被检测并在其他PropertySource之前返回.这个MutablePropertySource API暴露了大量的方法允许对属性资源进行操作.

###7.13.4 @PropertySource 属性资源

属性资源注解提供了一个适合的,显示的机制,它可以吧一个PropertySource添加到spring的Environment中.

给定一个文件"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的异常.

###7.13.5 申明中的占位符解决方案 曾经,在元素中的占位符的值只能通过JVM系统属性或环境变量转化.现在不同了.因为环境抽象已经整合到容器里,这很容易通过它来解决占位符.这意味着你可以以你喜欢的方式配置解决方案:通过系统环境和环境变量的搜索优先级修改或者移除他们;向混合资源适当的添加你自己的属性资源;

具体的,只要customer在Environment中可以获得,那么下面的在申明使用了customer属性的地方当然就会工作.

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!