feat: configurable encoding with legacy 1.0.x compatibility mode - #22
Merged
Merged
Conversation
Version 1.1.0 switched the UUID pairing algorithm from Szudzik's elegant pairing to bit-shifting BigIntegerPairing, silently changing every generated FriendlyId and making old identifiers decode to wrong UUIDs. Services still on 1.0.x (with identifiers persisted by external consumers) could never upgrade without breaking their public ID space. - new public enum FriendlyIdEncoding: STANDARD (default, 1.1.0+ compatible) and LEGACY (1.0.x compatible, restored ElegantPairing) - global selection via FriendlyIds.setEncoding(...); all modules (Jackson, Jackson2, JPA, jOOQ, OpenFeign, Spring) funnel through it - Spring Boot starter property: com.devskiller.friendly-id.encoding=legacy (new FriendlyIdProperties, bound in FriendlyIdAutoConfiguration) - wire-format pinned by test vectors generated from released 1.0.4 and 1.1.0/2.0.0-beta5 artifacts, incl. edge cases (zero UUID, all-bits UUID, lsb sign bit)
Hide implicit public constructor (java:S1118) and use BigInteger.TWO instead of the string constructor (java:S2129).
The jmh profile referenced an undefined jmh.version property, and UuidConverterBenchmark no longer compiled after UuidConverter started taking an encoding. Benchmarks now run for STANDARD and LEGACY via @PARAM and decode real encoder output instead of random 127-bit values.
ElegantPairing.unpair computed floor(sqrt) with a ~130-step BigInteger binary search. A double estimate followed by one Newton step and an off-by-one correction returns the same root, so the wire format is unchanged (1.0.4 vectors still pass). JMH on JDK 21 (ops/s): - UuidConverter.toUuid LEGACY: 274k -> 4.85M (~18x) - FriendlyIds.toUuid LEGACY: 178k -> 515k (on par with STANDARD) BigInteger.sqrt() is not used yet: on JDK 21 it is ~9x slower than this implementation; it becomes faster on JDK 25.
SonarCloud flags new BigDecimal(double) as a bug (java:S2111), failing the quality gate. Convert the double estimate to BigInteger exactly via Math.scalb and shiftLeft instead, which is also slightly faster (UuidConverter.toUuid LEGACY: 4.85M -> 5.43M ops/s).
|
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Version 1.1.0 switched the UUID pairing algorithm (ElegantPairing → BigIntegerPairing), silently changing every generated FriendlyId — and old identifiers decode to a different UUID without any error. Services still on 1.0.x whose identifiers are persisted by external consumers (e.g. a public API) could never upgrade past 1.0.4 without breaking their published ID space.
This makes the encoding selectable while keeping the current behaviour as the default:
FriendlyIdEncoding:STANDARD— bit-shifting pairing, wire-compatible with 1.1.0+, defaultLEGACY— Szudzik's elegant pairing, wire-compatible with 1.0.x (restoredElegantPairing)FriendlyIds.setEncoding(...)— every integration module (Jackson 3, Jackson 2, JPA, jOOQ, OpenFeign, Spring) funnels throughFriendlyIds/Url62, so the setting applies everywherecom.devskiller.friendly-id.encoding=legacy(newFriendlyIdPropertiesbound inFriendlyIdAutoConfiguration, plus first tests for the starter)LEGACY decoding performance
The restored 1.0.x
ElegantPairing.unpaircomputedfloor(sqrt)with a BigInteger binary search (~127 iterations), which made LEGACY unpairing ~50× slower than STANDARD (~3.4× end-to-end). It now uses a double estimate + one Newton step + off-by-one correction. The root is identical, so the wire format is unchanged: the 1.0.4 vectors still pass, and the newElegantPairingTestcheckssqrtagainstBigInteger.sqrt()around perfect squares up to 66-bit roots and at extremelongvalues.BigInteger.sqrt()itself is not used yet: on JDK 21 it is ~9× slower than this implementation (it becomes faster on JDK 25), so a TODO marks the switch for after the JDK 25 upgrade.JMH, JDK 21, ops/s (1 fork, 2×5s warmup, 5×5s measurement):
UuidConverter.toUuidFriendlyIds.toUuid(end-to-end, incl. Base62)Encoding (
FriendlyIds.toFriendlyId) is unaffected and on par with STANDARD (~1.5M ops/s for both).JMH benchmarks
The
jmhprofile did not build:jmh.versionwas undefined, andUuidConverterBenchmarkno longer compiled afterUuidConverterstarted taking an encoding. Both benchmarks now run forSTANDARDandLEGACYvia@Param, and decode real encoder output instead of random 127-bit values.Note for choosing an encoding: LEGACY identifiers are not shorter. Random (v4) and time-ordered (v7) UUIDs always encode to 22 characters, while STANDARD gives 21–22 for v4 and 21 for v7.
Full build: 7169 tests, 0 failures.
Motivation: lets 1.0.x-era services adopt 2.0 (Spring Boot 4 / Jackson 3) with
legacyencoding, while 1.1.0+ services upgrade with no changes.