spring

时间秒杀一切 提交于 2020-04-08 12:42:40

程序一般都会有开发环境、测试环境以及线上环境,这些环境下程序运行依赖的基础一般不同,例如在有数据源访问的程序中,开发时可能使用了嵌入式数据库,而到测试环境上会使用独立的mysql,正式上线后的又会不同,不止数据源连接信息是与环境相关的,很多其他程序依赖的属性值也是与环境相关的。编写程序时最好要能够灵活处理不同环境的差异,我们希望如果程序运行在开发机中,则自动启用嵌入式数据库的配置值,如果在生产环境中运行则会自动适用线上的数据库连接配置值。在公司先前老的项目中,没有做好环境可变信息的分离,导致每一次在不同环境上部署程序时都要手工修改一遍程序的配置,非常难受,并且凡事手工操作就意味着效率低易出错。

目前很多java项目恐怕都要使用Spring来开发,并且Spring也支持对环境依赖相关的信息灵活管理的能力,因此我们应该把类似数据源连接这类配置交给Spring来管理。

Environment

Environment是在Spring3.0引入的一个抽象组件,Environment能够灵活的让某种条件匹配后才注册bean到IoC容器中,Environment还能够统一的管理底层的属性资源,即Environment封装的就是两方面的内容:profiles和properties。

对于profile,可以理解为profile关联了一组bean的定义,这组bean只有当这个profile满足条件时才会注册到IoC容器中,profile有一个名字,通常名字会体现当前的环境,例如development,test或者production等。

property表示的是“key-value”形式的属性值,属性是大多数程序都会使用的一种配置形式,这里的属性可以来源于多种地方,例如properties文件,jvm系统属性,操作系统环境属性,jndi属性,servlet context参数,也可以直接是内存中的map结构等,Environment对象会管理这些属性信息。

定义profile

定义一个profile其实就是声明一个名字来关联一组bean的定义,当这个定义的profile被处于活动状态时,这一组bean才会注册到IoC容器中。例如对于涉及到数据源访问的程序,程序运行在开发、测试、线上等不同环境中的数据源连接信息是不同的,这种情况下我们可以定义三个DataSource对象,分别用不同的profile来关联,下面的配置就是定义DataSource的示例:

@Configuration
public class DataSourceConfig {
    @Profile("development")
    @Bean(destroyMethod = "close")
    public DataSource devDataSource() {
		// 开发使用一个内嵌式数据库
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("init-schema.sql")
                .addScript("init-data.sql")
                .build();
    }

    @Profile("test")
    @Bean(destroyMethod = "close")
    public DataSource testDataSource() {
        BasicDataSource ds = new BasicDataSource();
        // 测试环境使用了mysql
        ds.setUrl("jdbc:mysql://test-db-host:port/test-schema");
        ds.setUsername("root");
        ds.setPassword("root");
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return ds;
    }
	
    @Profile("production")
    @Bean(destroyMethod = "close")
    public DataSource prodDataSource() throws NamingException {
        Context context = new InitialContext();
        return (DataSource) context.lookup("java:comp/env/jdbc/datasource");
    }
}

使用@Profile标注来定义一个profile,上述配置代码分别为开发、测试和线上环境定义了不同的profile,profile的名字也分别反映出了环境。与上述配置等价的xml配置内容如下:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
  
  <!-- 其他bean的定义 -->

  <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="test">
  	<bean id="dataSoutce" class="org.apache.commons.dbcp2.BasicDataSource" 
          destroy-method="close">
    	<property name="url" value="jdbc:mysql://test-db-host:port/test-schema"/>
      <property name="username" value="root"/>
      <property name="password" value="root"/>
      <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    </bean>
  </beans>
  
  <besns profile="production">
    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
  </besns>
</beans>

激活profile

我们想要的效果是应用程序如果运行在开发机器上,那么程序中注入的DataSource对象应该是“development”这个profile中定义的DataSource,同理如果运行在测试环境或者生产环境,那么IoC容器中注册的DataSource对象分别应该是“test”或者“production”这个profile中的定义的DataSource,那么Spring如何知道程序此时运行在那种环境下呢,这还是需要我们明确告诉它,然后Environment能够通过约定好的规则获得当前激活的profile列表,Environment有如下查询激活profile列表的方法:

public interface Environment extends PropertyResolver {
  // 获取当前环境中激活的profiles
  String[] getActiveProfiles();
  // 获取当前环境中的默认profiles,默认的profile总是激活的
  String[] getDefaultProfiles();

  // 确定指定的profile是否是明确指定激活的,或者是默认profile
  boolean acceptsProfiles(String... profiles);
}

Environment还有一个子接口ConfigurableEnvironment,其中提供了激活profile的几个重要方法:

public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver {
  // 在当前环境中指定需要激活的profile列表;
  // 如果此方法参数传递一个空的profile列表,则会清除已经设置激活的所有profile,如果已经存在
  // 一个激活列表,则可以用下面addActiveProfile方法在列表中增加一个profile
  void setActiveProfiles(String... profiles);
  
  void addActiveProfile(String profile);

  // 设置默认profile列表,如果没有其他指明的profile,则默认profile将会激活
  void setDefaultProfiles(String... profiles);
}

现在我们就可以调用这些方法来激活我们期望的profile,ApplicationContext支持获取其关联Environment,我们可以先获得IoC容器的环境对象,下面是一个示例:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
Environment env = ctx.getEnvironment();
env.setActiveProfiles("production");
ctx.register(DataSourceConfig.class, OtherConfigurationClasses.class);
ctx.refresh();

在实际的javaee项目中,我们一般不会自己创建ApplicationContext对象,因此通过Java编程的方式激活profile列表不常用,而应该配置spring.profiles.active属性来激活profile,这个属性是Spring内部定义好的,不要更改为其他名字,我们可以把它配置为jvm系统属性,或者操作系统环境变量,或者web descriptor中的ServletContext参数,或者jndi资源都可以,例如在jvm系统属性中配置它:

java AppMain -Dspring.profiles.active=production

默认profile

默认profile的含义是如果当前环境中没有明确激活的profile,则程序中配置的默认profile列表被自动激活,如果有其他激活的profile,则默认profile不会被激活,下面是定义一个默认profile的方式:

@Configuration
public class DataSourceConfig {
    // 使用名字default
    @Profile("default")
    @Bean(destroyMethod = "close")
    public DataSource devDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("init-schema.sql")
                .addScript("init-data.sql")
                .build();
    }
}

可以看到我们取名为default的profile就成为默认profile,如果要更改Spring的默认profile的默认名字,可以通过ConfigurableEnvironmentsetDefaultProfiles(...)方法修改,或者通过配置spring.profiles.default属性修改。

java AppMain -Dspring.profiles.default=default

PropertySource抽象

统一管理属性资源是Environment的另一个重要的能力,属性对于几乎所有程序来说都是非常重要的一部分,其实我们需要使用profile的场景往往是由于程序在不同环境下运行时依赖的属性的值不相同。

PropertySource抽象的就是“key-value”形式的属性,前面已经说过,PropertySource对应多种实际的底层属性值来源,而Environment提供查询当前环境中的所有已加载的属性的能力,无论属性来源是哪里,Environment的这个能力是通过继承PropertyResolver接口获得的,PropertyResolver能够从底层的property sources层次中查询相关属性值。

查询当前环境中是否有“foo”这个属性:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
String v = evn.getProperty("foo");
System.out.print(v);

一般从ApplicationContext中获取的是一个StandardEnvironment类型的实例,StandardEnvironment中默认已经加载了两种PropertySource属性:jvm属性和操作系统属性。

Environment中搜索属性值时对于不同PropertySource具有不同的优先级,例如一般jvm中的属性就比操作系统环境属性优先级高。

在容器中添加属性

除了Environment默认加载的属性列表,我们可以在Environment对象中添加额外的属性,我们只需要自己实现一个PropertySource,然后通过Environment管理的PropertySources添加它即可,实现代码如下:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
ConfigurableEnvironment env = ctx.getEnvironment();
MutablePropertySources mps = env.getPropertySources();
mps.addFirst(new MyPropertySource());// addFirst添加为最高优先级,相应的还有addLast添加为最低优先级

配置方式添加属性

对于配置的方式,使用@PropertySource这个java标注也可以用来向Environment中添加额外的属性列表。例如我们有一个“db.properties”属性配置文件,内容包含jdbc数据源的连接信息,通过@PropertySource引用这个属性文件后可以直接在配置类中以及程序的其他部分获取属性值:

@Configuration
@PropertySource("classpath:/db.properties") 
// 还可以配置多个@PropertySource
public class PropertySourcesAnnotation {
    @Autowired
    Environment env;

    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource ds = new BasicDataSource();
        ds.setUrl(env.getProperty("url"));
        ds.setUsername(env.getProperty("username "));
        ds.setPassword(env.getProperty("password"));
        ds.setDriverClassName(env.getProperty("driverClassName"));
        return ds;
    }
}

解析${...}占位符

Environment存在之前,程序中使用的属性占位符“${property.placeholder}”只能从jvm属性和操作系统环境变量两个来源解析属性占位符的值,使用Environment后我们可以很灵活的配置其中管理的属性,我们可以增加属性,也可以删除属性,也可以修改属性的优先级,ApplicationContext集成了Environment,因此只要Environment能够查找到相应的属性值,属性占位符就能够得到正确替换。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!