【熵增教育】SpringBoot的配置外部化——熵增學院

在前面的課程中,我們給大家分享過SpringBoot精妙的啓動配置,主要闡述的是spring的IoC容器在SpringBoot中的加載過程,並與傳統項目中Spring的IoC容器加載過程進行了一個對比.我們在開發的過程中,除了IoC容器的配置之外,當然還有許多其他的配置,諸如數據庫的鏈接信息,端口,以及項目的內部使用的一些個性化信息等.那SpringBoot是如何管理這些配置呢?我今天呢,就從以下這三個方面來給大家分享一下SpringBoot是如何管理配置信息的.

  1. 配置文件和屬性獲取

  2. 配置文件的名字、目錄和優先級

  3. 傳統的properties與YAML

1.配置文件和屬性獲

 

1.1 傳統配置文件的值獲取與SpringBoot中的值獲取

在傳統的項目裏,我們的配置信息一般都寫在配置文件中,然後關於spring需要的信息,我們就在spring的xml文件裏引用,大略如下所示:

classpath下的config裏創建一個jdbc.properties

jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/testdb?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
jdbc.username=develop
jdbc.password=&dT$BvYdOlH4*m9G

然後在我們的application.xml裏引入我們需要的這個數據源:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="  
        http://www.springframework.org/schema/beans  
        http://www.springframework.org/schema/beans/spring-beans.xsd  
        http://www.springframework.org/schema/context  
        http://www.springframework.org/schema/context/spring-context.xsd  
        http://www.springframework.org/schema/tx  
        http://www.springframework.org/schema/tx/spring-tx.xsd">

    <context:property-placeholder location="classpath:config/jdbc.properties" />
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
    </bean>
    ... ...其他配置
</beans>

當然除了spring的數據源信息之外,我們往往也會有一些其他的信息,比如項目返回給前端的一些報錯信息等,此時我們通常的做法是使用java的原生方法去加載。如以下所示:

在classpath的config下有個webreslt.properties

0=SUCCESS
100000=參數有誤
100001=AppKey不能爲空
100002=Signature不能爲空
100003=參數列表(paramMap)不能爲空
100004=secret不能包含在參數列表中
100005=參數簽名不合法
100006=手機號不能爲空
100007=驗證碼不能爲空
100008=郵件內容不能爲空
100009=收件人不能爲空
100010=郵件主題不能爲空
200000=應用無權限
200001=應用未註冊
300000=Api異常
300001=短信發送失敗
300002=短信驗證失敗
300003=郵件發送失敗
300004=短信發送超過最大條數限制

獲取值的方法如下:

package com.ailu.paas.common.utils;

import org.apache.commons.lang3.StringUtils;

import java.util.Locale;
import java.util.ResourceBundle;

public class PropertyFileReader {
    public static String getItem(String key) {
		return getItem(key, "");
	}

	public static String getItem(String key, String defaultValue) {
		ResourceBundle rb = ResourceBundle.getBundle("config/webresult");
		String value = "";

        try {
        	value = new String(rb.getString(key).getBytes("ISO-8859-1"), "UTF-8");
        } catch (Exception e) {
        	e.printStackTrace();
        }

        if (StringUtils.isEmpty(value)) {
        	value = defaultValue;
        }

        return value.trim();
    }

}

然後在其他的地方,我們就可以直接使用以下這樣的方式去獲取屬性文件中的值了.

String value=PropertyFileReader.getItem(key);

而在SpringBoot中呢,我們則可以使用@Component+@Value這兩個組合,來快速的讀取配置文件中的值,

仍然是在classpath下,我們在配置文件裏寫上如下配置:

name="Lianmengtu"

然後我們創建一個java類,並加上@Component和@Value,如下所示:

package top.lianmengtu.testprofile.common;

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

@Component
@Getter
@Setter
public class TestProperty {

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

其中@Component是爲了把TestProperty作爲組件放入到Spring的IoC容器中,而@Value("${name}")則是爲了從配置文件中取值,${name}中的name即是配置文件中的key.然後我們就可以在其他地方通過注入直接使用了:

@Autowired
private TestProperty testProperty;

public void test(){
    System.out.println(testProperty.getName());
}

 

1.2 隨機值的綁定


在某些場景下,我們可能需要在項目的配置中添加一些隨機值,並且這些值在我們項目啓動後就自動的初始化,而SpringBoot就考慮到了這種情況,於是給我們準備了一些工具,方便我們的使用.使用情況如下:

# 隨機字符串
secret=${random.value}

#隨機數
setup=${random.int}

#0-10之間的隨機數
range-int=${random.int[0,10]}

#生成uuid
uuid=${random.uuid}

獲取方式和其他的屬性相同,如下所示:

 
package top.lianmengtu.testprofile.common;

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

@Component
@Getter
@Setter
public class TestProperty {

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

    @Value("${setup}")
    private Integer setup;

