解析
我们平常访问/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来调用的.
同时, InfoEndpoint向EndpointHandlerMapping注册的方式是通过EndpointMvcAdapter的方式来完成的,关于这点,可以看spring boot 源码解析55-spring boot actuator HandlerMapping全网独家揭秘
解析
关于这部分的类图如下:
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,如下:
此时,我们启动应用后,访问如下链接http://127.0.0.1:8080/info,就可以发现其返回内容如下:
{ 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,来输出更多的信息,试一下吧
参考链接: