【ライブラリ管理を効率化!】JavaプロジェクトでのBOMの生成・利用方法の解説

この記事では、Javaアプリケーション開発において、ライブラリ管理を効率化する手段としてBOM (Bill of Materials) の生成と利用方法について説明します。

BOMを利用するとversionが一元管理でき、アプリケーションごとに最適なversionが何か悩まなくて良くなります!

ひらべー
ひらべー

この記事を読むとこんな課題が軽減されます!

・どのversionを使えばいいのか迷う

・脆弱性対応でのversion upがめんどう

BOMの生成方法

この記事では、Gradleを用いたBOMの生成方法をご紹介します。

なぜGradle?
後述するメインフレームワークのversionごと複数のBOMを生成するのがやりやすいからです!

BOMを生成するリポジトリはbuild.gradleのみ以下のように記述すればOKです!

plugins {
    // bom生成に必要なプラグイン
    id 'java-platform'
    id 'maven-publish'
}

group = 'hirabay.bom'
version = '0.0.1'

javaPlatform {
    allowDependencies()
}

repositories {
    mavenCentral()
}

dependencies {
    // constraintsでくくるとbom利用者側でversion指定がいらなくなる
    // ライブラリを使用していない場合は何も起きない(勝手にライブラリが追加されたりしない)
    constraints {
        api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2'
        api 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.2'
    }

    // bomの中にbomを含めることも可能
    api platform('org.springframework.boot:spring-boot-dependencies:3.0.8')
    api platform('org.springframework.cloud:spring-cloud-dependencies:2022.0.3')
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            group = 'hirabay.bom' // 未指定の場合、プロジェクトのgroupを使用
            artifactId = 'sample-bom' // 未指定の場合、プロジェクト名を使用
            version = '0.0.1'  // 未指定の場合、プロジェクトのversionを使用
            from components.javaPlatform
        }
    }
}

この例で publishタスクを実行すると、hirabay.bom:sample-bom:0.0.1が利用可能になります!

※publishのこれ以上の設定は、publish先によって変わるため省略させていただきます。。

BOMを利用する

bomの利用はgradleの標準機能でも可能なのですが、io.spring.dependency-managementプラグインが便利です!

plugins {
	...
	
	// BOMの利用が便利になるプラグイン
	id 'io.spring.dependency-management' version '1.1.0'
}

...

repositories {
	mavenCentral()
	mavenLocal() // お試し検証ではMavenLocalを使うとスムーズ 
}

// 利用するBOMを定義する(bomを追加したい場合は、独自のbomに内包させると便利)
dependencyManagement {
	imports {
		mavenBom "hirabay.bom:sample-bom:0.0.1"
	}
}

dependencies {
	// BOMに含まれるライブラリであれば、versionを記載しなくて良い
	
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter'
	implementation 'org.springframework.cloud:spring-cloud-starter-gateway'

	testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

versionは指定していませんが、bom側で定義したversionでライブラリが取り込まれていることが確認できます!

※Intellij画面(左:External Libraries、右:build.gradle)

dependencyManagementというタグでbomをimportします。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.1.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>rhirabay.bom</groupId>
	<artifactId>maven-user</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>maven-user</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<!-- bomに定義されているライブラリはversionを記載しなくて良い -->
	<dependencies>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-gateway</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<!-- dependencyManagementタグでbomを定義-->
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>hirabay.bom</groupId>
				<artifactId>sample-bom</artifactId>
				<version>0.0.1</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
</project>

versionは指定していませんが、bom側で定義したversionでライブラリが取り込まれていることが確認できます!

※Intellij画面(左:External Libraries、右:pom.xml)

特定のアプリケーションの特定のライブラリだけ別versionを使う

bomによりライブラリのversionの一元管理ができましたが、場合によってはbomで定義したversion以外を使用したいケースもあると思います。

そんな時は、bom利用前と同様に例外的なライブラリだけversionを指定すればOKです!

※Intellij画面(左:External Libraries、右:build.gradle)

※Intellij画面(左:External Libraries、右:pom.xml)

メインフレームワークのversionごと、複数のbomを生成する

例えば、メインフレームワークとしてSpringBootを使っている場合、minor versionごとにサポートversionが異なるライブラリがあります。

そんなとき1つのリポジトリで複数のversionに対応したbomが生成できると便利ですよね!

そこで先ほど紹介したbomの生成方法を少しアレンジして、複数のversionに対応できるようにしてみます。

ext {
    // ライブラリのversionをmap形式で定義(keyはSpringBootのminor version)
    versionMap = [
            'org.mybatis.spring.boot:mybatis-spring-boot-starter': [
                    '2.7': '2.2.+',
                    '3.0': '3.0.+'
            ],
            'org.mybatis.spring.boot:mybatis-spring-boot-starter-test': [
                    '2.7': '2.2.+',
                    '3.0': '3.0.+'
            ],
    ]
    // bomのversionをmap形式で定義(keyはSpringBootのminor version)
    bomVersionMap = [
            'org.springframework.boot:spring-boot-dependencies': [
                    '2.7': '2.7.+',
                    '3.0': '3.0.+'
            ],
            'org.springframework.cloud:spring-cloud-dependencies': [
                    '2.7': '2021.0.+',
                    '3.0': '2022.0.+'
            ],
    ]
}

dependencies {
    constraints {
        // mapからライブラリを展開
        // SpringBootに依存しないライブラリを利用する場合を考慮し、
        // 「*」を指定するとminor versionに関係なく組み込み可能にしておく
        for (lib in versionMap.keySet()) {
            if (versionMap[lib]['*'] != null) {
                api "${lib}:${versionMap[lib]['*']}"
            } else if (versionMap[lib][springBootVersion] != null) {
                api "${lib}:${versionMap[lib][springBootVersion]}"
            }
        }
    }

    // mapからbomを展開
    // SpringBootに依存しないbomを利用する場合を考慮し、
    // 「*」を指定するとminor versionに関係なく組み込み可能にしておく
    for (bom in bomVersionMap.keySet()) {
        if (bomVersionMap[bom]['*'] != null) {
            api platform("${bom}:${bomVersionMap[bom]['*']}")
        } else if (bomVersionMap[bom][springBootVersion] != null) {
            api platform("${bom}:${bomVersionMap[bom][springBootVersion]}")
        }
    }
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            group = 'hirabay.bom'
            artifactId = 'sample-bom'

            // versionを日時を使って自動で生成
            def date = new Date()
            def format = new SimpleDateFormat("yyyyMMddHHmmss")
            version = "${springBootVersion}.${format.format(date)}"

            from components.javaPlatform
        }
    }
}

また、gradle.propertiesにデフォルトの変数springBootVersionを設定しておきます。

springBootVersion=3.0

これで準備はOK!

以下のようにspringBootVersionを指定することで、複数のbomを生成できるようになりました!

$ ./gradlew publish -PspringBootVersion=2.7

$ ./gradlew publish -PspringBootVersion=3.0

ブランチ分けるではダメなの?
ダメではないです!
ただ、ブランチ間のライブラリの生合成を揃えるのが大変に感じるので
個人的には1ブランチ内で複数version対応できた方が管理が楽に感じます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です