我如何在 Java 中复制 PostgreSQL 的 uuid_generate_v3()?

How do I replicate PostgreSQL's uuid_generate_v3() in Java?

PostgreSQL:

 create extension if not exists "uuid-ossp";
 select uuid_generate_v3(uuid_nil(), 'this is a test');
           uuid_generate_v3           
--------------------------------------
 e1e27115-9f5b-366d-90e8-e07b1b36b99c
(1 row)

Java:

java> java.util.UUID.nameUUIDFromBytes("this is a test".getBytes());
java.util.UUID res9 = 54b0c58c-7ce9-32a8-b551-351102ee0938

如何在 Java 中生成与 PostgreSQL 相同的 UUID?

据我所知,没有直接的方法,您必须深入研究 PostgreSQL(及其库)的源代码并自己在 Java 中复制算法,或本机调用相同的库函数。一个更简单的选择是让 PostgreSQL 数据库为您生成它们,但我认为由于某种原因这是不可能的。

正如您从 the documentation, PostgreSQL takes an MD5 hash first to protect against reverse-engineering. They then use a UUID generator from the OSSP UUID 库中看到的那样。

我试过了:

java.util.UUID.nameUUIDFromBytes(Md5Utils.getMd5Digest("this is a test"));

但是产生了:

d69495fb-d538-3991-b96b-aa262ab6dce5

虽然这不是原始问题的答案,但我不得不以另一种方式进行此转换,即在迁移期间模仿 PostgreSQL 中的 UUID.nameUUIDFromBytes 行为:

CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "plpgsql";

CREATE OR REPLACE FUNCTION uuidv3(bytes bytea)
  RETURNS uuid
  RETURNS null on null input
  STABLE
  PARALLEL SAFE
AS $$
DECLARE
  md5bytes  bytea;
BEGIN
  md5bytes := digest(bytes, 'md5');
  --       md5Bytes[6]  &= 0x0f;  /* clear version        */
  --       md5Bytes[6]  |= 0x30;  /* set to version 3     */
  md5bytes := set_byte(md5bytes, 6, (get_byte(md5bytes, 6) & x'0F'::int) | x'30'::int);
  --       md5Bytes[8]  &= 0x3f;  /* clear variant        */
  --       md5Bytes[8]  |= 0x80;  /* set to IETF variant  */
  md5bytes := set_byte(md5bytes, 8, (get_byte(md5bytes, 8) & x'3F'::int)|  x'80'::int);
  RETURN (encode(md5bytes, 'hex'))::uuid;
END
$$
  LANGUAGE plpgsql;

并且您应该使用内部 textsend 将文本列转换为 bytea:

SELECT uuidv3(textsend('this is a test'));

虽然我必须承认我从未尝试过另一种方式。

此处描述了生成版本 3 UUID 的算法https://www.rfc-editor.org/rfc/rfc4122#section-4.3

但关键步骤是:

  • 分配一个 UUID 用作所有 UUID 的“名称 space ID” 从该名称中的名称生成 space.
  • 选择 MD5 或 SHA-1 作为哈希算法
  • 将名称转换为规范的八位字节序列
  • 计算名称 space ID 与名称连接的哈希值。
  • 将 的某些字节更改为预定义值(参见上面的 link)
  • 将生成的 UUID 转换为本地字节顺序。

postgres 函数签名是 uuid_generate_v3(namespace uuid, name text) 所以它以名称 space UUID 和 name 作为参数。
Java 方法 nameUUIDFromBytes(byte[] name) 仅采用 name 并使用 MD5 对其进行哈希处理以创建 UUID。要获得与 PostgreSQL 相同的输出,您必须自己将 namespace 字节和 name 字节连接在一起。

对于名称space,您使用了 uuid_nil()(全为零),即 Java.

中的 new UUID(0L, 0L)

将它们放在一起看起来像这样:

byte[] bytes = Arrays.concatenate(toByteArray(new UUID(0L, 0L)), "this is a test".getBytes(StandardCharsets.UTF_8));
System.out.println(UUID.nameUUIDFromBytes(bytes)); // prints out e1e27115-9f5b-366d-90e8-e07b1b36b99c

并且您可以将名称space UUID 转换为字节数组,如下所示:

private static byte[] toByteArray(UUID uuid) {
    ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
    bb.putLong(uuid.getMostSignificantBits());
    bb.putLong(uuid.getLeastSignificantBits());
    return bb.array();
}

这是一个使用 System.arraycopy() 连接命名空间和名称的示例。

package com.example;

import java.nio.charset.StandardCharsets;
import java.util.UUID;

public class Example {
    
    /**
     * Returns a name-based UUID using NIL UUID as namespace.
     * 
     * @param name a string
     * @return a UUID
     */
    public static UUID getNameBasedUsingNil(String name) {
        
        // 1. Get NIL and NAME bytes
        final byte[] nil = new byte[16]; // NIL namespace
        final byte[] nam = name.getBytes(StandardCharsets.UTF_8);
        
        // 2. Concatenate NIL and NAME bytes
        final byte[] bytes = new byte[nil.length + nam.length];
        System.arraycopy(nil, 0, bytes, 0, 16);
        System.arraycopy(nam, 0, bytes, 16, nam.length);
        
        // 3. Generate a name-based UUID
        return UUID.nameUUIDFromBytes(bytes);
    }
    
    public static void main(String[] args) {       
        String name = "this is a test";
        UUID uuid = getNameBasedUsingNil(name);
        System.out.println(uuid);
    }    
}

输出:

e1e27115-9f5b-366d-90e8-e07b1b36b99c