これは、なにをしたくて書いたもの?
JavaでUUIDを使おうと思ったら、java.util.UUID#randomUUID#toString
でバージョン4のUUIDを使っていると思います。
でも、UUIDってバージョン4以外にも種類がありましたよね、ということで、Javaでバージョン4以外のUUIDを扱えるライブラリーを
調べてみました。
UUIDのバージョン
そもそも、UUIDについて少し見ておきましょう。UUIDのRFCはこちらです。
RFC 4122: A UUID URN Namespace
UUIDは128ビットの数字で、一意の識別子となるものです。以下のような感じですね。
jshell> java.util.UUID.randomUUID().toString() $1 ==> "a3240b70-d32d-4551-86d2-3bf5fc07649f"
UUIDの形式は以下となっています。
The formal definition of the UUID string representation is provided by the following ABNF [7]: UUID = time-low "-" time-mid "-" time-high-and-version "-" clock-seq-and-reserved clock-seq-low "-" node time-low = 4hexOctet time-mid = 2hexOctet time-high-and-version = 2hexOctet clock-seq-and-reserved = hexOctet clock-seq-low = hexOctet node = 6hexOctet hexOctet = hexDigit hexDigit hexDigit = "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "a" / "b" / "c" / "d" / "e" / "f" / "A" / "B" / "C" / "D" / "E" / "F"
time-high-and-versionフィールドにはバージョンが含まれていて、-
区切りの3つ目の最初の文字はバージョン番号になります。
The version number is in the most significant 4 bits of the time
stamp (bits 4 through 7 of the time_hi_and_version field).
たとえば、先ほどの例だと4551
の部分の4
がタイプ4であることを表しています。
jshell> java.util.UUID.randomUUID().toString() $1 ==> "a3240b70-d32d-4551-86d2-3bf5fc07649f"
RFC 4122では、5つのバージョンのUUIDが定められています。
- バージョン1
- タイムスタンプとMACアドレスを元にしたもの
- バージョン2
- DCEセキュリティUUIDと呼ばれ、バージョン1の一部をPOSIXのユーザーIDやグループIDで差し替えたもの
- バージョン3
- バージョン4
- 乱数から生成したもの
- バージョン5
そしてさらに、ドラフト段階のものが提案されていて、この中にはバージョン6、7、8が含まれています。
A Universally Unique IDentifier (UUID) URN Namespace
GitHub - uuid6/uuid6-ietf-draft: Next Generation UUID Formats
この中では、UUIDは一意である特性を活かしてデータベースのキーとしてよく使われるものの、オートインクリメントなものとしては
使えないということが言われています。
One area UUIDs have gained popularity is as database keys. This stems from the increasingly distributed nature of modern applications. In such cases, "auto increment" schemes often used by databases do not work well, as the effort required to coordinate unique numeric identifiers across a network can easily become a burden. The fact that UUIDs can be used to create unique, reasonably short values in distributed systems without requiring synchronization makes them a good alternative, but UUID versions 1-5 lack certain other desirable characteristics
そして、ソート可能な識別子を定めることが動機になっています。
Due to the aforementioned issue, many widely distributed database applications and large application vendors have sought to solve the problem of creating a better time-based, sortable unique identifier for use as a database key. This has lead to numerous implementations over the past 10+ years solving the same problem in slightly different ways.
それぞれのバージョンの特性は、以下です。
- バージョン6
- バージョン1を改善し、タイムスタンプの情報をソート可能な形に見直したもの
- バージョン7
- バージョン8
- 実験的またはベンダー固有のユースケース向けのもの
バージョン8は、ちょっと扱いが違いますね。
まだドラフト段階ですが、バージョン7がよく使われることになりそうですね。
なお、ソート可能な一意なIDなら、ULIDというものもあります。
JavaでULIDを使いたい(Sulky ULIDを使う) - CLOVER🍀
JavaでのUUIDの選択肢
ここでJavaでのUUIDの選択肢を探してみると、以下のような感じでした。
- UUID (Java SE 17 & JDK 17)
- バージョン4
- Commons Id - UUID Documentation
- Apache Commons Id
- バージョン1、3、4、5
- UUID – About
- バージョン1
- https://johannburkard.de/software/uuid/
- バージョン1
- GitHub - f4b6a3/uuid-creator: A Java library for generating Universally Unique Identifiers (UUID).
- バージョン1、2、3、4、5、6、7
- GitHub - cowtowncoder/java-uuid-generator: Java Uuid Generator (JUG) is a library for generating all (3) types of UUIDs on Java. See (http://github.com/tlaukkan/mono-uuid-generator) for C#-based sister project!
- バージョン1、4、5、6、7
- 作者はJacksonの開発者
更新状態などを見て、実際に使おうと思うと以下あたりから絞り込むのかなと思います。
- UUID (Java SE 17 & JDK 17)
- GitHub - f4b6a3/uuid-creator: A Java library for generating Universally Unique Identifiers (UUID).
- GitHub - cowtowncoder/java-uuid-generator: Java Uuid Generator (JUG) is a library for generating all (3) types of UUIDs on Java. See (http://github.com/tlaukkan/mono-uuid-generator) for C#-based sister project!
今回は、Java Uuid Generator(JUG)を試してみます。
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.8 2023-07-18 OpenJDK Runtime Environment (build 17.0.8+7-Ubuntu-122.04) OpenJDK 64-Bit Server VM (build 17.0.8+7-Ubuntu-122.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.4 (dfbb324ad4a7c8fb0bf182e6d91b0ae20e3d2dd9) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.8, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-78-generic", arch: "amd64", family: "unix"
Java Uuid Generator(JUG)を使う
Java Uuid Generator(JUG)のGitHubリポジトリーは、こちら。
ドキュメントはREADME.md
のみで、WikiからはJavadocをたどることができます。
Home · cowtowncoder/java-uuid-generator Wiki · GitHub
使い方は、こちら。
Java Uuid Generator (JUG) / Usage
依存関係を追加して
Java Uuid Generator (JUG) / Usage / Maven Dependency
こちらに従ってUUIDを生成するライブラリーとして使います。
Java Uuid Generator (JUG) / Usage / Using JUG as Library
また、単体でCLIとしても使えるようです。
Java Uuid Generator (JUG) / Usage / Using JUG as CLI
今回は、ライブラリーとして使ってみます。
準備
Maven依存関係等はこちら。
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>com.fasterxml.uuid</groupId> <artifactId>java-uuid-generator</artifactId> <version>4.2.0</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.24.2</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>3.1.2</version> </plugin> </plugins> </build>
Java Uuid Generator(JUG)に必要な依存関係は、こちらですね。
<dependency> <groupId>com.fasterxml.uuid</groupId> <artifactId>java-uuid-generator</artifactId> <version>4.2.0</version> </dependency>
確認はテストコードでやるので、JUnit 5とAssertJを入れています。
使ってみる
では、Java Uuid Generator(JUG)を使ってみます。
テストコードの雛形はこちら。
src/test/java/org/littlewings/uuid/JugGettingStartedTest.java
package org.littlewings.uuid; import com.fasterxml.uuid.Generators; import com.fasterxml.uuid.impl.*; import org.junit.jupiter.api.Test; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; class JugGettingStartedTest { // ここに、テストを書く! }
以下のREADME.md
にも書かれているように、起点はcom.fasterxml.uuid.Generators
になります。
Java Uuid Generator (JUG) / Usage / Using JUG as LibraryGenerating UUIDs
こんな感じですね。
@Test void simple() { UUID uuidV1 = Generators.timeBasedGenerator().generate(); System.out.printf("UUID v1 = %s%n", uuidV1.toString()); assertThat(uuidV1.toString()).containsPattern("^[^-]+-[^-]+-1[^-]+-[^-]+-[^-]+$"); UUID uuidV4 = Generators.randomBasedGenerator().generate(); System.out.printf("UUID v4 = %s%n", uuidV4.toString()); assertThat(uuidV4.toString()).containsPattern("^[^-]+-[^-]+-4[^-]+-[^-]+-[^-]+$"); UUID uuidV5 = Generators.nameBasedGenerator().generate("source name"); System.out.printf("UUID v5 = %s%n", uuidV5.toString()); assertThat(uuidV5.toString()).containsPattern("^[^-]+-[^-]+-5[^-]+-[^-]+-[^-]+$"); UUID uuidV6 = Generators.timeBasedReorderedGenerator().generate(); System.out.printf("UUID v6 = %s%n", uuidV6.toString()); assertThat(uuidV6.toString()).containsPattern("^[^-]+-[^-]+-6[^-]+-[^-]+-[^-]+$"); UUID uuidV7 = Generators.timeBasedEpochGenerator().generate(); System.out.printf("UUID v7 = %s%n", uuidV7.toString()); assertThat(uuidV7.toString()).containsPattern("^[^-]+-[^-]+-7[^-]+-[^-]+-[^-]+$"); }
Generators
から目的にあったcom.fasterxml.uuid.UUIDGenerator
クラスのサブクラスのインスタンスを生成し、generate
メソッドを
使うことでUUID
が得られます。
System.out.printf
している結果はこちらです。
UUID v1 = acaffd7b-3465-11ee-a8c8-d57e5908feb4 UUID v4 = da265cbd-2c53-46fa-a65c-bd2c53a6fa22 UUID v5 = 6c7475b2-9251-5490-82ca-c2d1a2af1b98 UUID v6 = 1ee3465a-cb8f-6e2c-a8c8-1d53d7fe1737 UUID v7 = 0189cb40-f3ea-74f8-b863-7f05ac9cd0c8
正規表現で確認していますが、-
区切りの3つ目の先頭はバージョン番号になっていますね。
ちょっと驚いたのは、生成されるUUIDはjava.util.UUID
のインスタンスだということです。独自のクラスになっているのかな?と思って
いたのですが、そうではありませんでした。
各com.fasterxml.uuid.UUIDGenerator
クラスのインスタンスは、こんな感じに保持して使いまわすことができます。
@Test void generator() { TimeBasedGenerator timeBasedGenerator = Generators.timeBasedGenerator(); assertThat(timeBasedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-1[^-]+-[^-]+-[^-]+$"); assertThat(timeBasedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-1[^-]+-[^-]+-[^-]+$"); RandomBasedGenerator randomBasedGenerator = Generators.randomBasedGenerator(); assertThat(randomBasedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-4[^-]+-[^-]+-[^-]+$"); assertThat(randomBasedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-4[^-]+-[^-]+-[^-]+$"); NameBasedGenerator nameBasedGenerator = Generators.nameBasedGenerator(); assertThat(nameBasedGenerator.generate("source name1").toString()).containsPattern("^[^-]+-[^-]+-5[^-]+-[^-]+-[^-]+$"); assertThat(nameBasedGenerator.generate("source name2").toString()).containsPattern("^[^-]+-[^-]+-5[^-]+-[^-]+-[^-]+$"); TimeBasedReorderedGenerator timeBasedReorderedGenerator = Generators.timeBasedReorderedGenerator(); assertThat(timeBasedReorderedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-6[^-]+-[^-]+-[^-]+$"); assertThat(timeBasedReorderedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-6[^-]+-[^-]+-[^-]+$"); TimeBasedEpochGenerator timeBasedEpochGenerator = Generators.timeBasedEpochGenerator(); assertThat(timeBasedEpochGenerator.generate().toString().toString()).containsPattern("^[^-]+-[^-]+-7[^-]+-[^-]+-[^-]+$"); assertThat(timeBasedEpochGenerator.generate().toString().toString()).containsPattern("^[^-]+-[^-]+-7[^-]+-[^-]+-[^-]+$"); }
README.md
によると、各Generatorはスレッドセーフだそうです。
Generators are fully thread-safe, so a single instance may be shared among multiple threads.
com.fasterxml.uuid.Generators
のJavadocも見ておくとよいでしょう。
Generators (Java UUID Generator 4.2.0 API)
一意なIDが生成されることを確認する
最後に、大量のUUID生成+マルチスレッドで一意なIDが作成できているか、簡単に確認するテストを書いてみます。
src/test/java/org/littlewings/uuid/CreatedUUIDsTest.java
package org.littlewings.uuid; import com.fasterxml.uuid.Generators; import com.fasterxml.uuid.impl.*; import org.junit.jupiter.api.Test; import java.util.Set; import java.util.UUID; import java.util.concurrent.*; import java.util.function.IntFunction; import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; class CreatedUUIDsTest { void uniqueness(IntFunction<UUID> generator, int times) { ExecutorService es = Executors.newFixedThreadPool(10); try { Set<Future<String>> futures = IntStream .rangeClosed(1, times) .mapToObj(i -> es.submit(() -> generator.apply(i).toString())) .collect(Collectors.toSet()); Set<String> ids = futures .stream() .map(f -> { try { return f.get(); } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } }) .collect(Collectors.toSet()); assertThat(ids).hasSize(times); } finally { es.shutdown(); try { es.awaitTermination(10L, TimeUnit.SECONDS); } catch (InterruptedException e) { // ignore } } } @Test void v1() { TimeBasedGenerator timeBasedGenerator = Generators.timeBasedGenerator(); uniqueness(count -> timeBasedGenerator.generate(), 10_000_000); } @Test void v4() { RandomBasedGenerator randomBasedGenerator = Generators.randomBasedGenerator(); uniqueness(count -> randomBasedGenerator.generate(), 10_000_000); } @Test void v5() { NameBasedGenerator nameBasedGenerator = Generators.nameBasedGenerator(); uniqueness(count -> nameBasedGenerator.generate("source name" + count), 10_000_000); } @Test void v6() { TimeBasedReorderedGenerator timeBasedReorderedGenerator = Generators.timeBasedReorderedGenerator(); uniqueness(count -> timeBasedReorderedGenerator.generate(), 10_000_000); } @Test void v7() { TimeBasedEpochGenerator timeBasedEpochGenerator = Generators.timeBasedEpochGenerator(); uniqueness(count -> timeBasedEpochGenerator.generate(), 10_000_000); } }
確かに、マルチスレッド環境下でも問題にはならなさそうですね。
ところでシングルスレッドなどいろんなパターンで確認してみたのですが、バージョン4のUUID生成が1番速度が出ない傾向にあるように
見えますね。
こんなところで。
まとめ
Javaの標準ライブラリーで使うことができる、バージョン4以外のUUIDを扱えるライブラリーを調べつつ、そもそもUUIDについても
少し調べてまとめてみました。
バージョン6以降のUUIDがドラフトで存在することは知っていたのですが、今回ちゃんと見る機会を作っておいてよかったかなと。
正式に決まった時に、またキャッチアップしたいところですね。