SpringBoot 自定义配置和多环境配置

806 阅读7分钟

在这里插入图片描述

Spring Boot 配置加载的优先级:

1.  命令行参数。
2.  来自 java:comp/env 的 JNDI 属性。
3.  Java 系统属性(System.getProperties())。
4.  操作系统环境变量。
5.  Random ValuePropertySource 配置的 random.\* 属性值。
6.  jar 包外部的 application-{profile}.properties 或者 application-{profile}.yml 配置文件。
7.  jar 包内部的 application-{profile}.properties 或者 application-{profile}.yml 配置文件。
8.  jar 包外部的 application.properties 或者 application.yml 配置文件。
9.  jar 包内部的 application.properties 或者 application.yml 配置文件。
10. @Configuration 注解类上的 @PropertySource11. 通过 SpringApplication.setDefaultProperties 指定的默认属性。

1. 自定义配置

1.1. @Value

二者区别@ConfigurationProperties@Value
功能批量注入配置文件中的属性一个个指定
松散绑定(松散语法)支持不支持
SpEL语法不支持支持
JSR303数据校验支持不支持
复杂类型封装支持不支持

1.1.1 默认值设置

下面第一种写法要求对应的配置文件中必须有相应的 key,没有的话会抛出异常;第二种写法添加了默认值,则不需要必须对应的 key。设置默认值的方式:@Value("${key:defaultVlaue}")

@Value("${test.field.empty}")
@Value("${test.field.empty:123}")

注意事项1:

默认值你的配置参数的值是 String 类型的话也不需要加双引号:

@Value("${address.host:127.0.0.1}")
private String host;

或者我们想让 host 属性的默认值为空字符串(即""):

@Value("${address.host:}")
private String host;

这样 host 的默认值就是""了;

注意事项2:

PropertyPlaceholderHelper 是 Spring 框架中用于解析占位符的一个工具类,它可以帮助解析文本模板中的${...}占位符。ignoreUnresolvablePlaceholders 是 PropertyPlaceholderHelper 类中的一个布尔变量,它决定当遇到一个无法解析的占位符时是否忽略该占位符。

image.png

如果 ignoreUnresolvablePlaceholders 设置为 true,则当遇到一个无法解析的占位符时,PropertyPlaceholderHelper 会直接将该占位符以字符串形式原样输出,而不抛出异常。

比如下面的代码,如果配置文件没有配置address.host,并且 ignoreUnresolvablePlaceholders 为 true 时,则服务启动不会报错,且String host = "${address.host}"(这就是占位符以字符串形式原样输出的意思)。

@Value("${address.host}")
private String host;

如果 ignoreUnresolvablePlaceholders 设置为 false,则当遇到一个无法解析的占位符时,PropertyPlaceholderHelper 会抛出一个异常,通常是 IllegalArgumentException。

在 Spring 的配置文件中设置 ignoreUnresolvablePlaceholders 的方式如下:

<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    <property name="ignoreUnresolvablePlaceholders" value="true"/>
</bean>

或者在 Java 配置中:

@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
    PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
    configurer.setIgnoreUnresolvablePlaceholders(true);
    return configurer;
}

1.1.2. 字符串格式

(1)yml 文件:

不加引号:字符串默认不用加上单引号或者双引号,不加引号和加单引号效果一样。

单引号:会转义字符串里面的特殊字符,特殊字符最终只会作为一个普通的字符串数据输出。

双引号:不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思输出。例如下面示例中换行符则会起到换行作用。

custom:
  field:
    string: abc \n def
    string1: 'abc \n def'
    string2: "abc \n def"

yml 文件对应控制台输出结果:

abc \n def
abc \n def
abc 
 def

(2)properties 文件:

properties 文件不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思输出。引号会被一起输出。

custom.field.string=abc \n def
custom.field.string1='abc \n def'
custom.field.string2="abc \n def"

properties 文件对应控制台输出结果:

abc
def
'abc
def'
"abc
def"

(3)java 代码

