Spring Boot Actuator endpoints offer a way to get insights from the running Spring Boot application. Out of the box, /actuator/info
endpoint exposes information about the operating system the app is running on, the JVM version or Git information.
In this short article you'll learn how you can add a list of the Maven dependencies used in your project to the info endpoint. Actually, you'll learn how to add much more information than just the dependencies.
Similar to getting Git information, we need to hook into the build process to generate the dependency list from pom.xml
and put it into target/classes
directory so that it gets packaged into application jar. Sounds a bit tedious? For sure it would be, but lucklily we don't need to do it manually as there are ready to use solutions for that. One of them is CycloneDX Maven Plugin.
CycloneDX
CycloneDX in fact is much more than a plugin that gathers list of dependencies. It's a cross platform standard for Software Bill Of Materials. I'll not go deeper into it, as it's not relevant for this article - we're just going to use it to extract the list of the dependencies.
Configure CycloneDX Maven Plugin
In your pom.xml
plugin section, add CycloneDX Maven Plugin:
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.7.1</version>
<executions>
<execution>
<phase>validate</phase> (1)
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
<configuration>
<outputFormat>json</outputFormat> (2)
<outputName>classes/bom</outputName> (3)
</configuration>
</plugin>
- Plugin has to be executed early in the process - especially before tha package phase - so that the result of running this plugin gets into final
jar
file - By default, it generates both XML and JSON files. We need just one.
- By default, generated file is located in
target
directory. We need to move it toclasses
so that it gets into the finaljar
.
For more sophisticated configuration options, check the plugin page.
Now, once you execute ./mvnw package
, you'll find in target/classes/bom.json
quite a big file containing serious amount of information about dependencies including licenses, checksums. I stripped it out heavily just to give you a feeling how it looks, as the original file even for relatively small project is over 1k lines long:
{
"bomFormat" : "CycloneDX",
"specVersion" : "1.4",
"serialNumber" : "urn:uuid:42fafc34-3467-4253-a4f5-41b7a9b26f76",
"version" : 1,
"metadata" : {
"timestamp" : "2022-07-25T10:59:44Z",
"tools" : [
...
],
"component" : {
...
}
},
"components" : [
{
"publisher" : "Pivotal Software, Inc.",
"group" : "org.springframework.boot",
"name" : "spring-boot-starter-actuator",
"version" : "2.7.2",
"description" : "Starter for using Spring Boot's Actuator which provides production ready features to help you monitor and manage your application",
"scope" : "optional",
"hashes" : [
{
"alg" : "MD5",
"content" : "46f58ea5c19a4248ebbd2126320d1b07"
},
{
"alg" : "SHA-1",
"content" : "8a87caacd6d4f10c542f3bbafe20828f48b1119f"
},
{
"alg" : "SHA-256",
"content" : "13ec81781150bf7c68d93a756c715313f0036422bac9eaa06dfa02af9e9661be"
},
{
"alg" : "SHA-512",
"content" : "a319ebe51846e94a49b3123d00837ed603ad1f42802d584077d4fc2be493d6d8d3838e0419ced58048d4fe629904f5e651705f1d346f6feb6eacbd2934bbeade"
},
{
"alg" : "SHA-384",
"content" : "befdeaaf049b3c6b43518b15123a36476dd551dfe26144046db47d0aec581c287591461848f48e2c32caba2c6342387b"
},
{
"alg" : "SHA3-384",
"content" : "d6ad532c861a50f0ddca5e7903ae5cb52fc6749f0c7b91b03fc7b36de703a605150d29e14cf1780ecf353d17febe5e5e"
},
{
"alg" : "SHA3-256",
"content" : "a9460987319d96a144336ec182d56d1b670c0e1b5fd0baee3f4cc101ced97d2e"
},
{
"alg" : "SHA3-512",
"content" : "fcf41d875e1e7955005320b521c5e23856bd2809b5a554da03e505ae6b6808efc1a006ced43bd1c8de92c5d7b259131e049f730c6980b474354276cd0884558f"
}
],
"licenses" : [
{
"license" : {
"id" : "Apache-2.0"
}
}
],
"purl" : "pkg:maven/org.springframework.boot/spring-boot-starter-actuator@2.7.2?type=jar",
"externalReferences" : [
{
"type" : "website",
"url" : "https://spring.io"
},
{
"type" : "issue-tracker",
"url" : "https://github.com/spring-projects/spring-boot/issues"
},
{
"type" : "vcs",
"url" : "https://github.com/spring-projects/spring-boot"
}
],
"type" : "library",
"bom-ref" : "pkg:maven/org.springframework.boot/spring-boot-starter-actuator@2.7.2?type=jar"
}
// ... the rest of the dependencies
]
}
Info Contributor
Once file gets generated, we need a piece of Java code that will take information from this file and put it into what's returned in /actuator/info
endpoint.
INFO
Do not forget to expose info endpoint in application.properties
file:
management.endpoints.web.exposure.include=info
Spring Boot offers a simple way to do it by creating bean implementing InfoContributor
interface.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
@Component
class CycloneDxInfoContributor implements InfoContributor, InitializingBean {
private final Resource bomFile;
private final ObjectMapper objectMapper = new ObjectMapper();
private @Nullable JsonNode bom;
SimpleCycloneDxInfoContributor(@Value("classpath:bom.json") Resource bomFile) {
this.bomFile = bomFile;
}
@Override
public void contribute(Info.Builder builder) {
if (bom != null) {
builder.withDetail("bom", bom);
}
}
@Override
public void afterPropertiesSet() throws Exception {
if (bomFile.exists()) {
try (var is = bomFile.getInputStream()) {
this.bom = objectMapper.readTree(is);
}
}
}
}
This piece of code will expose everything that plugin generated in the /actuator/info
endpoint.
If that's what you need - great - but it's likely more than you actually need. Assuming we need just the list of structures like group id, artifact id and version, we can filter greater part of what's currently returned.
Listing the dependencies
To filter out the noise, we need to deserialize JSON to something more meaningful and then filter and map to object structures we are interested in. While we could create classes that represent the JSON structure, we can save ourselves time by adding the dependency containing the CycloneDX model (but note that it does contain few transitive dependencies).
<dependency>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-core-java</artifactId>
<version>7.2.0</version>
</dependency>
And then update the CycloneDxInfoContributor
class with following:
import java.util.List;
import java.util.stream.Collectors;
import org.cyclonedx.parsers.JsonParser;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
@Component
class CycloneDxInfoContributor implements InfoContributor, InitializingBean {
private final Resource bomFile;
private final JsonParser jsonParser = new JsonParser();
private @Nullable List<Dependency> dependencies;
CycloneDxInfoContributor(@Value("classpath:bom.json") Resource bomFile) {
this.bomFile = bomFile;
}
@Override
public void contribute(Info.Builder builder) {
if (dependencies != null) {
builder.withDetail("dependencies", dependencies);
}
}
@Override
public void afterPropertiesSet() throws Exception {
if (bomFile.exists()) {
try (var is = bomFile.getInputStream()) {
var bom = jsonParser.parse(is);
this.dependencies = bom.getComponents()
.stream()
.map(Dependency::new)
.collect(Collectors.toList());
}
}
}
record Dependency(String groupId, String artifactId, String version){
Dependency(org.cyclonedx.model.Component component) {
this(component.getGroup(), component.getName(), component.getVersion());
}
}
}
One important thing to note here is that we should not use Spring Boot managed ObjectMapper
for deserialization because its configuration may not reflect how the CycloneDX JSON file was generated. Instead, it's safer to use Cyclone's JsonParser
class.
Once you hit now the /actuator/info
endpoint, you'll get very concise list of the dependencies (including transitive dependencies):
{
"dependencies": [
{
"groupId": "org.springframework.boot",
"artifactId": "spring-boot-starter-actuator",
"version": "2.7.2"
},
{
"groupId": "org.springframework.boot",
"artifactId": "spring-boot-starter",
"version": "2.7.2"
},
{
"groupId": "org.springframework.boot",
"artifactId": "spring-boot",
"version": "2.7.2"
},
{
"groupId": "org.springframework.boot",
"artifactId": "spring-boot-autoconfigure",
"version": "2.7.2"
},
{
"groupId": "org.springframework.boot",
"artifactId": "spring-boot-starter-logging",
"version": "2.7.2"
},
// ... the rest of the dependencies
]
}
Conclusion
CycloneDX is a standardized way to describe Software Bill Of Materials, and we can use its Maven plugin to generate the list of the dependencies exposed by Spring Boot Actuator in the info
endpoint. In addition to the dependencies coordinates, we can also list licenses, checksums and links to project websites.
If you need something like that but you use Gradle, instead of using Maven plugin go with CycloneDX Gradle Plugin.
Sources for this tutorial are available at: https://github.com/maciejwalkowiak/cyclonedx-spring-boot-sample