Spring 与配置获取方式

1. 前言

Spring 在配置文件方面的支持非常强大,本文不再赘述,有需求可以查看 Spring Boot 的官方文档。本文的叙述内容是如何在程序中使用 Spring Boot 配置文件中的参数值。常见的手法有三种:

  • @Value 注解

  • org.springframework.core.env.Environment 对象

  • @ConfigurationProperties 注解

2. @Value

先从 @Value 说起,通过配置完整名称的形式即可获取需要的值,如:

@Value("${myConfig.name}")
private String name;

但是需要注意的一点是 @Value 注入的时间一般在 Bean 构造完成之后。如果构造 Bean 的方法需要使用到配置文件里的参数,那么可以把这些参数作为构造函数或者 @Bean 注解修饰的方法的传入参数,并以 @Value 注解来指定注入的参数。如:

@Bean("client1")
public Client getClient(@Value("${endPoint.beijing}") String endPoint) {
    Client client = new Client();
    client.setEndPoint(endPoint);
    return client;
}

3. org.springframework.core.env.Environment

@Value 注解在获取少量配置的时候还是相当方便的,但是如果我们需要从配置文件中获取大量配置的时候往往需要定义大量的实例变量,就不如直接从 Environment 获取来的方便。Environment 对象可以直接通过 @Autowired 注解注入得到。从 Environment 获取配置的方式也相当简单,如:

String myConfigNameOfEnv = environment.getProperty("myConfig.name");

getProperty 方法还有可以指定默认值和参数类型的重载方法,此处不展开。

4. @ConfigurationProperties

通过 @ConfigurationProperties 注解获取配置有两种形式:

下面给出一个示例,对于如下配置:

myConfig:
  name: myConfig
  list:
    - a
    - b
yourConfig:
  name: yourConfig
  list:
    - c
    - d

如果我们想使用 myConfig 前缀下的所有配置,那么两种方案分别可以按照如下形式获取。

方案一:

@Configuration
@ConfigurationProperties(prefix = "myConfig")
@Data
public class MyConfig {
  private String       name;
  private List<String> list;
}

这里通过前缀和字段的名字来映射配置。

方案二:

@Configuration
@ConfigurationProperties(prefix = "")
@Data
public class ConfigAsMap {
  private Map<String, String> myConfig;
  private Map<String, String> yourConfig;
}

这里我们将所有所有配置映射为一个对象,每一个 namespace 下的配置以 Map 的形式来存储。使用的时候可以按照如下方式获取特定配置:

Map<String, String> myConfigMap     = configAsMap.getMyConfig();
String              nameOfConfigMap = myConfigMap.get("name");

这里还要记述一下 Environment 对象和 @ConfigurationProperties 映射为 Map 后两者在获取列表时的区别。

对于 Environemnt 对象,我们要获取 myConfig.list 对象时的方式如下:

List<String> myConfigListOfEnv = Lists.newArrayList(environment.getProperty("myConfig.list[0]"),
                                            environment.getProperty("myConfig.list[1]"));

而通过 @ConfigurationProperties 映射为 Map 后获取的方式则是:

List<String> listOfConfigMap = Lists.newArrayList(myConfigMap.get("list.0"),
                                                  myConfigMap.get("list.1"));

两者在处理列表的索引时不一致,使用时需要我们注意。

5. 跨 Module 引用配置

最近开始接手改造一个项目,改造过程中有这样一个问题:该项目原来所有的配置都采用集中管理的方式,每一个项目的配置都从配置服务器的接口获取然后初始化 Spring 的容器。该项目将从配置服务器获取配置的部分抽出为一个单独的 Maven Module。

但是这个配置集中管理的项目即将下线,于是配置本地化的改造就迎面而来了。

我的改造方案是以不同的 profile 的形式将配置固化到配置 Module 的配置文件中。在线上环境和测试环境通过指定不同的 profile 达到配置的切换。

对于运行时指定的 profile 能否作用到依赖的 Maven 模块并没有绝对把握,而改造的范围比较广,所以还是捏了一把汗的。于是写了一个小的 Demo 作为原型验证。

首先给出配置文件:

application.yaml

myConfig:
  name: myConfig
  list:
    - a
    - b
yourConfig:
  name: yourConfig
  list:
    - c
    - d

application-test.yaml

hisConfig:
  name: hisConfig-test
  list:
    - g
    - h

application-prod.yaml

hisConfig:
  name: hisConfig-prod
  list:
    - e
    - f

两种通过 @ConfigurationProperties 获取配置的方式:

package com.avaloninc.springconfigexample.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
@ConfigurationProperties(prefix = "myConfig")
@Data
public class MyConfig {
  private String       name;
  private List<String> list;
}

以及:

package com.avaloninc.springconfigexample.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

@Configuration
@ConfigurationProperties(prefix = "")
@Data
public class ConfigAsMap {
  private Map<String, String> myConfig;
  private Map<String, String> yourConfig;
  private Map<String, String> hisConfig;
}

先在当前 Module 测试一下:

package com.avaloninc.springconfigexample;

import com.avaloninc.springconfigexample.config.ConfigAsMap;
import com.avaloninc.springconfigexample.config.MyConfig;
import junit.framework.TestCase;
import org.assertj.core.util.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;
import java.util.Map;

@SpringBootTest
@RunWith(SpringRunner.class)
@ActiveProfiles(value = "test")
public class MainTest extends TestCase {

  @Value("${myConfig.name}")
  private String name;
  @Autowired
  private Environment environment;
  @Autowired
  private ConfigAsMap configAsMap;
  @Autowired
  private MyConfig    myConfig;

  @Test
  public void test() {
    System.out.println("this.name = " + this.name);

    String myConfigNameOfEnv = environment.getProperty("myConfig.name");
    List<String> myConfigListOfEnv = Lists.newArrayList(environment.getProperty("myConfig.list[0]"),
                                                        environment.getProperty("myConfig.list[1]"));
    System.out.println("myConfigNameOfEnv = " + myConfigNameOfEnv);
    System.out.println("myConfigListOfEnv = " + myConfigListOfEnv);

    String       myConfigName = myConfig.getName();
    List<String> myConfigList = myConfig.getList();

    Map<String, String> myConfigMap     = configAsMap.getMyConfig();
    String              nameOfConfigMap = myConfigMap.get("name");
    List<String> listOfConfigMap = Lists.newArrayList(myConfigMap.get("list.0"),
                                                      myConfigMap.get("list.1"));

    System.out.println("myConfig = " + myConfig);
    System.out.println("configAsMap = " + configAsMap);

    assertEquals(this.name, myConfigNameOfEnv);
    assertEquals(myConfigNameOfEnv, myConfigName);
    assertEquals(myConfigName, nameOfConfigMap);
    assertEquals(myConfigListOfEnv, myConfigList);
    assertEquals(myConfigList, listOfConfigMap);
  }
}

然后我们在另一个 Module 中引用这里的配置,下面是单元测试:

package com.avaloninc.springtestexample;

import com.avaloninc.springconfigexample.config.ConfigAsMap;
import com.avaloninc.springconfigexample.config.MyConfig;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@Import({ConfigAsMap.class, MyConfig.class})
public class MainTest extends TestCase {

  @Autowired
  private ConfigAsMap configAsMap;
  @Autowired
  private MyConfig  myConfig;

  @Test
  public void test() {
    System.out.println("configAsMap = " + configAsMap);
    assertEquals(configAsMap.getHisConfig().get("name"), "hisConfig-test");
    System.out.println("myConfig = " + myConfig);
  }
}

注意:跨模块引用配置的时候记得使用 @Import 注解,否则启动的时候会报错:org.springframework.beans.factory.UnsatisfiedDependencyException。

发表评论

电子邮件地址不会被公开。 必填项已用*标注