fix: initialize MethodHandles.Lookup before reading IMPL_LOOKUP (#7691) - #7718
Conversation
wenshao
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
— qwen3.8-max-preview via Qwen Code /review
Code ReviewOverviewWraps the LambdaMetafactory-based accessor creation in The direction is right — defense in depth here is clearly better than a hard 1. Root cause description is not accurate (and points at a better fix)The PR body says JDK 8 rejects "a trusted lookup whose So the reported failure means return IMPL_LOOKUP.in(objectClass); // Lookup.in() drops PRIVATE/PROTECTEDThat also explains the odd behaviour in #7691 ("swapping the two lines makes both work", This suggests a more targeted fix, in addition to (or instead of) the blanket catch: check the lookup's capability before calling MethodHandles.Lookup lookup = JDKUtils.trustedLookup(declaringClass);
if ((lookup.lookupModes() & MethodHandles.Lookup.PRIVATE) == 0) {
return super.create(...); // no private access -> lambda path can never work
}That fixes the whole class of problem (getters, setters, 2. Failure is re-attempted for every propertyAs written, on an affected JVM every single property pays: build MethodHandle → Suggest memoizing: a 3.
|
| Correctness | Fix works, but treats a symptom; the real degradation is trustedLookup() falling back to a non-private IMPL_LOOKUP.in(...) |
| Performance | Per-property exception on affected JVMs; should be detected once |
| Tests | Pass with the fix reverted — no regression protection |
| Style | Large re-indent diff; blanket catch (Throwable) |
Recommendation: keep the fallback as a safety net, but add the up-front lookup-capability check (§1), narrow/memoize the catch (§2–3), and add a test that actually exercises the fallback (§5).
wenshao
left a comment
There was a problem hiding this comment.
— qwen3.8-max-preview via Qwen Code /review (v0.21.3)
Root cause: JDKUtils reads MethodHandles.Lookup.IMPL_LOOKUP directly from memory with Unsafe. Neither MethodHandles.Lookup.class nor getDeclaredField triggers class initialization, so when nothing has used MethodHandles before fastjson2 is loaded the field is still null. JDKUtils then falls back to MethodHandles.lookup(), whose lookupClass is JDKUtils, so trustedLookup() cannot find the private Lookup(Class, int) constructor, sets CONSTRUCTOR_LOOKUP_ERROR once and for all, and from then on returns IMPL_LOOKUP.in(beanClass) - a lookup without private access. LambdaMetafactory rejects it with "LambdaConversionException: Invalid caller", so every bean getter fails. This never reproduces under surefire because JUnit initializes MethodHandles long before fastjson2 is loaded, which is why the JDK 8 CI jobs stayed green. - JDKUtils: call MethodHandles.lookup() before the Unsafe read, so MethodHandles.Lookup is initialized and IMPL_LOOKUP is the real trusted lookup; read it from staticFieldBase() rather than assuming the base is the Class mirror - PropertyAccessorFactoryLambda: keep a safety net for the environments where no trusted lookup can be obtained at all - check the lookup's PRIVATE mode up front and go straight to the reflection accessors, extract the lambda path into createLambdaAccessor() and fall back to super.create() if it still fails, rethrowing VirtualMachineError - PropertyAccessorFactoryLambda.getObject() now uses lookup(Method) like the other accessor methods instead of calling JDKUtils.trustedLookup() directly Verified on JDK 8 (zulu8.0.502): JSON.toJSONString(bean) reproduced the exact stack trace of #7691 before the change, and afterwards trustedLookup() returns /trusted with lookupModes=15, so the lambda fast path is used rather than the reflection fallback.
5f14bbe to
42f4144
Compare
Problem
Since 2.0.61,
JSON.toJSONString(bean)throwsLambdaConversionException: Invalid calleron JDK 8 for any Bean with getters, making Bean serialization unusable.Root cause
JDKUtilsreadsMethodHandles.Lookup.IMPL_LOOKUPstraight from memory withUnsafe:Neither
.classnorgetDeclaredFieldtriggers class initialization, so if nothing has usedMethodHandlesbefore fastjson2 is loaded,Lookup.<clinit>has not run yet and the field is still null. Verified on JDK 8:From there the whole chain degrades:
IMPL_LOOKUPis null, soJDKUtilsfalls back toMethodHandles.lookup(), whoselookupClassisJDKUtils— not trusted.trustedLookup(Class)therefore cannot find the privateLookup(Class, int)constructor, and sets the stickyCONSTRUCTOR_LOOKUP_ERRORflag.trustedLookup()call returnsIMPL_LOOKUP.in(beanClass);Lookup.in()drops the PRIVATE bit.LambdaMetafactoryrejects a caller without private access:LambdaConversionException: Invalid caller.This never reproduces under surefire, because JUnit initializes
MethodHandleslong before fastjson2 is loaded — which is why theTest JDK 8CI jobs stayed green onmain.Fix
JDKUtils(the actual fix) — callMethodHandles.lookup()before theUnsaferead soMethodHandles.Lookupis initialized andIMPL_LOOKUPis the real trusted lookup. Also read it fromUNSAFE.staticFieldBase(implLookup)instead of assuming the static-field base is theClassmirror.PropertyAccessorFactoryLambda(safety net) — for environments where no trusted lookup can be obtained at all (Android, SecurityManager, GraalVM, a future JDK withoutUnsafe):createLambdaAccessor()and fall back tosuper.create()if it fails anyway, rethrowingVirtualMachineError;getObject(Method)now useslookup(Method)like every other accessor method, instead of callingJDKUtils.trustedLookup()directly.Verification
On JDK 8 (zulu8.0.502), a plain
maincallingJSON.toJSONString(people)reproduced the exact stack trace of #7691 before the change:After the change it serializes normally, and
JDKUtils.trustedLookup(Bean.class)returns/trustedwithlookupModes = 15, so the lambda fast path is used rather than the reflection fallback — no performance regression.Test suites:
core7956 tests pass, and also with-Dfastjson2.creator=reflect;core+extension+fastjson1-compatible+kotlin+safemode-test9397 tests pass.Tests
Issue7691covers serialization/round-trip for String/Integer/Date/int properties and non-public beans, plus two tests that pin the fallback with a factory whoselookup()has no private access and one whose lambda creation fails. Both fail with theLambdaConversionExceptionfrom the issue when thePropertyAccessorFactoryLambdachange is reverted.Note:
testTrustedLookupHasPrivateAccessasserts the invariant the lambda path depends on, but it cannot fail in a surefire JVM, sinceMethodHandles.Lookupis already initialized there. TheJDKUtilsfix was verified manually on JDK 8 as described above.Closes #7691