spring boot 原始碼解析57-actuator元件:info背後的密碼(全網獨家)
解析
我們平常訪問/info時會返回一些自定義的資訊,一般人只知道在application.properties中配置info.author=herry 開頭的配置,這樣就可以在訪問/info時,就會返回author: “herry”,但是如下的返回值是如何返回的,很多人就不會了
{
author: "herry",
git: {
commit: {
time: 1515694386000,
id: "自定義的commit.id.abbrev"
},
branch: "主幹"
},
build: {
version: "0.0.1-SNAPSHOT",
artifact: "demo",
description: "Demo project for Spring Boot",
group: "com.example",
time: 1515694386000
}
}
InfoEndpoint的解析在spring boot 原始碼解析23-actuate使用及EndPoint解析中有介紹,InfoContributor最終是通過InfoEndpoint來呼叫的.
解析
關於這部分的類圖如下:
InfoContributor
InfoContributor–> 新增 應用詳情.程式碼如下:
public interface InfoContributor {
// 向Info.Builder中新增資訊
void contribute(Info.Builder builder);
}
Info
很簡單的一個封裝類,使用了建造者模式
程式碼如下:
@JsonInclude(Include.NON_EMPTY)
public final class Info {
private final Map<String, Object> details;
private Info(Builder builder) {
LinkedHashMap<String, Object> content = new LinkedHashMap<String, Object>();
content.putAll(builder.content);
this .details = Collections.unmodifiableMap(content);
}
@JsonAnyGetter
public Map<String, Object> getDetails() {
return this.details;
}
public Object get(String id) {
return this.details.get(id);
}
@SuppressWarnings("unchecked")
public <T> T get(String id, Class<T> type) {
Object value = get(id);
if (value != null && type != null && !type.isInstance(value)) {
throw new IllegalStateException("Info entry is not of required type ["
+ type.getName() + "]: " + value);
}
return (T) value;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj != null && obj instanceof Info) {
Info other = (Info) obj;
return this.details.equals(other.details);
}
return false;
}
@Override
public int hashCode() {
return this.details.hashCode();
}
@Override
public String toString() {
return getDetails().toString();
}
public static class Builder {
private final Map<String, Object> content;
public Builder() {
this.content = new LinkedHashMap<String, Object>();
}
public Builder withDetail(String key, Object value) {
this.content.put(key, value);
return this;
}
public Builder withDetails(Map<String, Object> details) {
this.content.putAll(details);
return this;
}
public Info build() {
return new Info(this);
}
}
}
EnvironmentInfoContributor
EnvironmentInfoContributor–> 一個提供所有environment 屬性中字首為info的InfoContributor.
程式碼如下:
public class EnvironmentInfoContributor implements InfoContributor {
private final PropertySourcesBinder binder;
public EnvironmentInfoContributor(ConfigurableEnvironment environment) {
this.binder = new PropertySourcesBinder(environment);
}
@Override
public void contribute(Info.Builder builder) {
// 通過PropertySourcesBinder 將info為字首的環境變數抽取出來
builder.withDetails(this.binder.extractAll("info"));
}
}
MapInfoContributor
程式碼如下:
public class MapInfoContributor implements InfoContributor {
private final Map<String, Object> info;
public MapInfoContributor(Map<String, Object> info) {
this.info = new LinkedHashMap<String, Object>(info);
}
@Override
public void contribute(Info.Builder builder) {
builder.withDetails(this.info);
}
}
該類沒有自動裝配
SimpleInfoContributor
程式碼如下:
public class SimpleInfoContributor implements InfoContributor {
private final String prefix;
private final Object detail;
public SimpleInfoContributor(String prefix, Object detail) {
Assert.notNull(prefix, "Prefix must not be null");
this.prefix = prefix;
this.detail = detail;
}
@Override
public void contribute(Info.Builder builder) {
if (this.detail != null) {
builder.withDetail(this.prefix, this.detail);
}
}
該類沒有自動裝配
InfoPropertiesInfoContributor
InfoPropertiesInfoContributor –> 一個暴露InfoProperties的InfoContributor.其泛型引數為T extends InfoProperties
欄位,構造器如下:
private final T properties; // 暴露的模式 private final Mode mode; protected InfoPropertiesInfoContributor(T properties, Mode mode) { this.properties = properties; this.mode = mode; }
Mode是1個列舉,程式碼如下:
public enum Mode { // 暴露所有的資訊 FULL, // 只暴露預設的資訊 SIMPLE }
其聲明瞭如下幾個方法:
generateContent –> 抽取出內容為info endpoint 使用.程式碼如下:
protected Map<String, Object> generateContent() { // 1. 根據模式的不同暴露出所有的資料 Map<String, Object> content = extractContent(toPropertySource()); // 2. 預設空實現,子類可複寫 postProcessContent(content); return content; }
根據模式的不同暴露出所有的資料.
toPropertySource方法實現如下:
protected PropertySource<?> toPropertySource() { // 如果模式為FULL,則返回所有的資料,否則,只暴露預設的資訊 if (this.mode.equals(Mode.FULL)) { return this.properties.toPropertySource(); } return toSimplePropertySource(); }
- 如果模式為FULL,則返回所有的資料,否則,只暴露預設的資訊
返回PropertySource–>SIMPLE 模式,抽象方法,子類實現.程式碼如下:
protected abstract PropertySource<?> toSimplePropertySource();
extractContent程式碼如下:
protected Map<String, Object> extractContent(PropertySource<?> propertySource) { return new PropertySourcesBinder(propertySource).extractAll(""); }
預設空實現,子類可複寫.程式碼如下:
protected void postProcessContent(Map<String, Object> content) { }
copyIfSet –> 如果properties中有配置key的話,則copy到target中.程式碼如下:
protected void copyIfSet(Properties target, String key) { String value = this.properties.get(key); if (StringUtils.hasText(value)) { target.put(key, value); } }
replaceValue –> 替換值.程式碼如下:
protected void replaceValue(Map<String, Object> content, String key, Object value) { if (content.containsKey(key) && value != null) { content.put(key, value); } }
getNestedMap –> 獲得巢狀的map 如果map中有給定key的話,否則返回empty map.程式碼如下:
protected Map<String, Object> getNestedMap(Map<String, Object> map, String key) { Object value = map.get(key); if (value == null) { return Collections.emptyMap(); } return (Map<String, Object>) value; }
InfoProperties
InfoProperties實現了Iterable介面,其泛型為InfoProperties.Entry.Entry如下:
public final class Entry {
private final String key;
private final String value;
private Entry(String key, String value) {
this.key = key;
this.value = value;
}
public String getKey() {
return this.key;
}
public String getValue() {
return this.value;
}
}
欄位構造器如下:
private final Properties entries; public InfoProperties(Properties entries) { Assert.notNull(entries, "Entries must not be null"); this.entries = copy(entries); }
copy 方法如下:
private Properties copy(Properties properties) { Properties copy = new Properties(); copy.putAll(properties); return copy; }
其它方法實現如下:
get,如下:
public String get(String key) { return this.entries.getProperty(key); }
getDate,如下:
public Date getDate(String key) { String s = get(key); if (s != null) { try { return new Date(Long.parseLong(s)); } catch (NumberFormatException ex) { // Not valid epoch time } } return null; }
iterator,如下:
public Iterator<Entry> iterator() { return new PropertiesIterator(this.entries); }
PropertiesIterator 程式碼如下:
private final class PropertiesIterator implements Iterator<Entry> { private final Iterator<Map.Entry<Object, Object>> iterator; private PropertiesIterator(Properties properties) { this.iterator = properties.entrySet().iterator(); } @Override public boolean hasNext() { return this.iterator.hasNext(); } @Override public Entry next() { Map.Entry<Object, Object> entry = this.iterator.next(); return new Entry((String) entry.getKey(), (String) entry.getValue()); } @Override public void remove() { throw new UnsupportedOperationException("InfoProperties are immutable."); } }
toPropertySource,程式碼如下:
public PropertySource<?> toPropertySource() { return new PropertiesPropertySource(getClass().getSimpleName(), copy(this.entries)); }
BuildProperties
BuildProperties–> 繼承自InfoProperties,提供專案構建相關的資訊
構造器如下:
public BuildProperties(Properties entries) { super(processEntries(entries)); }
processEntries–>從給的Properties 將time所對應的值轉換為時間戳的格式.程式碼如下:
private static Properties processEntries(Properties properties) { coerceDate(properties, "time"); return properties; } private static void coerceDate(Properties properties, String key) { String value = properties.getProperty(key); if (value != null) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); try { String updatedValue = String.valueOf(format.parse(value).getTime()); properties.setProperty(key, updatedValue); } catch (ParseException ex) { // Ignore and store the original value } } }
其他方法,都是最終呼叫InfoProperties#get,如下:
public String getGroup() { return get("group"); } public String getArtifact() { return get("artifact"); } public String getName() { return get("name"); } public String getVersion() { return get("version"); } public Date getTime() { return getDate("time"); }
GitProperties
GitProperties–> 繼承自InfoProperties,提供git相關的資訊比如 commit id 和提交時間。
構造器如下:
public GitProperties(Properties entries) { super(processEntries(entries)); }
processEntries–>將git.properties中的commit.time,build.time 轉換為yyyy-MM-dd’T’HH:mm:ssZ 格式的資料.程式碼如下:
private static Properties processEntries(Properties properties) { coercePropertyToEpoch(properties, "commit.time"); coercePropertyToEpoch(properties, "build.time"); return properties; }
coercePropertyToEpoch程式碼如下:
private static void coercePropertyToEpoch(Properties properties, String key) { String value = properties.getProperty(key); if (value != null) { properties.setProperty(key, coerceToEpoch(value)); } }
coerceToEpoch –> 嘗試將給定的字串轉換為紀元時間.Git屬性資訊被指定為秒或使用yyyy-MM-dd’T’HH:mm:ssZ 格式的資料.程式碼如下:
private static String coerceToEpoch(String s) { Long epoch = parseEpochSecond(s); if (epoch != null) { return String.valueOf(epoch); } SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); try { return String.valueOf(format.parse(s).getTime()); } catch (ParseException ex) { return s; } } private static Long parseEpochSecond(String s) { try { return Long.parseLong(s) * 1000; } catch (NumberFormatException ex) { return null; } }
其他方法,都是最終呼叫InfoProperties#get,如下:
public String getBranch() { return get("branch"); } public String getCommitId() { return get("commit.id"); } public String getShortCommitId() { // 1. 獲得commit.id.abbrev String shortId = get("commit.id.abbrev"); if (shortId != null) { return shortId; } // 2.commit.id,如果不等於null並且id長度大於7,則擷取前7位 String id = getCommitId(); if (id == null) { return null; } return (id.length() > 7 ? id.substring(0, 7) : id); } public Date getCommitTime() { return getDate("commit.time"); }
BuildInfoContributor
BuildInfoContributor–>繼承自InfoPropertiesInfoContributor,將BuildProperties暴露出去
構造器如下:
public BuildInfoContributor(BuildProperties properties) { super(properties, Mode.FULL); }
方法實現如下:
contribute,程式碼如下:
public void contribute(Info.Builder builder) { builder.withDetail("build", generateContent()); }
由於BuildInfoContributor預設的Mode為FULL,因此該方法最終會將BuildProperties中的所有資料暴露出去,其key為build
toSimplePropertySource–> 當BuildInfoContributor的Mode為SIMPLE時呼叫,一般不會呼叫該方法程式碼如下:
protected PropertySource<?> toSimplePropertySource() { Properties props = new Properties(); // 1. 預設讀取META-INF/build-info.properties中的build.group copyIfSet(props, "group"); copyIfSet(props, "artifact"); copyIfSet(props, "name"); copyIfSet(props, "version"); copyIfSet(props, "time"); return new PropertiesPropertySource("build", props); }
- 將META-INF/build-info.properties中的build.group, build.artifact, build.name, build.version, build.time 複製到Properties中
- 例項化PropertiesPropertySource,名字為build
postProcessContent–> 將build.time 轉換為time.程式碼如下:
protected void postProcessContent(Map<String, Object> content) { // 將build.time 轉換為time replaceValue(content, "time", getProperties().getTime()); }
GitInfoContributor
GitInfoContributor–>繼承自InfoPropertiesInfoContributor,將GitProperties暴露出去
構造器如下:
public GitInfoContributor(GitProperties properties, Mode mode) { // 預設是SIMPLE super(properties, mode); } public GitInfoContributor(GitProperties properties) { this(properties, Mode.SIMPLE); }
方法實現如下:
contribute–>當Mode為full時呼叫,由於預設是SIMPLE,因此該方法一般不會呼叫.程式碼如下:
public void contribute(Info.Builder builder) { builder.withDetail("git", generateContent()); }
toSimplePropertySource–> 預設呼叫,程式碼如下:
protected PropertySource<?> toSimplePropertySource() { Properties props = new Properties(); // 1. 從git.properties中獲得branch copyIfSet(props, "branch"); // 2. 從git.properties中獲得commit.id.abbrev String commitId = getProperties().getShortCommitId(); if (commitId != null) { props.put("commit.id", commitId); } // 2. 從git.properties中獲得commit.time copyIfSet(props, "commit.time"); return new PropertiesPropertySource("git", props); }
- 從git.properties中獲得branch,複製到props中
- 從git.properties中獲得commit.id.abbrev,複製到props中
- 從git.properties中獲得commit.time,複製到props中
- 例項化 PropertiesPropertySource,名字為git
postProcessContent,程式碼如下:
protected void postProcessContent(Map<String, Object> content) { // 1. 獲得commit所對應的map中time所對應的值,將其轉換為Date,然後將其進行替換 replaceValue(getNestedMap(content, "commit"), "time", getProperties().getCommitTime()); // 2. 獲得build所對應的map中time所對應的值,將其轉換為Date,然後將其進行替換 replaceValue(getNestedMap(content, "build"), "time", getProperties().getDate("build.time")); }
- 獲得commit所對應的map中time所對應的值,將其轉換為Date,然後將其進行替換
- 獲得build所對應的map中time所對應的值,將其轉換為Date,然後將其進行替換
自動裝配
InfoProperties
InfoProperties相關的類–>GitProperties,BuildProperties的自動裝配是在ProjectInfoAutoConfiguration中, ProjectInfoAutoConfiguration聲明瞭如下註解:
@Configuration
@EnableConfigurationProperties(ProjectInfoProperties.class)
ProjectInfoProperties程式碼如下:
@ConfigurationProperties(prefix = "spring.info")
public class ProjectInfoProperties {
// 構建的具體的資訊,預設載入路徑為META-INF/build-info.properties
private final Build build = new Build();
// git具體的資訊,預設載入路徑為classpath:git.properties
private final Git git = new Git();
public Build getBuild() {
return this.build;
}
public Git getGit() {
return this.git;
}
/**
* Make sure that the "spring.git.properties" legacy key is used by default.
* @param defaultGitLocation the default git location to use
*/
@Autowired
void setDefaultGitLocation(
@Value("${spring.git.properties:classpath:git.properties}") Resource defaultGitLocation) {
getGit().setLocation(defaultGitLocation);
}
/**
* Build specific info properties.
*/
public static class Build {
/**
* Location of the generated build-info.properties file.
*/
private Resource location = new ClassPathResource(
"META-INF/build-info.properties");
public Resource getLocation() {
return this.location;
}
public void setLocation(Resource location) {
this.location = location;
}
}
/**
* Git specific info properties.
*/
public static class Git {
/**
* Location of the generated git.properties file.
*/
private Resource location;
public Resource getLocation() {
return this.location;
}
public void setLocation(Resource location) {
this.location = location;
}
}
}
其中Build的預設配置為META-INF/build-info.properties,Git的預設配置為classpath:git.properties
可通過如下屬性來配置:
spring.info.build.location=classpath:META-INF/build-info.properties # Location of the generated build-info.properties file.
spring.info.git.location=classpath:git.properties # Location of the generated git.properties file.
–
在ProjectInfoAutoConfiguration中宣告2個@Bean方法:
buildProperties,程式碼如下:
@ConditionalOnResource(resources = "${spring.info.build.location:classpath:META-INF/build-info.properties}") @ConditionalOnMissingBean @Bean public BuildProperties buildProperties() throws Exception { return new BuildProperties( loadFrom(this.properties.getBuild().getLocation(), "build")); }
@ConditionalOnResource(resources = “${spring.info.build.location:classpath:META-INF/build-info.properties}”)–>滿足如下條件時生效:
- 如果spring.info.build.location配置了,則如果spring.info.build.location:classpath配置路徑下存在資原始檔,則返回true
- 如果spring.info.build.location沒配置,則如果在classpath:META-INF/build-info.properties中存在的話,則生效
@ConditionalOnMissingBean –> BeanFactory中不存在BuildProperties型別的bean時生效
其建立BuildProperties時呼叫了loadFrom方法,來載入配置的檔案,並且將檔案中不是build開頭的配置進行過濾.
loadFrom–> 方法只加載給定location的Properties檔案中以prefix開頭的配置.如下:
protected Properties loadFrom(Resource location, String prefix) throws IOException { String p = prefix.endsWith(".") ? prefix : prefix + "."; Properties source = PropertiesLoaderUtils.loadProperties(location); Properties target = new Properties(); for (String key : source.stringPropertyNames()) { if (key.startsWith(p)) { target.put(key.substring(p.length()), source.get(key)); } } return target; }
gitProperties,程式碼如下:
@Conditional(GitResourceAvailableCondition.class) @ConditionalOnMissingBean @Bean public GitProperties gitProperties() throws Exception { return new GitProperties(loadFrom(this.properties.getGit().getLocation(), "git")); }
- @ConditionalOnMissingBean–> BeanFactory中不存在GitProperties型別的bean時生效
@Conditional(GitResourceAvailableCondition.class) –> 如果以下路徑中任意1個存在則生效:
- spring.info.git.location配置的路徑
- spring.git.properties配置的路徑
- classpath:git.properties配置的路徑
其建立GitProperties時呼叫了loadFrom方法,來載入配置的檔案,並且將檔案中不是git開頭的配置進行過濾.
InfoContributor
InfoContributor相關的子類的自動裝配在InfoContributorAutoConfiguration中進行了配置,其聲明瞭如下註解:
@Configuration
@AutoConfigureAfter(ProjectInfoAutoConfiguration.class)
@AutoConfigureBefore(EndpointAutoConfiguration.class)
@EnableConfigurationProperties(InfoContributorProperties.class)
InfoContributorProperties程式碼如下:
@ConfigurationProperties("management.info")
public class InfoContributorProperties {
private final Git git = new Git();
public Git getGit() {
return this.git;
}
public static class Git {
/**
* Mode to use to expose git information.
*/
private GitInfoContributor.Mode mode = GitInfoContributor.Mode.SIMPLE;
public GitInfoContributor.Mode getMode() {
return this.mode;
}
public void setMode(GitInfoContributor.Mode mode) {
this.mode = mode;
}
}
}
因此可以通過management.info.git.mode,來配置GitInfoContributor的輸出模式,預設為SIMPLE
–
InfoContributorAutoConfiguration聲明瞭3個bean方法:
envInfoContributor,程式碼如下:
@Bean @ConditionalOnEnabledInfoContributor("env") @Order(DEFAULT_ORDER) public EnvironmentInfoContributor envInfoContributor( ConfigurableEnvironment environment) { return new EnvironmentInfoContributor(environment); }
- @Bean –> 註冊1個id為envInfoContributor,型別為EnvironmentInfoContributor的bean
- @ConditionalOnEnabledInfoContributor(“env”) –> 如果配置有management.info.env .enabled= true或者配置有management.info.enabled = true. 或者沒有配置時預設匹配
gitInfoContributor,程式碼如下:
@Bean @ConditionalOnEnabledInfoContributor("git") @ConditionalOnSingleCandidate(GitProperties.class) @ConditionalOnMissingBean @Order(DEFAULT_ORDER) public GitInfoContributor gitInfoContributor(GitProperties gitProperties) { return new GitInfoContributor(gitProperties, this.properties.getGit().getMode()); }
- @Bean –> 註冊1個id為gitInfoContributor,型別為GitInfoContributor的bean
- @ConditionalOnEnabledInfoContributor(“git”) –> 如果配置有management.info.git.enabled= true或者配置有management.info.enabled = true. 或者沒有配置時預設匹配
- @ConditionalOnMissingBean–> BeanFactory中不存在型別為GitInfoContributor的bean時生效
- @ConditionalOnSingleCandidate(GitProperties.class) –> 如果BeanFactory中只存在1個GitProperties型別的bean或者存在多個,但是存在1個被指定為Primary的bean時生效
buildInfoContributor,程式碼如下:
@Bean @ConditionalOnEnabledInfoContributor("build") @ConditionalOnSingleCandidate(BuildProperties.class) @Order(DEFAULT_ORDER) public InfoContributor buildInfoContributor(BuildProperties buildProperties) { return new BuildInfoContributor(buildProperties); }
- @Bean –> 註冊1個id為buildInfoContributor,型別為InfoContributor的bean
- @ConditionalOnEnabledInfoContributor(“build”) –> 如果配置有management.info.build.enabled= true或者配置有management.info.enabled = true. 或者沒有配置時預設匹配
實戰
要先在spring boot的專案中啟用 BuildInfoContributor的配置,需要在META-INF目錄下存在build-info.properties,我們可以spring-boot-maven-plugin來完成,其有5個goal:
- spring-boot:repackage,預設goal。在mvn package之後,再次打包可執行的jar/war,同時保留mvn package生成的jar/war為.origin
- spring-boot:run,執行Spring Boot應用
- spring-boot:start,在mvn integration-test階段,進行Spring Boot應用生命週期的管理
- spring-boot:stop,在mvn integration-test階段,進行Spring Boot應用生命週期的管理
- spring-boot:build-info,生成Actuator使用的構建資訊檔案build-info.properties.預設輸出路徑為${project.build.outputDirectory}/META-INF/build-info.properties
因此,我們可以修改pom檔案為如下:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>build-info</goal> </goals> </execution> </executions> </plugin>
此時我們執行mvn: clean install ,就可以發現在最終生成的jar包中存在build-info.properties,如下:
同樣,要啟用GitInfoContributor,我們可以在pom檔案中加入如下配置:
<plugin> <groupId>pl.project13.maven</groupId> <artifactId>git-commit-id-plugin</artifactId> <version>2.1.15</version> <executions> <execution> <goals> <goal>revision</goal> </goals> </execution> </executions> <configuration> <dotGitDirectory>${project.basedir}/.git</dotGitDirectory> </configuration> </plugin>
執行mvn:git-commit-id:revision,就可以發現在最終生成的jar包中存在build-info.properties,如下:
-
{ git: { commit: { time: 1517484030000, id: "8973672" }, branch: "master" }, build: { version: "0.0.1-SNAPSHOT", artifact: "spring-boot-analysis", name: "spring-boot-analysis", group: "com.roncoo", time: 1517484169000 } }
當然我們可以配置management.info.git.mode=FULL,來輸出更多的資訊,試一下吧
參考連結: