配置外化

应用开发中,开发者会将诸如数据库配置信息,NFS服务器的地址,消息队列的大小等等信息保存到配置文件中。比如Java Web中的application.properties文件,Rails中的database.yml等。这样我们可以在不同的环境中方便切换,只需要修改几行配置信息,应用的代码则完全不用修改。

下面是一个Rails应用的数据库配置文件:

test:
  adapter: sqlite3
  database: db/test.sqlite3
  pool: 5
  timeout: 5000

production:
  adapter: mysql2
  database: test-db
  pool: 5
  username: root
  password: s3cr3t
  socket: /tmp/mysql.sock

这个yml定义了test环境,数据库使用sqlite3,数据库文件为db/development.sqlite3。而在production环境,数据库采用mysql

在运行时,只需要指定环境变量,即可切换数据库:

$ RAILS_ENV=test rails server

实例

我们以一个简单的Java应用来演示如何将应用程序的配置信息。在这个应用程序中,我们需要数据库配置可以在运行是改变,而不是将配置内置在应用程序中。

在实际场景中,应用程序可能在部署时才知道要连接的数据库地址是什么,而且数据库的名称,数据库连接池的大小等信息都可能因环境而变化。

在这个应用程序中,我们将在应用程序中连接mongodb数据库,从数据库中读取一个集合的内容,然后打印出整个集合。我们会使用soringspring-data-mongodb来完成简化应用的编写。

我们使用gradleinit命令来生成一个典型的Java应用程序:

$ mkdir -p spring-mongo-demo
$ cd spring-mongo-demo
$ gradle init --type=java-library

另外我们为应用程序添加依赖:

apply plugin: 'java'
apply plugin: 'idea'

buildscript {
    repositories {
        jcenter()
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.13'
    compile 'org.springframework:spring-context:4.2.4.RELEASE'
    compile 'org.springframework.data:spring-data-mongodb:1.8.4.RELEASE'
}

这样,只需要执行

$ gradle build

就可以下载所有依赖库了。

配置文件

我们首先来为应用程序创建这样的包接口:

src
├── main
│   ├── java
│   │   └── com
│   │       └── thoughtworks
│   │           └── mongo
│   │               ├── config
│   │               ├── model
│   │               └── repo
│   └── resources
└── test
    └── java

要做到在运行时可改变配置,我们首先需要保证配置和代码分离。这个步骤很容易实现,只需要将配置定义在配置文件中,然后应用在启动时读取该配置(application.properties)即可:

mongo.host=localhost
mongo.database=test

根据惯例,application.properties会放在resources目录下。

在我们这个应用中,数据库中又一个people的集合,对应的我们需要又一个Person的实体,和一个用来访问数据库集合的PersonRepository

package com.thoughtworks.mongo.model;

import org.springframework.data.annotation.Id;

public class Person {
    @Id
    private String id;

    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //getter & setter

    @Override
    public String toString() {
        return "{name="+name+", age="+age+"}";
    }
}

spring-data-mongo提供了一个MongoRepository接口,我们的应用只需要继承这个接口,就可以免费获得很多有用的数据库访问功能。

package com.thoughtworks.mongo.repo;

import com.thoughtworks.mongo.model.Person;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PersonRepository extends MongoRepository<Person, String> {
}

借助spring强大的注入器,我们很容易在应用中使用这个接口,而不用关心背后它是如何被实例化的:

public class Application {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        PersonRepository personRepository = context.getBean(PersonRepository.class);

        List<Person> all = personRepository.findAll();
        System.err.println(all);
    }

}

我们首先创建一个基于注解的Context,具体的应用配置我们放在了AppConfig类中,有了这个Context,我们可以从中获取PersonRepository的实例,并使用它的findAll方法来获取所有的人员列表。

对于我们的应用来说,所有的配置信息都放在AppConfig中。我们来看看这个类:

package com.thoughtworks.mongo.config;

import com.mongodb.MongoClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;

@Configuration
@ComponentScan(value = "com.thoughtworks.mongo.*")
@PropertySource(value = "classpath:application.properties")
public class AppConfig {
    @Autowired
    private Environment environment;

    @Bean
    public MongoDbFactory mongoDbFactory() throws Exception {
        String mongoHost = environment.getProperty("mongo.host");
        String mongoDatabase = environment.getProperty("mongo.database");

        return new SimpleMongoDbFactory(new MongoClient(mongoHost), mongoDatabase);
    }

