|
11 | 11 |
|
12 | 12 | /// UUID Generator providing time-ordered globally unique IDs. |
13 | 13 | /// |
| 14 | +/// This is a backport of UUIDv7 from Java 26 (JDK-8334015: https://bugs.openjdk.org/browse/JDK-8334015) |
| 15 | +/// |
| 16 | +/// UUIDv7's time-based sortability makes it an attractive option for globally unique identifiers, |
| 17 | +/// especially in database applications. (https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-7) |
| 18 | +/// |
| 19 | +/// As DBMS vendors add support for UUIDv7 (https://commitfest.postgresql.org/47/4388/) Java users |
| 20 | +/// will not easily take advantage of its benefits until it is included in the Java core libraries. |
| 21 | +/// |
14 | 22 | /// Generation: |
15 | | -/// - timeThenRandom: Time-ordered globally unique 128-bit identifier |
| 23 | +/// - ofEpochMillis: Creates a UUIDv7 from Unix Epoch timestamp (backport from Java 26) |
16 | 24 | /// - uniqueThenTime: User-ID-then-time-ordered 128-bit identifier |
17 | 25 | /// |
18 | 26 | /// Formatting: |
19 | 27 | /// - formatAsUUID: RFC 4122 format with dashes, 36 characters, uppercase or lowercase |
20 | 28 | /// - formatAsDenseKey: Base62 encoded, 22 characters, zero-padded fixed-width |
21 | 29 | /// |
22 | | -/// Note: Intended usage is one instance per JVM process. Multiple instances |
23 | | -/// in the same process do not guarantee uniqueness due to shared sequence counter. |
24 | | -/// |
25 | 30 | /// Note: 22-character keys are larger than Firebase push IDs (20 characters) |
26 | 31 | /// but provide full 128-bit time-ordered randomized identifiers. |
27 | 32 | public class UUIDGenerator { |
@@ -52,14 +57,70 @@ static long timeCounterBits() { |
52 | 57 |
|
53 | 58 | // Generation - Public API |
54 | 59 |
|
55 | | - /// ┌──────────────────────────────────────────────────────────────────────────────┐ |
56 | | - /// │ time+counter (64 bits) │ random (64 bits) │ |
57 | | - /// └──────────────────────────────────────────────────────────────────────────────┘ |
58 | | - public static UUID timeThenRandom() { |
59 | | - long msb = timeCounterBits(); |
60 | | - long lsb = getRandom().nextLong(); |
| 60 | + /// Creates a type 7 UUID (UUIDv7) {@code UUID} from the given Unix Epoch timestamp. |
| 61 | + /// |
| 62 | + /// The returned {@code UUID} will have the given {@code timestamp} in |
| 63 | + /// the first 6 bytes, followed by the version and variant bits representing {@code UUIDv7}, |
| 64 | + /// and the remaining bytes will contain random data from a cryptographically strong |
| 65 | + /// pseudo-random number generator. |
| 66 | + /// |
| 67 | + /// @apiNote {@code UUIDv7} values are created by allocating a Unix timestamp in milliseconds |
| 68 | + /// in the most significant 48 bits, allocating the required version (4 bits) and variant (2-bits) |
| 69 | + /// and filling the remaining 74 bits with random bits. As such, this method rejects {@code timestamp} |
| 70 | + /// values that do not fit into 48 bits. |
| 71 | + /// <p> |
| 72 | + /// Monotonicity (each subsequent value being greater than the last) is a primary characteristic |
| 73 | + /// of {@code UUIDv7} values. This is due to the {@code timestamp} value being part of the {@code UUID}. |
| 74 | + /// Callers of this method that wish to generate monotonic {@code UUIDv7} values are expected to |
| 75 | + /// ensure that the given {@code timestamp} value is monotonic. |
| 76 | + /// |
| 77 | + /// @param timestamp the number of milliseconds since midnight 1 Jan 1970 UTC, |
| 78 | + /// leap seconds excluded. |
| 79 | + /// |
| 80 | + /// @return a {@code UUID} constructed using the given {@code timestamp} |
| 81 | + /// |
| 82 | + /// @throws IllegalArgumentException if the timestamp is negative or greater than {@code (1L << 48) - 1} |
| 83 | + /// |
| 84 | + /// @since Backport from Java 26 (JDK-8334015) |
| 85 | + public static UUID ofEpochMillis(long timestamp) { |
| 86 | + if ((timestamp >> 48) != 0) { |
| 87 | + throw new IllegalArgumentException("Supplied timestamp: " + timestamp + " does not fit within 48 bits"); |
| 88 | + } |
| 89 | + |
| 90 | + SecureRandom ng = getRandom(); |
| 91 | + byte[] randomBytes = new byte[16]; |
| 92 | + ng.nextBytes(randomBytes); |
| 93 | + |
| 94 | + // Embed the timestamp into the first 6 bytes |
| 95 | + randomBytes[0] = (byte)(timestamp >> 40); |
| 96 | + randomBytes[1] = (byte)(timestamp >> 32); |
| 97 | + randomBytes[2] = (byte)(timestamp >> 24); |
| 98 | + randomBytes[3] = (byte)(timestamp >> 16); |
| 99 | + randomBytes[4] = (byte)(timestamp >> 8); |
| 100 | + randomBytes[5] = (byte)(timestamp); |
| 101 | + |
| 102 | + // Set version to 7 |
| 103 | + randomBytes[6] &= 0x0f; |
| 104 | + randomBytes[6] |= 0x70; |
| 105 | + |
| 106 | + // Set variant to IETF |
| 107 | + randomBytes[8] &= 0x3f; |
| 108 | + randomBytes[8] |= (byte) 0x80; |
| 109 | + |
| 110 | + // Convert byte array to UUID using ByteBuffer |
| 111 | + ByteBuffer buffer = ByteBuffer.wrap(randomBytes); |
| 112 | + long msb = buffer.getLong(); |
| 113 | + long lsb = buffer.getLong(); |
61 | 114 | return new UUID(msb, lsb); |
62 | 115 | } |
| 116 | + |
| 117 | + /// Convenience method to create a UUIDv7 from the current system time. |
| 118 | + /// Equivalent to {@code ofEpochMillis(System.currentTimeMillis())}. |
| 119 | + /// |
| 120 | + /// @return a {@code UUID} constructed using the current system time |
| 121 | + public static UUID timeThenRandom() { |
| 122 | + return ofEpochMillis(System.currentTimeMillis()); |
| 123 | + } |
63 | 124 |
|
64 | 125 | /// ┌──────────────────────────────────────────────────────────────────────────────┐ |
65 | 126 | /// │ unique (64 bits) │ time+counter (44 bits) │ random (20 bits) │ |
@@ -134,12 +195,18 @@ public static String formatAsDenseKey(UUID uuid) { |
134 | 195 | } |
135 | 196 |
|
136 | 197 | public static void main(String[] args) { |
| 198 | + // Test UUIDv7 with current time |
137 | 199 | UUID uuid1 = UUIDGenerator.timeThenRandom(); |
138 | | - System.out.println("UUID: " + UUIDGenerator.formatAsUUID(uuid1)); |
| 200 | + System.out.println("UUIDv7: " + UUIDGenerator.formatAsUUID(uuid1)); |
139 | 201 | System.out.println("Dense: " + UUIDGenerator.formatAsDenseKey(uuid1)); |
140 | 202 |
|
141 | | - UUID uuid2 = UUIDGenerator.uniqueThenTime(0x123456789ABCDEF0L); |
142 | | - System.out.println("Unique UUID: " + UUIDGenerator.formatAsUUID(uuid2, true)); |
143 | | - System.out.println("Unique Dense: " + UUIDGenerator.formatAsDenseKey(uuid2)); |
| 203 | + // Test UUIDv7 with specific timestamp |
| 204 | + UUID uuid2 = UUIDGenerator.ofEpochMillis(System.currentTimeMillis()); |
| 205 | + System.out.println("UUIDv7 (explicit): " + UUIDGenerator.formatAsUUID(uuid2)); |
| 206 | + |
| 207 | + // Test uniqueThenTime |
| 208 | + UUID uuid3 = UUIDGenerator.uniqueThenTime(0x123456789ABCDEF0L); |
| 209 | + System.out.println("Unique UUID: " + UUIDGenerator.formatAsUUID(uuid3, true)); |
| 210 | + System.out.println("Unique Dense: " + UUIDGenerator.formatAsDenseKey(uuid3)); |
144 | 211 | } |
145 | 212 | } |
0 commit comments