    @Value("${range-int}")
    private Integer rangeInt;

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

 

1.3 變量的引用與佔位符


有些時候我們會在配置項中引用另一項的值,當然,是以變量的形式進行引用.如下所示:

protocal=http://
domain=${protocal}ask.lianmengtu.top

這裏我們看到,在springBoot中使用的變量佔位符是${key}.這時候就有一個問題,因爲我們現在大多數的開發環境都是maven,我們都知道maven也是支持變量佔位的,我們可以在打包不同環境的時候可以激活不同的profile,然後對變量值進行替換,而我們往常在使用Maven的時候,用的變量佔位符正好是${key},那此時,我們該怎麼辦呢?SpringBoot是不是不支持Maven的變量佔位呢?我們必須要二選其一嗎?

當然不是.SpringBoot也考慮到了這個問題,因此給maven佔位提供了另外一個符號即@key@,如下所示:

protocal=http://
domain=${protocal}ask.lianmengtu.top
username=@username@
password=@password@

然後我們就可以在我們的pom裏這麼寫了,而當我們激活某一個profile的時候,相應的maven變量就會被替換了

<profiles>
    <profile>
        <id>dev</id>
        <properties>
            <username>dev</username>
            <password>dev123</password>
        </properties>
    </profile>
    <profile>
        <id>test</id>
        <properties>
            <username>test</username>
            <password>test123</password>
        </properties>
    </profile>
</profiles>

 

2. yml與properties

SpringBoot是除了支持properties這種方式之外,它也支持YAML這種方式,並且因爲yml結構清晰,又可以繼承,所以使用yml這種方式的人也越來越多了.而我就是這麼對yml路轉粉的.

 

2.1 yml結構化

 


yml的第一個好處是結構化,這與properties是明顯的差別.這裏放上兩份同樣的配置文件來個視覺對比,首先是properties

environments.dev.url= 
environments.dev.name=Developer Setup
environments.prod.url= 
environments.prod.name=My Cool App
my.servers[0]=dev.example.com
my.servers[1]=another.example.com

 

對比yml

environments:	
    dev:		
        url: http://dev.example.com		
        name: Developer Setup	
    prod:		
        url: http://another.example.com		
        name: My Cool App
my:
    servers:
	- dev.example.com
	- another.example.com

這裏只是舉個例子,所以可能這兩三行大家看起來沒什麼感覺,但要知道在實際的項目中,我們的配置可能包含各種各樣的信息,數據庫的,緩存的,第三方平臺的等等,那時候,如果我們還用properties,那麼看着將會非常頭大.

 

2.2 yml的繼承

 


在我們使用配置文件的時候,尤其是分環境使用的時候,常常會碰到這麼一個問題: 大多數的項目配置都一樣,只有少數的不一樣,此時,如果是使用properties,那麼我們就只能每個文件各寫一份,而在yml裏,我們就不用,我們只需要將通用的那部分寫到application-common.yml裏,然後少量不同的,我們在分環境進行描述,application-dev.yml,application-prod.yml裏,這樣我們只需要激活一份文件,剩下的就會自動的進行繼承和使用了,具體方式如下:

application.yml

app:
  name: "lianmengtu"
  country: "China"
  username: "melon"
  password: "melon123"
application-dev.yml:

app:
  username: "jacobdev"
  password: "dev123456"


application-prod.yml

app:
  username: "LMTprod"
  password: "prod456"
 

這樣當我們在啓動時,激活不同的配置時,username和password會不同,但name和country則是從默認的yml中繼承過來的.

 

2.3 指定配置文件的名字和地址

 


剛剛我們提到過多種環境,配置文件之所以要區分環境,就是因爲有些信息是需要保密的,無法公開.而SpringBoot則允許從外部,通過命令行的形式,對配置文件進行指定,當然也可以指定變量.

 

2.3.1 指定配置文件的名字


 

我們可以使用--spring.config.name=xxx 這樣的參數形式指定配置文件的名字:

$ java -jar myproject.jar --spring.config.name=myproject

 

2.3.2 配置指定目錄下的配置文件

 


我們可以使用--spring.config.location=xxxxx這樣的參數形式來配置指定目錄下的配置文件,如下文則指定了classpath下的config目錄下的test.yml

java -jar myproject.jar --spring.config.location=classpath:/config/test.yml

 

當然從1.x轉過來的人可能更喜歡指定目錄,這裏要特別說明一下,如果--spring.config.location是以目錄結尾的,則必須加/ 如下所示:

java -jar myproject.jar --spring.config.location=classpath:/config/

 

2.3.3 配置的優先級


在我們沒有明確指定文件名字的時候,springBoot會按着以下順序進行考慮

  1. 當前目錄下的config目錄裏的application.properties

  2. 當前目錄下的application.properties

  3. classpath下的config目錄下的application.properties

  4. classpath下的application.properties

當然除了這些之外,命令行也是可以傳參數的,並且命令行參數的優先級是最高的.

 

3. 一些比較複雜的配置

使用@Value()來進行屬性注入有些時候會顯得比較笨重,尤其是使用多個配置或者我們的配置項是呈垂直結構化的數據時,更是這樣.SpringBoot提供了另外一種方法來處理這類比較複雜的數據.這就是我們要說的@ConfigurationProperties.

首先我們有這樣一個配置文件:

 

app:
  name: "lianmengtu"
  enabled: false
  security:
    username: "jacob"
    password: "jacob123"
    roles:
      - USER
      - ADMIN

我們看到在這個配置文件裏,app下有name屬性,有enabled屬性,其中較爲特殊的是security,因爲他還包含了些其他的屬性,包括username,包括password,還有一個類型爲String的roles集合,那麼此時,我們可以對應的寫成下面這個屬性類

 

 

package top.lianmengtu.testprofile.common;

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

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@ConfigurationProperties("app")
public class ComplexProperty {

    private String name;
    private boolean enabled;

    private final Security security=new Security();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public Security getSecurity() {
        return security;
    }

    public static class Security {
        private String username;

        private String password;

        private List<String> roles =new ArrayList<>(Collections.singleton("USER"));

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public String getPassword() {
            return password;
        }

        public void setPassword(String password) {
            this.password = password;
        }

        public List<String> getRoles() {
            return roles;
        }

        public void setRoles(List<String> roles) {
            this.roles = roles;
        }
    }
}

之後,我們可以在我們的service層引用它:

package top.lianmengtu.testprofile.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import top.lianmengtu.testprofile.common.ComplexProperty;

import java.util.List;

@Service
public class PropertyService  {

    private final ComplexProperty complexProperty;

    @Autowired
    public PropertyService(ComplexProperty complexProperty){
        this.complexProperty=complexProperty;
    }

    public String name(){
        return complexProperty.getName();
    }

    public boolean enabled(){
        return complexProperty.isEnabled();
    }

    public String userName(){
        return complexProperty.getSecurity().getUsername();
    }

    public String password(){
        return complexProperty.getSecurity().getPassword();
    }

    public String roles(){
        StringBuffer roles=new StringBuffer();
        List<String> roleArray=complexProperty.getSecurity().getRoles();
        roleArray.forEach(role->{
            roles.append(role).append("----");
        });
        return roles.toString();
    }
}

這裏的構造函數注入,我們也可以換成相應的@Autowried注入.爲了使這個配置生效,我們需要在Application上加上@EnableConfigurationProperties(ComplexProperty.class)

 

package top.lianmengtu.testprofile;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import top.lianmengtu.testprofile.common.ComplexProperty;

@SpringBootApplication
@EnableConfigurationProperties(ComplexProperty.class)
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

 

剛剛舉的那種類型可以說是比較複雜的一個類型了,除了那種內嵌的形式之外,如果說我們只想得到內嵌類裏面的屬性或者說只有內嵌類裏面有屬性,則我們可以寫成以下這種形式,其他的地方都不用變.這種形式我們稱之爲relaxed binding

@ConfigurationProperties("app.security")
public class ComplexProperty {
    private String username;
    private String password;
    private List<String> roles;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public List<String> getRoles() {
        return roles;
    }

    public void setRoles(List<String> roles) {
        this.roles = roles;
    }
}

並且也支持map類型,配置文件如下所示,其中key可以有兩種指定方式,一種是"[/key]",一種是/key:

app:
  name: "lianmengtu"
  enabled: false
  security:
    username: "jacob"
    password: "jacob123"
    roles:
      - USER
      - ADMIN
    work:
      "[/position]": "CEO"
      "[/company]": "bat"
      /address: "BeiJing"

而我們的配置類則如下所示:

package top.lianmengtu.testprofile.common;

import org.springframework.boot.context.properties.ConfigurationProperties;

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

@ConfigurationProperties("app.security")
public class ComplexProperty {
    private String username;
    private String password;
    private List<String> roles;
    private Map<String,String> work;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public List<String> getRoles() {
        return roles;
    }

    public void setRoles(List<String> roles) {
        this.roles = roles;
    }

    public Map<String, String> getWork() {
        return work;
    }

    public void setWork(Map<String, String> work) {
        this.work = work;
    }
}

這兩種方式都是Ok的,

總結

今天的內容,其實到這裏就已經結束了,在使用配置文件的過程中,我們還碰到了一些問題,首先,我們在使用這種配置方式,無論是@Component+@Value還是後來的@ConfigurationProperties這兩種方式,他都是在進入spring的時候進行初始化的,這也就意味着,如果我們沒有從Spring的容器中去取我們的屬性容器的話,那麼我們的屬性值是沒有辦法注入的,這一點希望大家能夠注意,其次,今天只是講了主要的幾種方式,還有一些像複雜類型的屬性合併,以及屬性驗證,這些希望大家可以研究一下,如果有不明白的,歡迎大家在論壇上提出來,我們可以一起探討.以下是@ConfigurationProperties和@Value的一些對比:

Feature @ConfigurationProperties @Value

Relaxed binding

Yes

No

Meta-data support

Yes

No

SpEL evaluation

No

Yes

 

轉載請註明出處:聯盟兔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章