    @Bean
    public MongoTemplate mongoTemplate() throws Exception {
        return new MongoTemplate(mongoDbFactory());
    }

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

}

这个类中,我们使用了PropertySource这个注解,并指定了配置文件应该从classpath中的application.properties中获取。

当我们执行应用时,配置文件会生效,一起正常!下来我们来构建一个可以独立发布的jar包,这样任何人都可以直接使用这个jar包,而不需要自己重新下载依赖,重新构建等,所以我们为build.gradle添加几条简单的命令:

jar {
    baseName = rootProject.name
    version =  '0.1.0'

    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }

    manifest {
        attributes("Main-Class": "com.thoughtworks.mongo.Application")
    }
}

这样构建出来的包就会包含所有依赖,我们还显式的指定了该jar包里的Main-Classcom.thoughtworks.mongo.Application

$ gradle build
$ java -jar build/libs/spring-mongo-demo-0.1.0.jar

会得到

[{name=Juntao, age=30}, {name=Abruzzi, age=28}]

很好,我们成功的访问了数据库,并打印出了集合中的所有元素,而且这个应用程序是可以独立发布的了。

系统配置文件

如果仔细想想使用场景,你会发现如果数据库连接发生变化了,我们修改application.properties文件,而该文件是打包在jar包内的!

这意味这应用程序和它的环境并没有分离,简单来说,我们需要提供机制来让外部的配置可以覆盖包内的配置。

最简单的方式下,我们创建一个新的properties文件,并让应用程序最后使用这个配置,这样就可以达到覆盖的目的了。当然,如果外部没有提供properties文件,应用还可以使用内部的配置提供功能。

spring在版本4之后,提供了比PropertySource更强大的注解PropertySources,它支持定义多个配置源,并形成一个链表,这样后边的元素就可以覆盖前面的元素了。

@Configuration
@ComponentScan(value = "com.thoughtworks.mongo.*")
@PropertySources({
        @PropertySource(value = "classpath:application.properties"),
        @PropertySource(value = "file:/etc/spring-mongo/application.properties", ignoreResourceNotFound=true)
})
public class AppConfig {
    //...
}

我们定义了两个配置源,一个是application.properties,另一个是绝对路径下/etc/spring-demo/下的同名文件。ignoreResourceNotFound=true保证如果找不到该配置,也不会报错。

这样,如果我们需要新的数据库连接/配置,只需要在/etc/spring-demo下创建同名文件,并设置新的值即可。

这样做的好处是,应用程序无需做任何修改,配置信息外化到了环境中,部署应用程序的环境来确定应用具体如何与外部依赖交互。

环境变量

另一种常用的方式是使用环境变量,这种方式下,我们只需要修改启动脚本,就可以将信息传递给应用程序,这中方式在UNIX世界已经存在多年。

spring提供了Environment对象,该对象提供了对环境的封装,其中包含了环境应用了那些profile的,系统配置,操作系统环境变量,以及所有的配置源propertySources

{activeProfiles=[], defaultProfiles=[default], propertySources=[systemProperties,systemEnvironment,URL [file:/etc/spring-mongo/application.properties],class path resource [application.properties]]}

这样,我们的代码中使用的

@Autowired
private Environment environment;

@Bean
public MongoDbFactory mongoDbFactory() throws Exception {
    String mongoHost = environment.getProperty("mongo.host");
    String mongoDatabase = environment.getProperty("mongo.database");

    return new SimpleMongoDbFactory(new MongoClient(mongoHost), mongoDatabase);
}

都自然的可以从Java环境变量中获得配置信息:

java -Dmongo.host=10.29.10.212 -Dmongo.database=prod-db -jar build/libs/spring-mongo-demo-0.1.0.jar

这样,配置信息就通过外部传入。这里仅仅是一个很简单的例子,项目中的配置信息会非常多,而且可能会有多种方式混用的场景:部分信息放在系统的环境变量中,如/etc/profile或者.bashrc中,另外一部分信息则存储在应用特定的properties中。

总体而言,这些配置信息都需要和应用程序本身分离。这和持续交付的实践其实也是相关的,在持续交付中,我们只会生成一个二进制包。这个二进制包在部署流水线上一直使用,在回归测试、性能测试等任务中,这个包会被部署在不同的环境中,并被测试有效性。这样我们才对应用程序自身有更多的信心。

results matching ""

    No results matching ""