前军教程网

中小站长与DIV+CSS网页布局开发技术人员的首选CSS学习平台

SB实战19-Spring Boot的外部配置(spring boot支持将配置文件外置到包外)

上篇我们学习了《SB实战18-Spring Boot的应用配置》,本篇我们学习Spring Boot的外部配置。

3.3 外部配置

Spring Boot可以从命令行、环境变量、properties文件、YAML文件等外部获得配置,而这个能力是Environment提供给我们的。我们可以通过三种方式来访问Environment中的属性:

  • 使用@Value注解,我们在上一章已经演示过;
  • 注入Environment的Bean,我们在上一章也演示过;
  • 通过@ConfigurationProperties注解来访问,这节我们会讲解。

为了更深入的理解外部配置的原理,我们首先需要了解Environment抽象。

3.3.1 外部配置源与Environment

我们在上一章了解到,Environment包含两部分的内容:ProfilePropertyEnvironment的定义如下:

public interface Environment extends PropertyResolver {
   String[] getActiveProfiles();
   String[] getDefaultProfiles();
   boolean acceptsProfiles(Profiles profiles);
}

Environment的三个接口方法负责Profile相关内容,而它继承的PropertyResolver接口负责的是对Property的查询。

public interface PropertyResolver {
   boolean containsProperty(String key);
   @Nullable
   String getProperty(String key);
   String getProperty(String key, String defaultValue);
   @Nullable
   <T> T getProperty(String key, Class<T> targetType);
   <T> T getProperty(String key, Class<T> targetType, T defaultValue);
   String getRequiredProperty(String key) throws IllegalStateException;
   <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;
   String resolvePlaceholders(String text);
   String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}

Environment中每一个配置属性都是PropertySource,多个PropertySource可聚集成PropertySources

PropertyResolver的实现类PropertySourcesPropertyResolver负责对PropertySources进行查询操作,即Environment可对PropertySources进行查询操作。

Spring不支持YAML文件作为PropertySource,Spring Boot使用YamlPropertySourceLoader来读取YAML文件并获得PropertySource

在Spring Boot下外部配置属性的加载顺序优先级如下,先列的属性配置优先级高,先列的配置属性可覆盖后列的配置属性。

  • 命令行参数
  • SPRING_APPLICATION_JSON
  • ServletConfig 初始化参数
  • ServletContext 初始化参数
  • JNDI (java:comp/env
  • Java系统属性(System.getProperties()
  • 操作系统变量
  • RandomValuePropertySource 随机值
  • 应用部署jar包外部的application-{profile}.properties/yml
  • 应用部署jar包内部的application-{profile}.properties/yml
  • 应用部署jar包外部的application.properties/yml
  • 应用部署jar包内部的application.properties/yml
  • @PropertySource
  • SpringApplication.setDefaultProperties

3.3.1.1 命令行参数

  • 使用gradle命令行传参:
$ ./gradlew bootRun --args='--server.port=8888 --server.ip=192.168.1.5'
  • 若打成jar包传参:
$ ./gradlew bootJar
$ java -jar build/libs/spring-boot-in-depth-0.0.1-SNAPSHOT.jar --server.port=8888 --server.ip=192.168.1.5
  • 在IntellJ IDEA里:
  • 代码校验结果:
@Value("${server.ip}")
private String serverIp;//1

@Bean
CommandLineRunner commandLineRunner(@Value("${server.port}") String serverPort ){//2
   return args -> {
      Stream.of(args).forEach(System.out::println); //3
      System.out.println(serverPort);
      System.out.println(serverIp);
   };
}
  1. 使用@Value注入值到类的变量中,server.ip不是Spring Boot内置配置,可接受自用;
  2. 使用@Value注入值到方法参数中,server.port是Spring Boot内置配置,会对应用配置起效,更改当前容器的端口号;
  3. 可从args参数中获取参数;

以上三种方式的运行结果均为:

3.3.1.2 SPRING_APPLICATION_JSON

  • 作为系统环境变量
$ SPRING_APPLICATION_JSON='{"server":{"ip":"192.168.1.5","port":"8888"}}' java -jar build/libs/spring-boot-in-depth-0.0.1-SNAPSHOT.jar
  • 使用系统属性
$ java -Dspring.application.json='{"server":{"ip":"192.168.1.5","port":"8888"}}' -jar build/libs/spring-boot-in-depth-0.0.1-SNAPSHOT.jar
  • 使用命令行参数执行:
$ java -jar build/libs/spring-boot-in-depth-0.0.1-SNAPSHOT.jar --spring.application.json='{"server":{"ip":"192.168.1.5","port":"8888"}}' 
  • 使用Intellij IDEA

3.3.1.5 RandomValuePropertySource随机值

RandomValuePropertySource为我们产生随机值,如:

$ ./gradlew bootRun --args='--server.port=${random.int[1024,10000]} --server.ip=192.168.1.5 --some.value=${random.value} --some.number=${random.int}'

使用下面代码验证:

@Value("${server.ip}")
private String serverIp;

@Value("${some.value}")
private String someValue;

@Value("${some.number}")
private String someNumber;

@Bean
CommandLineRunner commandLineRunner(@Value("${server.port}") String serverPort){
   return args -> {
      Stream.of(args).forEach(System.out::println);
      System.out.println(serverPort);
      System.out.println(serverIp);
      System.out.println(someValue);
      System.out.println(someNumber);
   };
}

执行刚开始的命令,控制台显示:

3.3.2 外部文件配置

Spring Boot会从以下位置加载外部配置文件application.properties/yml并读取成PropertySouces加载到Environment

  • 入口类的当前目录的/config子目录;
  • 入口类的当前目录;
  • 类路径下的/config目录;
  • 类路径的根目录。

Spring Boot给我们提供了大量的配置属性,通过设置这些配置属性达到对当前应用的配置,我们可以在resources/application.properties文件中配置:

spring.main.banner-mode=off
spring.main.lazy-initialization=true
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
server.port=1234

这些属性设置都会被读取到Environment,给应用的运行行为进行配置。Spring Boot提供的所有的属性配置参考:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#common-application-properties

3.3.2.1 YAML文件配置

在现代的云计算的环境里,很多配置都是基于YAML文件的,尽管Spring Boot支持基于properties和YAML文件的配置,我们在本书将全部使用YAML文件进行配置。

YAML是JSON格式的超级,对层级配置有极大的便利,我们比较一下properties和YAML的区别:

application.properties

server.port=1234
server.address=192.168.31.199

application.yml

server:
  port: 8888
  address: 192.168.31.199

从上面可以看出,层级结构下,我们不用写两个server,如果层级越多的配置,我们的配置的层次会更清晰。注意“:”后有一个空格,这是YAML要求的格式。

当我们的配置层级性不是很强的时候,我们在YAML文件里仍可以类似于properties文件那样配置,如:

server.port: 8888
spring.main.banner-mode: off

现在我们新建一个application.yml文件,后面的演示都将在这里编写。

3.3.2.2 占位符

配置文件可以从Environment中读取已定义的配置。

app:
  name: spring boot in depth
  desc: Chapter:${app.name} is hard to learn

我们用下列代码检查结果:

@Bean
CommandLineRunner placeholderClr(@Value("${app.name}") String name,
                         @Value("${app.desc}") String desc){
   return args -> {
      System.out.println(name);
      System.out.println(desc);
   };
}

3.3.2.3 类型安全的配置属性

我们前面获得配置属性都是通过使用@Value,Spring Boot提供的了大量的配置属性,Spring Boot自己是怎么获得并使用的呢?如server.port=1234,Servlet容器启动时端口会被修改成了1234,这是怎么做到的?我们讲“Spring Boot的魔法”的时候讲了“加载自动配置”和“如何自动配置”,在自动配置时Spring Boot给我们开放了一个口子去修改默认的自动配置:一个强类型的Bean *Properties和外部配置文件中的内容进行映射绑定,自动配置通过使用*Properties来配置应用的行为,如我们前面用到的server.port的配置是在ServerProperties绑定的:

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
   private Integer port;
   private InetAddress address;
  ...
}

那这个绑定是怎么实现的呢?很显然是通过@ConfigurationProperties来实现的。这个注解的功能是由一个BeanPostProcessor(ConfigurationPropertiesBindingPostProcessor)提供的,前提是*Properties是一个Bean,我们可以通过两种方式让他成为Bean。

在Spring Boot 2.2之前:

  • *Properties上注解@Component让它成为Bean,我们在配置使用的地方直接像常规Bean一样注入;
  • ServerProperties并没有通过注解标记成Bean,而是在使用ServerProperties的地方通过@EnableConfigurationProperties({*Properties.class})来动态注册Bean。

在Spring Boot 2.2:

@SpringBootApplication注解组合了@ConfigurationPropertiesScan注解,它使用ConfigurationPropertiesScanRegistrar为我们动态的扫描注册标注了在我们入口类包及其下级包中注解了@ConfigurationProperties的类,并将其注册成Bean。我们也可以通过:

@SpringBootApplication
@ConfigurationPropertiesScan({ "com.some.other", "top.wisely" })
public class SpringBootInDepthApplication {}

来覆盖默认的扫描。

下面我们做一个简单的例子来演示类型安全的配置属性的使用:

  • 属性类:
@Getter
@Setter
@ConfigurationProperties(prefix = "author")
public class AuthorProperties {
    private String name = "wyf";
    private Integer age = 35;
    private String motherTongue;
    private String secondLanguage;
    private String graduatedUniversity;
    private Integer graduationYear;
    private Address address = new Address();
    private List<Book> books = new ArrayList<>();
    private Map<String, String> remarks = new HashMap<>();

    @Getter
    @Setter
    public static class Address {
        private String province;
        private String city;
    }

    @Getter
    @Setter
    public static class Book{
        private String name;
        private Integer price;
    }
}

当前例子分别有普通属性、对象、List、和Map,可作为有代表性的配置。

  • 配置类
@Configuration
//已自动扫描注册AuthorProperties
//无需使用@EnableConfigurationProperties({AuthorProperties.class})
public class AuthorConfiguration {

    private final AuthorProperties authorProperties;

    public AuthorConfiguration(AuthorProperties authorProperties) {
        this.authorProperties = authorProperties;
    }

    @Bean
    public String strBean(){
        String str = authorProperties.getName() + "/"
                + authorProperties.getAge() + "/"
                + authorProperties.getMotherTongue() + "/"
                + authorProperties.getSecondLanguage() + "/"
                + authorProperties.getGraduatedUniversity() + "/"
                + authorProperties.getGraduationYear() + "/"
                + authorProperties.getAddress().getProvince() + "/"
                + authorProperties.getAddress().getCity() + "/";
        System.out.println(str);
        authorProperties.getBooks().forEach(book -> {
            System.out.println(book.getName());
            System.out.println(book.getPrice());
        });
        authorProperties.getRemarks().forEach((key,value) ->{
            System.out.println(key + ":" +value);
        });
        return str;
    }
}
  • 如何配置为了我们在application.yml输入配置的时候会自动提示,我们需要添加annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'build.gradle,IntelliJ IDEA需要开启注解处理器:


此时Spring Boot为我们生成了
build/classes/java/main/META-INF/spring-configuration-metadata.json文件,IDE会根据这个文件给我们自动提示:


我们的配置内容如下:

author:
  name: foo
  age: 40
  mother-tongue: Chinese # 1 烤串式
  second_language: English # 2 下划线式
  graduationYear: 2006 # 3 驼峰式
  GRADUATED_UNIVERSITY: WHUT # 4 大写字母
  address:
    province: Anhui
    city: Hefei
  books:
    - name: book1
      price: 89
    - name: book2
      price: 109
  remarks:
    hobby: reading
    some: value

Spring Boot提供了一种叫“松散绑定”的技术,我们的属性可以是烤串式、下划线式、驼峰式或者是大写字母。

运行的结果如下(注意前面我们的全局延迟加载不能打开spring.main.lazy-initialization=false):

3.3.3 Profile

3.3.3.1 单文件Profile

我们可以在一个application.yml内定义多个Profile,每个Profile用“”隔开,通过spring.profiles给每个Profile命名,每个Profile还可以通过spring.profiles.include来包含组合其他的Profile,最终通过spring.profiles.active来指定激活的Profile,如:

spring:
  profiles:
    active: prod #指定激活的Profile是prod

---
spring.profiles: prod # Profile的名为prod
spring.profiles.include: # 组合prod-port和prod-lazy两个Profile
  - prod-port
  - prod-lazy

---
spring:
  profiles: prod-port # Profile的名为prod-port
server:
  port: 8888

---
spring:
  profiles: dev-port # Profile的名为dev-port
server:
  port: 8080

---
spring:
  profiles: prod-lazy # Profile的名为prod-lazy
  main:
    lazy-initialization: true

启动时会执行端口号是8888、全局延迟加载的设置。我们也可以通过设置多个Profile获得相同的结果:

spring:
  profiles:
    active:
      - prod-port
      - pord-lazy

3.3.3.2 多文件Profile

我们还可以将上面的Profile拆到多个文件名称,即application-{profile}.yml,上面的可以拆成:

  • application-prod.yml
spring.profiles.include:
  - prod-port
  - prod-lazy
  • application-prod-port.yml
server:
  port: 8888
  • application-prod-lazy.yml
spring:
  main:
    lazy-initialization: true
  • application-dev-port.yml
server:
  port: 8080

我们最终同样需要在application.yml中指定要激活的Profile:

spring:
  profiles:
    active: prod

3.3.4 EnvironmentPostProcessor

我们在上一章对Bean行为的定制可以用BeanPostProcessor,对配置元数据的定制使用BeanFactoryPostProcessor,Spring Boot给我们提供了对EnvironmentSpringApplication进行定制的EnvironmentPostProcessor

我们自定义一个EnvironmentPostProcessor实现类:

public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor { // 1 
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { // 2
        Map<String, Object> map = new HashMap<>();
        map.put("key1","value1");
        map.put("key2","value2");
        PropertySource propertySource = new MapPropertySource("map",map); //3
        environment.getPropertySources().addLast(propertySource); //4
         .setBannerMode(Banner.Mode.OFF); //5
    }
}
  1. 实现EnvironmentPostProcessor接口;
  2. 重载postProcessEnvironment,它的入参让我们可以对EnvironmentSpringApplication进行设置;
  3. 新建一个PropertySource类型为MapPropertySource,将map的值设置给PropertySource
  4. propertySource添加到Environment中;
  5. 同样在此处可以设置SpringApplication

我们同样可以通过在本应用的META-INF/spring.factories启用该处理:

org.springframework.boot.env.EnvironmentPostProcessor=\
top.wisely.springbootindepth.processor.MyEnvironmentPostProcessor

我们再校验运行结果:

@Bean
CommandLineRunner environmentPostProcessorClr(@Value("${key1}") String value1,
                                   						@Value("${key2}") String value2){
   return args -> {
      System.out.println(value1);
      System.out.println(value2);
   };
}

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言