@Value("${custom.field.string}")
private String testString;

@Value("${custom.field.string1}")
private String testString1;

@Value("${custom.field.string2}")
private String testString2;
...
System.out.println(testString);
System.out.println(testString1);
System.out.println(testString2);

1.1.3. List 的注入

${…}用于加载外部属性文件中的值。

#{…}用于执行 SpEL 表达式,并将执行结果的内容赋值给属性。

yml 文件写法:

custom:
  field:
    list: 2,4,6,8,10

properties 文件写法:

custom.field.list=2,4,6,8,10

Java 代码:

@Value("#{'${custom.field.list}'.split(',')}")
private List<Integer> list;
...
System.out.println(list);

控制台输出:

[2, 4, 6, 8, 10]

注意:

如果默认值为空串的话,当不配置该 key 值,默认值会为空串,这样解析出来 list 的元素个数就不是空了,而是 1。这个问题比较严重,因为它会导致代码中的判空逻辑执行错误。这个问题也是可以解决的,在 split() 之前判断下是否为空即可:

@Value("#{'${custom.field.list:}'.empty ? null : '${custom.field.list:}'.split(',')}")
private List<String> testList;

1.1.4. Set 的注入

解析 Set 和解析 List 本质上是相同的,唯一的区别是 Set 会做去重操作。

test:
  set: 111,222,333,111
@Value("#{'${test.set:}'.empty ? null : '${test.set:}'.split(',')}")
private Set<Integer> testSet;

// output: [111, 222, 333]

1.1.5. Map 的注入

yml 文件写法:

注意在 Map 解析中,一定要用引号把 Map 所对应的 value 包起来,要不然解析会失败,因为 yaml 语法中如果一个值以{开头,yaml 将认为它是一个字典。(外层使用双引号和单引号的区别参考 1.1.2,针对特殊字符是否转义的区别)

custom:
  field:
    map: "{name:'tom', age:18}"

properties 文件写法:

properties 文件中值则不需要使用引号包起来。如果用了引号,则会被当做一个字符串,无法转为 Map。

custom.field.map={name:"tom", age:18}

Java 代码:

@Value("#{${custom.field.map}}")
private Map<String, Object> map;

1.1.6. 综合示例

yml 文件

custom:
  field:
    boolean: true
    string: abc
    integer: 123
    long: 456
    float: 1.23
    double: 4.56
    array: 1,2,3
    list: 4,5,6
    set: 7,8,9
    map: "{name:'tom', age:18}"

properties 文件:

custom.field.boolean=true
custom.field.string=abc
custom.field.integer=123
custom.field.long=456
custom.field.float=1.23
custom.field.double=4.56
custom.field.array=1,2,3
custom.field.list=4,5,6
custom.field.set=7,8,9
custom.field.map={name:"tom", age:18}

Java 代码:

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

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

@Data
@Component
public class Custom2 {

    @Value("${custom.field.boolean}")
    private Boolean testBoolean;

    @Value("${custom.field.string}")
    private String testString;

    @Value("${custom.field.integer}")
    private Integer testInteger;

    @Value("${custom.field.long}")
    private Long testLong;

    @Value("${custom.field.float}")
    private Float testFloat;

    @Value("${custom.field.double}")
    private Double testDouble;

    @Value("#{'${custom.field.array}'.split(',')}")
    private Integer[] testArray;

    @Value("#{'${custom.field.list}'.split(',')}")
    private List<Integer> testList;

    @Value("#{'${custom.field.set}'.split(',')}")
    private List<Integer> testSet;

    @Value("#{${custom.field.map}}")
    private Map<String, Object> testMap;

    @Value("${custom.field.empty:111}")
    private Integer testEmpty;

}

控制台输出:

Custom(testBoolean=true, testString=abc, testInteger=123, testLong=456, testFloat=1.23, testDouble=4.56, testArray=[1, 2, 3], testList=[4, 5, 6], testSet=[7, 8, 9], testMap={name=tom, age=18}, testEmpty=111)

注:未找到 @Value 注解支持注入 List<Map<String, Object>> 类型的方法。

注意:@Value 注解不能和 Lombok 的 @AllArgsConstructor 注解同时使用,否则会报错

Consider defining a bean of type 'java.lang.String' in your configuration

1.2. @ConfigurationProperties

1.2.1. 属性介绍

属性描述
prefix可有效绑定到此对象的属性的名称前缀。有效的前缀由一个或多个单词使用点分隔而成(例如 "acme.system.feature")。
value和 prefix 属性用法相同,用一个就行
ignoreInvalidFields表示绑定到此对象时应忽略无效字段。无效的意思是字段类型错误或无法强制转换为正确类型。默认为 false。
ignoreUnknownFields表示绑定到此对象时应忽略未知字段。未知字段是配置文件和对象属性不匹配的字段。默认为 true。

1.2.2. 综合示例

yml 文件:

user:
  field:
    name: zhangsan
    age: 25
    sex: true
    other:
      email: abc@163.com
      idcard: 123456
    interset1: english,math
    interset2:
      - music
      - sports
    family:
      - mom: li
        dad: wang
      - nan: chen
        yey: wang

properties 文件:

user.field.name=zhangsan
user.field.age=25
user.field.sex=true
user.field.other.email=abc@163.com
user.field.other.idcard=123456
user.field.interset1=english,math
user.field.interset2[0]=music
user.field.interset2[1]=sports
user.field.family[0].mom= li
user.field.family[0].dad= wang
user.field.family[1].nan= chen
user.field.family[1].yey= wang

Java 代码:

1.2.2.1. Class 绑定
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

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

@Data
@Component
@ConfigurationProperties(prefix = "user.field")
public class Custom {
    private String name;
    private Integer age;
    private Boolean sex;
    private Map<String, Object> other;
    private List<String> interset1;
    private List<String> interset2;
    private List<Map<String, String>> family;
}
...
@Autowired
private Custom custom;
...
System.out.println(custom);

控制台输出:

Custom(name=zhangsan, age=25, sex=true, other={email=abc@163.com, idcard=123456}, interset1=[english, math], interset2=[music, sports], family=[{mom=li, dad=wang}, {nan=chen, yey=wang}])
1.2.2.2. Bean 绑定
import lombok.Data;

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

@Data
public class Custom {
    private String name;
    private Integer age;
    private Boolean sex;
    private Map<String, Object> other;
    private List<String> interset1;
    private List<String> interset2;
    private List<Map<String, String>> family;
}
import com.example.demo.model.properties.Custom;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 自定义配置类
 * @author wangbo
 * @date 2020/7/16 15:02
 */
@Configuration
public class CustomConfiguration {

    //不指定name的话默认使用方法名作为bean的名称
    @Bean(name = "myCustom")
    //这个注解也可以直接加在Custom类上面
    @ConfigurationProperties(prefix = "user.field")
    public Custom buildCustom(){
        return new Custom();
    }
    
}
@Autowired
private Custom custom;
...
System.out.println(custom);

控制台输出:

Custom(name=zhangsan, age=25, sex=true, other={email=abc@163.com, idcard=123456}, interset1=[english, math], interset2=[music, sports], family=[{mom=li, dad=wang}, {nan=chen, yey=wang}])
1.2.2.3. @EnableConfigurationProperties 绑定
import lombok.Data;

import java.util.List;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "user.field")
public class Custom {
    private String name;
    private Integer age;
    private Boolean sex;
    private Map<String, Object> other;
    private List<String> interset1;
    private List<String> interset2;
    private List<Map<String, String>> family;
}
import com.example.demo.model.properties.Custom;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 自定义配置类
 * @author wangbo
 * @date 2020/7/16 15:02
 */
@Configuration
@EnableConfigurationProperties(Custom.class)
public class CustomConfiguration {
}
@Autowired
private Custom custom;
...
System.out.println(custom);

控制台输出:

Custom(name=zhangsan, age=25, sex=true, other={email=abc@163.com, idcard=123456}, interset1=[english, math], interset2=[music, sports], family=[{mom=li, dad=wang}, {nan=chen, yey=wang}])

1.2.3. 松散绑定

yml 和 properties 类似,下面仅以 properties 进行示例。

(1)@ConfigurationProperties 注解的 prefix 属性写的时候不支持松散绑定。只有对应的字段值支持松散绑定。

(2)可以使用驼峰式,下划线,短横线,大写字母,或者这几种混合使用都可以实现 Java 驼峰字段绑定。驼峰和分隔符可以在任意位置,但是最好是在单词分隔处添加分隔符,单词首字母使用驼峰(首字母小写)。推荐短横线的写法,这个感觉最清晰明了。

#测试正常绑定
loose.binging.firstName=one
#测试下划线
loose.binging.two_name=two
#测试短横线(多单词推荐写法,清晰明了)
loose.binging.three-name=three
#测试大写
loose.binging.FOURNAME=four
#测试混合使用
loose.binging.FI-VENA_ME=five

Java 代码:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "loose.binging")
public class Loose {
    private String firstName;
    private String twoName;
    private String threeName;
    private String fourName;
    private String fiveName;
}

1.2.4. 自定义结构

属性文件: yml 和 properties 类似,下面仅以 properties 进行示例。

company.employee.man[0].name=小刚
company.employee.man[0].age=25
company.employee.man[1].name=小明
company.employee.man[1].age=27

company.employee.woman[0].name=小美
company.employee.woman[0].age=24
company.employee.woman[1].name=小娟
company.employee.woman[1].age=26

Java 代码:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;

@Data
@Component
@ConfigurationProperties(prefix = "company.employee")
public class Employee {

    private List<People> man;
    private List<People> woman;

    @Data
    public static class People{
        private String name;
        private Integer age;
    }
}
...
@Autowired
private Employee employee;
...
System.out.println(employee);

控制台输出:

Employee(man=[Employee.People(name=小刚, age=25), Employee.People(name=小明, age=27)], woman=[Employee.People(name=小美, age=24), Employee.People(name=小娟, age=26)])

1.2.5. 参数校验

spring-boot-starter-web 依赖会自动引入 validation-api 依赖,在 validation-api 下的 javax.validation.constraints 包下有很多校验器,可以直接使用。

在这里插入图片描述

1.3. @PropertySource

如果想读取自定义 properties 文件的自定义属性值,可以使用 @PropertyResource 注解指定文件路径。

@PropertySource 默认不支持 yaml 文件读取,但是可以自己修改实现支持,只需要继承 DefaultPropertySourceFactory 类并修改就可以了,这里就不说了。

1.3.1. 属性介绍

属性描述
name指示此属性源的名称。如果省略,名字会根据底层资源的描述生成。
value指示要加载的属性文件的资源位置。支持 properties 和 xml 格式的属性文件,例如 "classpath:/com/myco/app.properties" 或者 "file:/path/to/file.xml"。不允许使用资源位置通配符,例如 **/*.properties。每个指定的位置必须只有一个属性文件。
ignoreResourceNotFound指定的配置文件不存在是否报错,默认是false。当设置为 true 时,若该文件不存在,程序不会报错。实际项目开发中,最好设置为 false。
encoding指定读取属性文件所使用的编码,通常使用的是 UTF-8。
factory指定一个自定义的 PropertySourceFactory,默认情况下,将使用标准资源文件的默认工厂。

1.3.2. 综合示例

在 resources 目录下新建了一个自定义的 source.properties 文件。

user.field.name=lisi
user.field.age=29
user.field.sex=false
user.field.other.email=def@163.com
user.field.other.idcard=654321
user.field.interset1=english,math
user.field.interset2[0]=music
user.field.interset2[1]=sports
user.field.family[0].mom= li
user.field.family[0].dad= wang
user.field.family[1].nan= chen
user.field.family[1].yey= wang

Java 代码: 也可使用 @Value 注解进行字段绑定,这里只用 @ConfigurationProperties 注解进行示例。

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

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

@Data
@Component
@ConfigurationProperties(prefix = "user.field")
@PropertySource(value = "classpath:source.properties", encoding = "UTF-8")
public class Custom {
    private String name;
    private Integer age;
    private Boolean sex;
    private Map<String, Object> other;
    private List<String> interset1;
    private List<String> interset2;
    private List<Map<String, String>> family;
}
...
@Autowired
private Custom custom;
...
System.out.println(custom);

控制台输出:

Custom(name=lisi, age=29, sex=false, other={idcard=654321, email=def@163.com}, interset1=[english, math], interset2=[music, sports], family=[{mom=li, dad=wang}, {yey=wang, nan=chen}])

1.4. @ConditionalOnProperty

在 Spring Boot 中,@ConditionalOnProperty注解用于根据特定的配置属性条件性地创建 bean。这个注解非常有用,因为它允许你基于应用程序的配置(通常是application.propertiesapplication.yml文件中的属性)来启用或禁用某些配置类或 bean 的实例化。

1.4.1. 属性介绍

@ConditionalOnProperty的用法很简单,它有六个静态方法,代码如下:

String[] value() default {};

String prefix() default "";

String[] name() default {};

String havingValue() default "";

boolean matchIfMissing() default false;

boolean relaxedNames() default true;

value() 和 name() 的用法是一样的,只能使用一个,系统在加载的时候会在配置文件中找 value() 或者 name()中的值,如果有就返回 true,没有就返回 false。

prefix() 就是要找的 name() 或者 value() 的前缀。

havingValue(),如果从配置文件中找到的 name() 或者 value() 中的值如果和 havingValue() 中的值一样,就返回 true,否则就返回 false。

matchIfMissing(),如果没有找到 name() 或者 value() 中的值就返回 false(默认的,可以修改)。

relaxedNames(),是否支持宽松绑定,一般不用,使用完全匹配就行。

最后如果返回的值为 false,那么我们注解的类就不会生效,否则就生效。

1.4.2. 综合示例

项目支持三种云存储,每次会根据io.engine配置使用一种,也就是实例化一个 bean。

顺便介绍下这种属性文件中的环境变量的配置方式,这里io.engine的默认值为s3,也就是说当环境变量IO_ENGINE不存在时,则使用默认值。

properties 文件:

#IO引擎:s3、minio、obs
io.engine=${IO_ENGINE:s3}

#亚马逊S3
io.s3.region=${IO_S3_REGION}
io.s3.bucketName=${IO_S3_BUCKETNAME}
io.s3.accessKey=${IO_S3_ACCESSKEY}
io.s3.secertKey=${IO_S3_SECRETKEY}

#自建MinIO
io.minio.endPoint=${IO_MINIO_ENDPOINT}
io.minio.bucketName=${IO_MINIO_BUCKETNAME}
io.minio.accessKey=${IO_MINIO_ACCESSKEY}
io.minio.secretKey=${IO_MINIO_SECRETKEY}

#华为云OBS
io.obs.endPoint=${IO_OBS_ENDPOINT}
io.obs.bucketName=${IO_OBS_BUCKETNAME}
io.obs.accessKey=${IO_OBS_ACCESSKEY}
io.obs.secretKey=${IO_OBS_SECRETKEY}

这里其实也可以使用 @ConfigurationProperties 注解代替 @Value 注解。

@Slf4j
@Service
@ConditionalOnProperty(prefix = "io", name = "engine", havingValue = "s3")
public class S3FileStorageServiceImpl implements FileStorageService {

    @Value("${io.s3.region}")
    private String region;
    
    @Value("${io.s3.bucketName}")
    private String bucketName;

    @Value("${io.s3.accessKey}")
    private String accessKey;

    @Value("${io.s3.secertKey}")
    private String secretKey;
    
    private AmazonS3 amazonS3;

    @PostConstruct
    public void init() {
        AmazonS3ClientBuilder s3Builder = AmazonS3ClientBuilder.standard().withCredentials(
                new AWSCredentialsProvider() {
                    @Override
                    public AWSCredentials getCredentials() {
                        return new AWSCredentials() {
                            @Override
                            public String getAWSAccessKeyId() {
                                return accessKey;
                            }
                            @Override
                            public String getAWSSecretKey() {
                                return secretKey;
                            }
                        };
                    }
                    @Override
                    public void refresh() {
                    }
                });
        s3Builder.setRegion(region);
        amazonS3 = s3Builder.build();
    }
}
@Slf4j
@Service
@ConditionalOnProperty(prefix = "io", name = "engine", havingValue = "obs")
public class ObsFileStorageServiceImpl implements FileStorageService {

    @Value("${io.obs.bucketName}")
    private String bucketName;
    
    @Value("${io.obs.accessKey}")
    private String accessKey;
    
    @Value("${io.obs.secretKey}")
    private String secretKey;
    
    @Value("${io.obs.endPoint}")
    private String endPoint;

    private ObsClient obsClient;

    @PostConstruct
    public void init() {
        try {
            obsClient = new ObsClient(accessKey, secretKey, endPoint);
        } catch (Exception e) {
            log.error("obs client init error", e);
        }
    }
}
@Slf4j
@Service
@ConditionalOnProperty(prefix = "io", name = "engine", havingValue = "minio")
public class MinIOFileStorageServiceImpl implements FileStorageService {

    @Value("${io.minio.endPoint}")
    private String endPoint;
    
    @Value("${io.minio.bucketName}")
    private String bucketName;
    
    @Value("${io.minio.accessKey}")
    private String accessKey;
    
    @Value("${io.minio.secretKey}")
    private String secretKey;
    
    private MinioClient minioClient;

    @PostConstruct
    public void init() {
        try {
            minioClient = MinioClient.builder()
                    .endpoint(this.getEndPoint())
                    .credentials(this.getAccessKey(), this.getSecretKey())
                    .build();
            );
        } catch (Exception e) {
            log.error("minio client init error", e);
        }
    }
}

1.5. 参数间引用

属性文件:

yml 和 properties 类似,下面仅以 properties 进行示例。

blog.name=一线大码
blog.title=SpringBoot自定义配置和多环境配置
blog.desc=${blog.name}正在写${blog.title}

Java 代码:

也可使用 @Value 注解进行字段绑定,这里只用 @ConfigurationProperties 注解进行示例。

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "blog")
public class Blog {
    private String name;
    private String title;
    private String desc;
}
...
@Autowired
private Blog1 blog;
...
System.out.println(blog);

控制台输出:

Blog(name=一线大码, title=SpringBoot自定义配置和多环境配置, desc=一线大码正在写SpringBoot自定义配置和多环境配置)

中间遇到了中文乱码问题,可以设置 IDEA 的文件编码为 UTF-8 即可。

1.6. 使用随机数

属性文件:

yml 和 properties 类似,下面仅以 properties 进行示例。

# 随机字符串
test.random.string=${random.value}
# 随机int
test.random.number=${random.int}
# 随机long
test.random.bignumber=${random.long}
# 10以内的随机数
test.random.minnumber=${random.int(10)}
# 10-20的随机数
test.random.rangenumber=${random.int[10,20]}

Java 代码:

也可使用 @Value 注解进行字段绑定,这里只用 @ConfigurationProperties 注解进行示例。

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "test.random")
public class TestRandom {
    private String string;
    private Integer number;
    private Long bigNumber;
    private Integer minNumber;
    private Integer rangeNumber;
}
...
@Autowired
private TestRandom testRandom;
...
System.out.println(testRandom);

控制台输出:

TestRandom(string=83299a6016a3583c890eb472ce1ac5cc, number=-1814272333, bigNumber=-7943516043135095470, minNumber=1, rangeNumber=11)

2. 多环境配置

2.1. 命令行设置属性值

Spring Boot 项目 Jar 包启动命令:java -jar xxx.jar --server.port=8888,通过使用--server.port属性来设置xxx.jar应用的端口为8888

在命令行运行时,连续的两个减号--就是对 application.properties 中的属性值进行赋值的标识。所以,java -jar xxx.jar --server.port=8888命令,等价于在 application.properties 中添加属性 server.port=8888。application.yml 中类似。

通过命令行来修改属性值固然提供了不错的便利性,但是通过命令行就能更改应用运行的参数,那岂不是很不安全?是的,所以 Spring Boot 也提供了屏蔽命令行访问属性的设置:SpringApplication.setAddCommandLineProperties(false)。具体代码设置如下所示:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		//原有启动代码
		//SpringApplication.run(DemoApplication.class, args);

		//修改后的启动代码
		SpringApplication application = new SpringApplication(DemoApplication.class);
		//禁用命令行参数
		application.setAddCommandLineProperties(false);
		application.run(args);
	}
}

可以通过 IDEA 设置 SpringBoot 项目启动参数。

2.2. 加载环境配置文件

yml 文件同 properties 文件一样,下面仅以 properties 文件进行说明。

在 SpringBoot 中多环境配置文件名需要满足application-{profile}.properties的格式,其中{profile}对应你的环境标识,环境标志可以自己定义字符串,比如:

application-dev.properties:开发环境
application-test.properties:测试环境
application-prod.properties:生产环境

具体哪个配置文件会被加载,需要在application.properties文件中通过spring.profiles.active属性来设置,其值对应{profile}值。如:spring.profiles.active=dev就会加载application-dev.properties配置文件内容。不管加载哪个环境配置文件,基础配置文件application.properties都会被加载。

可以得出以下使用方法:

(1)application.properties中配置通用内容,比如设置spring.profiles.active=dev,表示以开发环境为默认配置。

(2)application-{profile}.properties中配置各个环境不同的内容。

(3)启动的时候通过命令行设置属性值的方式去激活不同环境的配置,比如:

java -jar app.jar --spring.profiles.active=prod

表示启动的时候加载application-prod.properties配置文件内容。

如果基础配置文件application.properties中未配置spring.profiles.active属性,项目启动时命令行也没设置spring.profiles.active属性,则不会加载环境配置文件,只使用基础配置文件,如果基础配置文件中没设置端口,则会使用默认的 Tomcat 端口 8080。启动日志如下:

- No active profile set, falling back to default profiles: default
- Tomcat initialized with port(s): 8080 (http)

2.3. 多环境日志配置

比如我们 SpringBoot 项目的 logback 日志配置,也需要区分不同的环境,有时候还需要获取配置文件中的配置。

(1)springProperty标签可以获取配置文件中的属性。比如我们需要在 logback-spring.xml 配置文件中获取配置文件的 spring.profiles.active 属性:

<springProperty scope="context" name="profiles" source="spring.profiles.active"/>

在 logback-spring.xml 中的其他位置可以直接使用$符号通过name属性去获取对应的值,比如获取上面的配置:${profiles}

(2)springProfile标签允许我们更加灵活配置文件,可选地包含或排除部分配置内容。元素中的任何位置均支持。使用该标签的name属性可以指定具体的环境,可以使用逗号分隔列表指定多个环境的配置文件。具体的环境由 spring.profiles.active 属性指定。例如:

<springProfile name="dev">
    <!-- 开发环境时激活 -->
</springProfile>
 
<springProfile name="dev,test">
    <!-- 开发,测试的时候激活-->
</springProfile>
 
<springProfile name="!prod">
    <!-- 当生产环境时,该配置不激活。不是生产环境时激活-->
</springProfile>

(3)还可以将 logback.xml 文件拆分为 logback-dev.xml,logback-test.xml,logback-prod.xml 三个环境对应的配置文件(logback-{profile}.xml)。具体的环境由 spring.profiles.active 属性指定。 application.properties 里面还需要添加配置:

logging.config: classpath:logback-${spring.profiles.active}.xml

这样也可以实现多环境日志配置。

2.4. 多环境代码调用

在某些情况下,在不同环境中应用的某些业务逻辑可能需要有不同的实现。例如邮件服务,假设 EmailService 中包含的 send() 方法向指定地址发送电子邮件,但是我们只希望在生产环境中才执行真正发送邮件的代码,而开发和测试环境里则不发送,以免向用户发送无意义的垃圾邮件。我们可以借助 Spring 的注解 @Profile 注解实现这样的功能,需要定义两个 EmailService 接口的实现类。 EmailService 接口:

package com.example.demo.service;

/**
 * @author wangbo
 * @date 2020/7/16 11:26
 */
public interface EmailService {
    /**
     * 发送邮件
     */
    void send();
}

开发环境和测试环境实现类: @Profile(value = {"dev", "test"})表示只有 Spring 定义的 profile 为 dev 或者 test 时才会实例化 EmailServiceImpl1 这个类。

package com.example.demo.service.impl;

import com.example.demo.service.EmailService;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

/**
 * @author wangbo
 * @date 2020/7/16 11:28
 */
@Service
@Profile(value = {"dev", "test"})
public class EmailServiceImpl1 implements EmailService {
    @Override
    public void send() {
        System.out.println("开发环境和测试环境发送邮件");
    }
}

生产环境实现类: @Profile(value = {"prod"})表示只有 Spring 定义的 profile 为 prod 时才会实例化 EmailServiceImpl2 这个类。

package com.example.demo.service.impl;

import com.example.demo.service.EmailService;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

/**
 * @author wangbo
 * @date 2020/7/16 11:29
 */
@Service
@Profile(value = {"prod"})
public class EmailServiceImpl2 implements EmailService {
    @Override
    public void send() {
        System.out.println("生产环境发送邮件");
        //具体的发送代码省略
    }
}

调用该 Service 的代码:

@Autowired
private EmailService emailService;
...
emailService.send();

3. 静态成员变量注入

