当前位置 : 主页 > 编程语言 > java >

从源码角度分析UUID的实现原理

来源:互联网 收集:自由互联 发布时间:2022-08-15
这是《水煮 JDK 源码》系列 的第 11 篇文章,计划撰写100篇关于JDK源码相关的文章 UUID 的全称是 universally unique identifier,表示通用唯一标识符,UUID 类位于 java.util 包下,自 JDK 1.5 版本新

这是《水煮 JDK 源码》系列 的第 11 篇文章,计划撰写100篇关于JDK源码相关的文章

UUID 的全称是 universally unique identifier,表示通用唯一标识符,UUID 类位于 java.util 包下,自 JDK 1.5 版本新增的,它是一个 final 类,不能被继承,在平常的开发中,通常会使用 UUID 类来生成唯一的标识符,比如下面的代码:

public static void main(String[] args) { System.out.println(UUID.randomUUID()); }

运行后,输出的结果如下:

981cd0fb-91cf-4d55-ba60-377806e6051a

上面的结果中显示的就是默认的 UUID 字符串,它是由数字、字母和 - 组成,其中数字和字母共占32个字符,- 占4个字符,总共36个字符,当我们运行上面的程序时,实际上会调用 UUID 哪些方法呢?具体如下:

UUID.randomUUID() > new UUID() > toString()

那么 UUID 是如何生成的呢?32个字符又分别代表什么含义呢?下面通过具体的源码来了解一下。

1、UUID 类定义

UUID 类实现了 Serializable 和 Comparable 接口,其定义如下:

public final class UUID implements java.io.Serializable, Comparable<UUID> { ... }
  • Serializable :它是一个标记接口,没有任何方法定义,用于对象的序列化;
  • Comparable :该接口只有一个 compareTo 方法,通常用于对实现它的类的对象进行比较大小;

2、成员变量

UUID 类中定义了2个成员变量,分别代表最高有效64位和最低有效64位,如下:

/** 最高有效64位 */ private final long mostSigBits; /** 最低有效64位 */ private final long leastSigBits;

mostSigBits 和 leastSigBits 它们是构成 UUID 标识符的重要组成部分。

3、构造函数

UUID 类提供了 2 个构造函数,其定义如下:

/** 私有的构造方法 */ private UUID(byte[] data) { long msb = 0; long lsb = 0; // 通过 assert 断言来判定参数 data 长度是否为 16 assert data.length == 16 : "data must be 16 bytes in length"; for (int i=0; i<8; i++) msb = (msb << 8) | (data[i] & 0xff); for (int i=8; i<16; i++) lsb = (lsb << 8) | (data[i] & 0xff); this.mostSigBits = msb; this.leastSigBits = lsb; } public UUID(long mostSigBits, long leastSigBits) { this.mostSigBits = mostSigBits; this.leastSigBits = leastSigBits; }

在构造函数中,主要是给成员变量 mostSigBits 和 leastSigBits 赋值,虽然 UUID 提供了 public 构造函数,但是在平时开发中,可能很少直接通过构造函数来创建 UUID 对象,更多的是使用 randomUUID() 方法,下面通过断点方式来看一下这个私有构造函数计算出的 mostSigBits 和 leastSigBits 具体值是多少,如下:

::: hljs-center

image20220809150530214.png

:::

4、方法

UUID 类中的方法主要分为静态方法和实例方法,其中静态方法主要用于创建 UUID 实例的,而实例方法主要用于获取 UUID 中的一些基本信息,比如版本号、时间戳、时钟序列等。

4.1 静态方法 - randomUUID()

public static UUID randomUUID() { // 通过内部静态类 Holder 获取 SecureRandom 对象 SecureRandom ng = Holder.numberGenerator; // 定义一个长度为 16 的字节数组 byte[] randomBytes = new byte[16]; // 通过 SecureRandom.nextBytes() 方法随机生成 16 个字节数字,并填充至数组中 ng.nextBytes(randomBytes); // 索引为 6 的字节数表示的是 UUID 的版本号 // 首先清除版本号,然后再设置版本号为 4 randomBytes[6] &= 0x0f; /* clear version */ randomBytes[6] |= 0x40; /* set to version 4 */ // 索引为 8 的字节数表示的是 UUID 的变种编号 // 首先清除该编号,然后设置为 IETF 变种,其值 为 2 randomBytes[8] &= 0x3f; /* clear variant */ randomBytes[8] |= 0x80; /* set to IETF variant */ // 调用私有的构造函数创建 UUID 对象实例 return new UUID(randomBytes); }

randomUUID() 方法可能是使用最多的一个方法,从实现可以看出,首先它通过内部静态类 Holder 获取了一个随机 SecureRandom 对象,主要用于产生随机数,Holder 类定义如下:

private static class Holder { static final SecureRandom numberGenerator = new SecureRandom(); }

4.2 静态方法 - nameUUIDFromBytes()

public static UUID nameUUIDFromBytes(byte[] name) { MessageDigest md; try { // 获取 MD5 信息摘要算法 md = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException nsae) { throw new InternalError("MD5 not supported", nsae); } // 通过 MD5算法 对传入的 name[] 字节数组进行转换 // 转换后得到长度为 16 的 md5Bytes 新数组 byte[] md5Bytes = md.digest(name); // 设置版本号为 3 md5Bytes[6] &= 0x0f; /* clear version */ md5Bytes[6] |= 0x30; /* set to version 3 */ md5Bytes[8] &= 0x3f; /* clear variant */ md5Bytes[8] |= 0x80; /* set to IETF variant */ return new UUID(md5Bytes); }

nameUUIDFromBytes() 方法可以通过一个字节数组来创建 UUID,首先会通过 MD5 摘要算法对字节数组进行加密转换,得到一个长度为 16 的新字节数组,然后使用新的字节数组构建 UUID,该方法种会设置版本号为 3,而通过 randomUUID() 方法设置的版本号为 4.

4.3 静态方法 - fromString()

public static UUID fromString(String name) { // 传入的 name 格式必须是和 UUID.toString() 得到的字符串一致 // 通过 - 对字符串进行分割,需要得到长度为 5 的字符串数组 String[] components = name.split("-"); if (components.length != 5) throw new IllegalArgumentException("Invalid UUID string: "+name); for (int i=0; i<5; i++) components[i] = "0x"+components[i]; // 计算出最高有效64位的值 long mostSigBits = Long.decode(components[0]).longValue(); mostSigBits <<= 16; mostSigBits |= Long.decode(components[1]).longValue(); mostSigBits <<= 16; mostSigBits |= Long.decode(components[2]).longValue(); // 计算出最低有效64位的值 long leastSigBits = Long.decode(components[3]).longValue(); leastSigBits <<= 48; leastSigBits |= Long.decode(components[4]).longValue(); return new UUID(mostSigBits, leastSigBits); }

除了上面的可以通过字节数组构建 UUID 外,还可以直接通过字符串来构建,但是不是随便的字符串都可以,而是需要和 UUID.toString() 方法得到的字符串格式一致,也就是使用 - 进行分割时,必须得到长度为 5 的字符串数组,否则就会抛出 IllegalArgumentException 异常,然后根据分割的值来计算 mostSigBits 和 leastSigBits 的值,最后再通过 mostSigBits 和 leastSigBits 构建 UUID 对象。

4.4 实例方法

UUID 类提供的实例方法,主要有以下这些:

  • version(): 获取当前 UUID 的版本信息;
  • variant():获取当前 UUID 的变体编号;
  • timestamp():获取当前 UUID 的时间戳;
  • clockSequence():获取当前 UUID 的时钟序列值;
  • node():获取当前 UUID 的节点值;
  • toString():将 UUID 对象转换为字符串,通过这个方法就可以了解 UUID 的构成;
  • hashCode():获取当前 UUID 的哈希值;
  • equals(Object obj):用于比较两个 UUID 对象是否相同;
  • compareTo(UUID val):比较两个 UUID 对象值的大小;

(1)基本信息方法

public int version() { // Version is bits masked by 0x000000000000F000 in MS long return (int)((mostSigBits >> 12) & 0x0f); } public int variant() { // This field is composed of a varying number of bits. // 0 - - Reserved for NCS backward compatibility // 1 0 - The IETF aka Leach-Salz variant (used by this class) // 1 1 0 Reserved, Microsoft backward compatibility // 1 1 1 Reserved for future definition. return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); }

对于版本号和变体编号信息,是所有 UUID 通用的,而对于基于时间的 UUID,则可以调用下面的 3 个方法获取时间相关的信息,如下:

public long timestamp() { // 如果 version 版本号不为 1,则直接抛出异常 // 必须是以时间为基准的 UUID if (version() != 1) { throw new UnsupportedOperationException("Not a time-based UUID"); } // 通过高位值进行转换获取时间戳 return (mostSigBits & 0x0FFFL) << 48 | ((mostSigBits >> 16) & 0x0FFFFL) << 32 | mostSigBits >>> 32; } public int clockSequence() { // 如果 version 版本号不为 1,则直接抛出异常 // 必须是以时间为基准的 UUID if (version() != 1) { throw new UnsupportedOperationException("Not a time-based UUID"); } // 通过低位值进行转换获取时钟序列值 return (int)((leastSigBits & 0x3FFF000000000000L) >>> 48); } public long node() { // 如果 version 版本号不为 1,则直接抛出异常 // 必须是以时间为基准的 UUID if (version() != 1) { throw new UnsupportedOperationException("Not a time-based UUID"); } // 通过低位值进行转换获取节点值 return leastSigBits & 0x0000FFFFFFFFFFFFL; }

(2)toString() 方法

public String toString() { return (digits(mostSigBits >> 32, 8) + "-" + digits(mostSigBits >> 16, 4) + "-" + digits(mostSigBits, 4) + "-" + digits(leastSigBits >> 48, 4) + "-" + digits(leastSigBits, 12)); }

通过 toString() 方法,可以得知 UUID 字符串的具体构成,主要是通过 mostSigBits 和 leastSigBits 进行变换得到的,digits() 方法定义如下:

private static String digits(long val, int digits) { long hi = 1L << (digits * 4); return Long.toHexString(hi | (val & (hi - 1))).substring(1); }

UUID 的构成主要分为以下的几个:

* UUID = <time_low> "-" <time_mid> "-" * <time_high_and_version> "-" * <variant_and_sequence> "-" * <node> * time_low = 4*<hexOctet> * time_mid = 2*<hexOctet> * time_high_and_version = 2*<hexOctet> * variant_and_sequence = 2*<hexOctet> * node = 6*<hexOctet> * 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"

通过上面的信息可以看出,UUID 由 时间低位 + 时间中位 + 时间高位和版本号 + 变体编号和序列号 + 节点值 五个部分组成的,可以看一个具体的值

033bd94b-1168-37e4-b0d6-44c3c95e35bf

(3)其他方法

public int hashCode() { long hilo = mostSigBits ^ leastSigBits; return ((int)(hilo >> 32)) ^ (int) hilo; } public boolean equals(Object obj) { if ((null == obj) || (obj.getClass() != UUID.class)) return false; UUID id = (UUID)obj; // 比较两个 UUID 是否相同,主要比较 mostSigBits 和 leastSigBits 是否相等 return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); } public int compareTo(UUID val) { // 比较两个 UUID 的大小,也是比较 mostSigBits 和 leastSigBits 的大小 return (this.mostSigBits < val.mostSigBits ? -1 : (this.mostSigBits > val.mostSigBits ? 1 : (this.leastSigBits < val.leastSigBits ? -1 : (this.leastSigBits > val.leastSigBits ? 1 : 0)))); }

对于比较两个 UUID 是否相同或者是否相等,都是直接比较 mostSigBits 和 leastSigBits 的值。

5、测试验证

下面通过 3 种不同的方式来创建 UUID 实例,同时输出基本信息。

package com.magic.test; import java.util.UUID; public class UUIDTest { public static void main(String[] args) { UUID uuid = UUID.randomUUID(); System.out.println(uuid); System.out.println("version : " + uuid.version()); System.out.println("variant : " + uuid.variant()); UUID uuid1 = UUID.nameUUIDFromBytes("TEST".getBytes()); System.out.println(uuid1); System.out.println("version : " + uuid1.version()); System.out.println("variant : " + uuid1.variant()); UUID uuid2 = UUID.fromString("033bd94b-1168-37e4-b0d6-44c3c95e35bf"); System.out.println(uuid2); System.out.println("version : " + uuid2.version()); System.out.println("variant : " + uuid2.variant()); } }

运行程序,输出信息如下:

d3f83827-8ccd-4ff7-8118-5155db6dbf4b version : 4 variant : 2 033bd94b-1168-37e4-b0d6-44c3c95e35bf version : 3 variant : 2 033bd94b-1168-37e4-b0d6-44c3c95e35bf version : 3 variant : 2

如果直接对上面的创建的 UUID 调用时间相关方法,则会抛出 UnsupportedOperationException 异常,测试代码如下:

System.out.println(uuid2.timestamp()); System.out.println(uuid2.clockSequence()); System.out.println(uuid2.node());

输出的错误信息如下:

Exception in thread "main" java.lang.UnsupportedOperationException: Not a time-based UUID at java.util.UUID.timestamp(UUID.java:293) at com.magic.test.UUIDTest.main(UUIDTest.java:26)

通过之前的源码分析可以知道,timestamp()、clockSequence()、node() 方法必须是基于时间的 UUID 才能调用,基于时间的 UUID 的版本号为 1,而上面的 uuid、uuid1、uuid2 都不是基于时间的,所以会抛出异常。

网友评论