0
点赞
收藏
分享

微信扫一扫

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

这是《水煮 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 类实现了 SerializableComparable 接口,其定义如下:

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;

mostSigBitsleastSigBits 它们是构成 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;
}

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

::: 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 异常,然后根据分割的值来计算 mostSigBitsleastSigBits 的值,最后再通过 mostSigBitsleastSigBits 构建 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 字符串的具体构成,主要是通过 mostSigBitsleastSigBits 进行变换得到的,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 是否相同或者是否相等,都是直接比较 mostSigBitsleastSigBits 的值。

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,而上面的 uuiduuid1uuid2 都不是基于时间的,所以会抛出异常。

举报

相关推荐

0 条评论