您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
spring boot actuator info端点背后的密码
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
spring boot actuator info端点背后的密码
京东科技IoT团队
2021-01-07
IP归属:未知
194000浏览
微服务
我们平常访问/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解析](http://blog.csdn.net/qq_26000415/article/details/79060258)中有介绍,**InfoContributor最终是通过InfoEndpoint来调用的.** 同时, InfoEndpoint向EndpointHandlerMapping注册的方式是通过EndpointMvcAdapter的方式来完成的,关于这点,可以看[spring boot 源码解析55-spring boot actuator HandlerMapping全网独家揭秘](http://blog.csdn.net/qq_26000415/article/details/79231279) # 解析 关于这部分的类图如下: ![InfoContributor-class-uml](//img1.jcloudcs.com/developer.jdcloud.com/a1a41d9c-0a45-440c-b717-a1491a9180da20210106000205.png) ## 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 1. 字段,构造器如下: ``` 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 } ``` 2. 其声明了如下几个方法: 1. generateContent --> 抽取出内容为info endpoint 使用.代码如下: ``` protected Map<String, Object> generateContent() { // 1. 根据模式的不同暴露出所有的数据 Map<String, Object> content = extractContent(toPropertySource()); // 2. 默认空实现,子类可复写 postProcessContent(content); return content; } ``` 1. 根据模式的不同暴露出所有的数据. toPropertySource方法实现如下: ``` protected PropertySource<?> toPropertySource() { // 如果模式为FULL,则返回所有的数据,否则,只暴露预设的信息 if (this.mode.equals(Mode.FULL)) { return this.properties.toPropertySource(); } return toSimplePropertySource(); } ``` 1. 如果模式为FULL,则返回所有的数据,否则,只暴露预设的信息 2. 返回PropertySource-->SIMPLE 模式,抽象方法,子类实现.代码如下: ``` protected abstract PropertySource<?> toSimplePropertySource(); ``` extractContent代码如下: ``` protected Map<String, Object> extractContent(PropertySource<?> propertySource) { return new PropertySourcesBinder(propertySource).extractAll(""); } ``` 2. 默认空实现,子类可复写.代码如下: ``` protected void postProcessContent(Map<String, Object> content) { } ``` 2. 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); } } ``` 3. replaceValue --> 替换值.代码如下: ``` protected void replaceValue(Map<String, Object> content, String key, Object value) { if (content.containsKey(key) && value != null) { content.put(key, value); } } ``` 4. 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; } } ``` 1. 字段构造器如下: ``` 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; } ``` 2. 其它方法实现如下: 1. get,如下: ``` public String get(String key) { return this.entries.getProperty(key); } ``` 2. 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; } ``` 3. 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."); } } ``` 4. toPropertySource,代码如下: ``` public PropertySource<?> toPropertySource() { return new PropertiesPropertySource(getClass().getSimpleName(), copy(this.entries)); } ``` ## BuildProperties BuildProperties--> 继承自InfoProperties,提供项目构建相关的信息 1. 构造器如下: ``` 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 } } } ``` 2. 其他方法,都是最终调用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 和提交时间。 1. 构造器如下: ``` 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; } } ``` 2. 其他方法,都是最终调用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暴露出去 1. 构造器如下: ``` public BuildInfoContributor(BuildProperties properties) { super(properties, Mode.FULL); } ``` 2. 方法实现如下: 1. contribute,代码如下: ``` public void contribute(Info.Builder builder) { builder.withDetail("build", generateContent()); } ``` **由于BuildInfoContributor默认的Mode为FULL,因此该方法最终会将BuildProperties中的所有数据暴露出去,其key为build** 2. 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); } ``` 1. 将META-INF/build-info.properties中的build.group, build.artifact, build.name, build.version, build.time 复制到Properties中 2. 实例化PropertiesPropertySource,名字为build 3. postProcessContent--> 将build.time 转换为time.代码如下: ``` protected void postProcessContent(Map<String, Object> content) { // 将build.time 转换为time replaceValue(content, "time", getProperties().getTime()); } ``` ## GitInfoContributor GitInfoContributor-->继承自InfoPropertiesInfoContributor,将GitProperties暴露出去 1. 构造器如下: ``` public GitInfoContributor(GitProperties properties, Mode mode) { // 默认是SIMPLE super(properties, mode); } public GitInfoContributor(GitProperties properties) { this(properties, Mode.SIMPLE); } ``` 2. 方法实现如下: 1. contribute-->当Mode为full时调用,由于默认是SIMPLE,因此该方法一般不会调用.代码如下: ``` public void contribute(Info.Builder builder) { builder.withDetail("git", generateContent()); } ``` 2. 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); } ``` 1. 从git.properties中获得branch,复制到props中 2. 从git.properties中获得commit.id.abbrev,复制到props中 3. 从git.properties中获得commit.time,复制到props中 4. 实例化 PropertiesPropertySource,名字为git 3. 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")); } ``` 1. 获得commit所对应的map中time所对应的值,将其转换为Date,然后将其进行替换 2. 获得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方法: 1. 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}")-->满足如下条件时生效: 1. 如果spring.info.build.location配置了,则如果spring.info.build.location:classpath配置路径下存在资源文件,则返回true 2. 如果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; } ``` 2. 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个存在则生效: 1. spring.info.git.location配置的路径 2. spring.git.properties配置的路径 3. 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方法: 1. 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. 或者没有配置时默认匹配 2. 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时生效 3. 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. 或者没有配置时默认匹配 # 实战 1. 要先在spring boot的项目中激活 BuildInfoContributor的配置,需要在META-INF目录下存在build-info.properties,我们可以spring-boot-maven-plugin来完成,其有5个goal: 1. spring-boot:repackage,默认goal。在mvn package之后,再次打包可执行的jar/war,同时保留mvn package生成的jar/war为.origin 2. spring-boot:run,运行Spring Boot应用 3. spring-boot:start,在mvn integration-test阶段,进行Spring Boot应用生命周期的管理 4. spring-boot:stop,在mvn integration-test阶段,进行Spring Boot应用生命周期的管理 5. 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,如下: ![build-info](//img1.jcloudcs.com/developer.jdcloud.com/402f6cfd-df76-4952-81f7-abee5722ad1420210106000110.png) 2. 同样,要激活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-info](//img1.jcloudcs.com/developer.jdcloud.com/9ccc00a1-ca2c-423d-8c00-9a77ce89f29a20210106000247.png) 3. 此时,我们启动应用后,访问如下链接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,来输出更多的信息,试一下吧 参考链接: [spring-boot:build-info](https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/maven-plugin/build-info-mojo.html) [Spring Boot的Maven插件Spring Boot Maven plugin详解](http://blog.csdn.net/taiyangdao/article/details/75303181) [Maven插件之git-commit-id-plugin](http://blog.csdn.net/wangjunjun2008/article/details/10526151) > 作者:何佳瑞
原创文章,需联系作者,授权转载
上一篇:Nextjs中文文档
下一篇:Typescript合成Webpack中
相关文章
spring boot actuator info端点背后的密码
京东科技IoT团队
文章数
13
阅读量
110382
作者其他文章
01
前端 | 小程序横竖屏的坑和 rpx 布局方案
如何避免小程序开发过程中的那些“坑”
01
前端 | Chrome 80 中 Iframe cookie 无法携带的问题
Chrome 80 中 Iframe cookie 无法携带的问题求解过程。
01
NLU | 智能音箱语义理解——MDIS三合一模型
MDIS模型(Unified Model for Multiple Domain Intent and Slot)可以做到同时对话术进行领域分类、意图判断以及填槽。
01
前端 |数据大屏适配方案
数据大屏适配方案详解
最新回复
丨
点赞排行
共0条评论
京东科技IoT团队
文章数
13
阅读量
110382
作者其他文章
01
前端 | 小程序横竖屏的坑和 rpx 布局方案
01
前端 | Chrome 80 中 Iframe cookie 无法携带的问题
01
NLU | 智能音箱语义理解——MDIS三合一模型
01
前端 |数据大屏适配方案
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号