3.1. set 方法注入方式

有时候我们需要注入一个静态成员变量,例如在工具类中某个静态工具方法使用到某个配置项或者需要用到某个 bean。示例:

@Component
public class JwtUtil {

    private static RedisTemplate redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate redisTemplate) {
        JwtUtil.redisTemplate = redisTemplate;
    }

    private static String accessKey;

    @Value("${access.key}")
    public void setAccessKey(String accessKey) {
        JwtUtil.accessKey = accessKey;
    }
}

注意项:

  • 需要在该类上添加注解@Component
  • 静态属性不能直接注入,需要通过其 set 方法进行注入,注意自动生成的 set 方法需要去掉static

3.2. @PostConstruct 注解方式

执行顺序:

Constructor(构造方法) -> @Autowired(依赖注入) / @Value -> @PostConstruct(初始化方法)

所以可以使用@PostConstruct注解在 Spring 初始化之后再给静态变量赋值。

@Component
public class SmsVerificationCodeUtil {

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

    private static String valueStatic;

    /**
    * 给静态变量赋值
    */
    @PostConstruct
    private void init() {
         valueStatic = value;
    }

    // 即可使用 valueStatic
    public static String method(){
        return valueStatic;
    }
}

4. 附录说明

(1)我们使用了@Component或者@Configuration注解配置类为 spring 管理的类,这样在别的类才可以进行 @Autowired 注入使用。在其他的一些文章中并没有使用这两个注解来将配置类注册到 Spring 容器中,而是通过在启动类上添加 @EnableConfigurationProperties({xxx.class, xxxx.class}) 这样的方式实现配置类注入。这两种方式都可以。

(2)@Component@Configuration都可以作为配置类注解,这两的区别可以参考以下文章: @Configuration和@Component区别 @Component和@Configuration作为配置类的差别

(3)@Value 读不到配置文件的几种情况