惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Full Disclosure
WordPress大学
WordPress大学
小众软件
小众软件
Cloudbric
Cloudbric
AWS News Blog
AWS News Blog
腾讯CDC
量子位
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Vulnerabilities – Threatpost
Scott Helme
Scott Helme
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Hacker News
The Hacker News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
IT之家
IT之家
Jina AI
Jina AI
Attack and Defense Labs
Attack and Defense Labs
S
SegmentFault 最新的问题
Simon Willison's Weblog
Simon Willison's Weblog
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
Last Week in AI
Last Week in AI
博客园 - 【当耐特】
Google Online Security Blog
Google Online Security Blog
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
罗磊的独立博客
L
LINUX DO - 最新话题
博客园 - Franky
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
The Last Watchdog
The Last Watchdog
J
Java Code Geeks
AI
AI
C
Cisco Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Cyber Attacks, Cyber Crime and Cyber Security
Cisco Talos Blog
Cisco Talos Blog
博客园 - 三生石上(FineUI控件)
雷峰网
雷峰网
Help Net Security
Help Net Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG
I
Intezer
S
Securelist

Maciej Walkowiak - Java & Spring

Blog Generating HTTP clients in Spring Boot application from OpenAPI spec PostgreSQL and UUID as primary key Dynamic Projections with Spring Data JPA Container logs with Spring Boot and Testcontainers Reified Generics in Java? Faster integration tests with reusable Testcontainers and Flyway Running one-time jobs with Quartz and Spring Boot The best way to use Testcontainers with Spring Boot Spring Boot & Flyway - clear database between integration tests What's new in Spring? Activate Maven Profile by Operating System Spring Boot with Thymeleaf and Tailwind CSS - Complete Guide How to publish a Java library to Maven Central - Complete Guide Docker Compose - waiting until containers are ready Single file Java applications with JBang Beautiful bash scripts with Gum Running Java on CRaC How to log PostgreSQL queries with Testcontainers Spring Boot 3.0 & GraalVM Native Image - not a free lunch Creating Spring Cloud Function projects with AWS SAM Loading classpath resources to String with a custom JUnit extension Creating Project Templates with Cookiecutter Auto-Registering JUnit 5 extensions Spring Boot component scanning without annotations Spring Cloud AWS 2.3 RC2 Released How I built vlad-cli - command line interface to Vlad Mihalcea The State of Java Relational Persistence On Choosing a Tech Stack
Listing Maven dependencies in Spring Boot Actuator Info endpoint
2022-07-26 · via Maciej Walkowiak - Java & Spring

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:

xml

<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>
  1. 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
  2. By default, it generates both XML and JSON files. We need just one.
  3. By default, generated file is located in target directory. We need to move it to classes so that it gets into the final jar.

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:

json

{
  "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.

java

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).

xml

<dependency>
	<groupId>org.cyclonedx</groupId>
	<artifactId>cyclonedx-core-java</artifactId>
	<version>7.2.0</version>
</dependency>

And then update the CycloneDxInfoContributor class with following:

java

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):

json

{
  "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

Let's stay in touch and follow me on Twitter: @maciejwalkowiak

Subscribe to RSS feed