Last active
February 1, 2018 10:51
-
-
Save nicky-zs/07c31f225d04ef64924c25c5ebe05675 to your computer and use it in GitHub Desktop.
A simple, thread-safe Base62 codec for hiding the real ID and its incresing trend. No dependencies other than JDK are required. It can encode and decode 250k+ IDs per thread per second on [email protected].
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.math.BigInteger; | |
import java.nio.charset.StandardCharsets; | |
import java.security.InvalidKeyException; | |
import java.security.Key; | |
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.spec.InvalidKeySpecException; | |
import javax.crypto.BadPaddingException; | |
import javax.crypto.Cipher; | |
import javax.crypto.IllegalBlockSizeException; | |
import javax.crypto.SecretKeyFactory; | |
import javax.crypto.spec.DESKeySpec; | |
/** | |
* A simple, thread-safe Base62 codec for hiding the real ID and its incresing trend. | |
* <p> | |
* DES Encryption is used. In a same ID system, the key must be the same. | |
* <p> | |
* No dependencies other than JDK are required. | |
* It can encode and decode 250k+ IDs per thread per second on [email protected]. | |
*/ | |
public class Base62IdCodec { | |
private final ThreadLocal<MessageDigest> digest; | |
private final ThreadLocal<Cipher> cipher; | |
private final ThreadLocal<Key> key; | |
/** | |
* @param key the key used to encrypt and decrypt | |
*/ | |
public Base62IdCodec(String key) { | |
this(key.getBytes(StandardCharsets.UTF_8)); | |
} | |
/** | |
* @param key the key used to encrypt and decrypt | |
*/ | |
public Base62IdCodec(byte[] key) { | |
digest = ThreadLocal.withInitial(() -> { | |
try { | |
return MessageDigest.getInstance("MD5"); | |
} catch (NoSuchAlgorithmException e) { | |
throw new RuntimeException("MD5 MessageDigest is not supported!", e); | |
} | |
}); | |
cipher = ThreadLocal.withInitial(() -> { | |
try { | |
return Cipher.getInstance("DES/ECB/NoPadding"); | |
} catch (Exception e) { | |
throw new RuntimeException("DES/ECB/NoPadding Cipher is not supported!", e); | |
} | |
}); | |
final MessageDigest digest = this.digest.get(); | |
this.key = ThreadLocal.withInitial(() -> { | |
final DESKeySpec keySpec; | |
try { | |
keySpec = new DESKeySpec(digest.digest(key)); | |
} catch (InvalidKeyException e) { | |
throw new RuntimeException("Should never happen!", e); | |
} | |
final SecretKeyFactory keyFactory; | |
try { | |
keyFactory = SecretKeyFactory.getInstance("DES"); | |
} catch (NoSuchAlgorithmException e) { | |
throw new RuntimeException("DES SecretKeyFactory is not supported!", e); | |
} | |
try { | |
return keyFactory.generateSecret(keySpec); | |
} catch (InvalidKeySpecException e) { | |
throw new RuntimeException("Should never happen!", e); | |
} | |
}); | |
} | |
public String encodeId(long idLong) { | |
final Cipher cipher = this.cipher.get(); | |
final Key key = this.key.get(); | |
try { | |
cipher.init(Cipher.ENCRYPT_MODE, key); | |
} catch (InvalidKeyException e) { | |
throw new RuntimeException("Should never happen!", e); | |
} | |
final byte[] resultBytes; | |
try { | |
resultBytes = cipher.doFinal(LongUtils.toByteArray(idLong)); | |
} catch (IllegalBlockSizeException | BadPaddingException e) { | |
throw new RuntimeException("Should never happen!", e); | |
} | |
return Base62Utils.encode(LongUtils.fromByteArray(resultBytes)); | |
} | |
public long decodeId(String idStr) { | |
final Cipher cipher = this.cipher.get(); | |
final Key key = this.key.get(); | |
try { | |
cipher.init(Cipher.DECRYPT_MODE, key); | |
} catch (InvalidKeyException e) { | |
throw new RuntimeException("Should never happen!", e); | |
} | |
final byte[] resultBytes; | |
try { | |
resultBytes = cipher.doFinal(LongUtils.toByteArray(Base62Utils.decode(idStr))); | |
} catch (IllegalBlockSizeException | BadPaddingException e) { | |
throw new RuntimeException("Should never happen!", e); | |
} | |
return LongUtils.fromByteArray(resultBytes); | |
} | |
private static final class Base62Utils { | |
private static final String R = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; | |
private static final char[] RADIX = R.toCharArray(); | |
private static final BigInteger RADIX_N = BigInteger.valueOf(RADIX.length); | |
private static final BigInteger[] REVERSE; | |
private static final BigInteger MAX_VALUE = new BigInteger(1, LongUtils.toByteArray(0xFFFFFFFF)); | |
private static final String MAX_STR = encode(MAX_VALUE.longValue()); | |
private static final int MAX_LENGTH = MAX_STR.length(); | |
static { | |
REVERSE = new BigInteger[128]; | |
for (int i = 0; i < RADIX.length; ++i) { | |
if (RADIX[i] >= REVERSE.length) { | |
throw new IllegalStateException("Invalid radix string for char: " + RADIX[i]); | |
} | |
REVERSE[RADIX[i]] = BigInteger.valueOf(i); | |
} | |
} | |
private static String encode(long l) { | |
BigInteger i = new BigInteger(1, LongUtils.toByteArray(l)); | |
if (i.equals(BigInteger.ZERO)) { | |
return String.valueOf(RADIX[0]); | |
} | |
StringBuilder sb = new StringBuilder(); | |
while (i.compareTo(BigInteger.ZERO) > 0) { | |
sb.append(RADIX[i.mod(RADIX_N).intValue()]); | |
i = i.divide(RADIX_N); | |
} | |
return sb.reverse().toString(); | |
} | |
private static long decode(String str) { | |
if (str.length() > MAX_LENGTH) { | |
throw new IllegalArgumentException("Invalid input length, the max is: " + MAX_LENGTH); | |
} | |
char[] chars = str.toCharArray(); | |
for (int i = 0; i < chars.length / 2; ++i) { | |
int j = chars.length - 1 - i; | |
char c = chars[i]; | |
chars[i] = chars[j]; | |
chars[j] = c; | |
} | |
BigInteger ret = BigInteger.ZERO; | |
BigInteger e = BigInteger.ONE; | |
for (int i = 0; i < chars.length; ++i) { | |
BigInteger r; | |
if (chars[i] >= REVERSE.length || (r = REVERSE[chars[i]]) == null) { | |
throw new IllegalArgumentException("Invalid input for char: " + chars[i]); | |
} | |
ret = ret.add(r.multiply(e)); | |
e = e.multiply(RADIX_N); | |
} | |
if (ret.compareTo(MAX_VALUE) > 0) { | |
throw new IllegalArgumentException("Invalid input exceeding the max(" + MAX_STR + "): " + str); | |
} | |
return ret.longValue(); | |
} | |
} | |
private static final class LongUtils { | |
private static byte[] toByteArray(long value) { | |
return new byte[]{(byte) ((value >> 56) & 255L), (byte) ((value >> 48) & 255L), | |
(byte) ((value >> 40) & 255L), (byte) ((value >> 32) & 255L), (byte) ((value >> 24) & 255L), | |
(byte) ((value >> 16) & 255L), (byte) ((value >> 8) & 255L), (byte) (value & 255L)}; | |
} | |
private static long fromByteArray(byte[] bytes) { | |
if (bytes.length != 8) { | |
throw new IllegalArgumentException("bytes.length must be 8"); | |
} | |
return ((long) bytes[0] & 255L) << 56 | ((long) bytes[1] & 255L) << 48 | ((long) bytes[2] & 255L) << 40 | | |
((long) bytes[3] & 255L) << 32 | ((long) bytes[4] & 255L) << 24 | ((long) bytes[5] & 255L) << 16 | | |
((long) bytes[6] & 255L) << 8 | (long) bytes[7] & 255L; | |
} | |
} | |
public static void main(String[] args) { | |
Base62IdCodec base62IdCodec = new Base62IdCodec("abcdefg"); | |
final long startId = System.currentTimeMillis(); | |
final int N = 1000000; | |
final long endId = startId + N; | |
final long startTime = System.currentTimeMillis(); | |
for (long id = startId; id < endId; ++id) { | |
base62IdCodec.encodeId(id); | |
} | |
final long cost = System.currentTimeMillis() - startTime; | |
System.out.println(N + " id used: " + cost / 1000.0 + "s"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment