diff --git a/core/src/main/java/com/alibaba/fastjson2/introspect/PropertyAccessorFactoryLambda.java b/core/src/main/java/com/alibaba/fastjson2/introspect/PropertyAccessorFactoryLambda.java index 7505b97b4b..0e1574353d 100644 --- a/core/src/main/java/com/alibaba/fastjson2/introspect/PropertyAccessorFactoryLambda.java +++ b/core/src/main/java/com/alibaba/fastjson2/introspect/PropertyAccessorFactoryLambda.java @@ -27,6 +27,21 @@ protected MethodHandles.Lookup lookup(Class declaringClass) { } } + /** + * LambdaMetafactory requires the caller Lookup to have private access, otherwise it rejects the + * call with {@code LambdaConversionException: Invalid caller}. When {@link #lookup(Class)} cannot + * produce a trusted lookup (for example {@code JDKUtils.trustedLookup} degraded to + * {@code IMPL_LOOKUP.in(declaringClass)}, which drops the PRIVATE bit), the lambda path can never + * succeed, so the reflection based accessors are used directly instead of throwing per property. + * + * @param declaringClass the class declaring the getter/setter + * @return true if lambda accessors can be created for the given class + */ + private boolean lambdaSupported(Class declaringClass) { + MethodHandles.Lookup lookup = lookup(declaringClass); + return lookup != null && (lookup.lookupModes() & MethodHandles.Lookup.PRIVATE) != 0; + } + /** * Creates a Supplier that can instantiate objects using the given constructor * via MethodHandle and LambdaMetafactory for better performance than reflection. @@ -267,51 +282,77 @@ public PropertyAccessor create( } boolean lambda = declaringClass.getName().contains("$$Lambda"); - if (!lambda && (setter == null || !isChainableSetter(setter))) { - if (propertyClass == boolean.class) { - return create(name, getBoolean(getter), setBoolean(setter)); - } - if (JDKUtils.JVM_VERSION == 8) { - if (propertyClass == byte.class) { - return create(name, getByte(getter), setByte(setter)); - } - if (propertyClass == short.class) { - return create(name, getShort(getter), setShort(setter)); - } - if (propertyClass == char.class) { - return create(name, getChar(getter), setChar(setter)); + if (!lambda && (setter == null || !isChainableSetter(setter)) && lambdaSupported(declaringClass)) { + try { + PropertyAccessor accessor = createLambdaAccessor( + name, propertyClass, propertyType, getter, setter, exceptionHandler); + if (accessor != null) { + return accessor; } + } catch (VirtualMachineError e) { + throw e; + } catch (Throwable ignored) { + // the lambda path is not usable for this property, fall back to reflection } - if (propertyClass == int.class) { - return create(name, getInt(getter), setInt(setter)); - } - if (propertyClass == long.class) { - return create(name, getLong(getter), setLong(setter)); + } + + return super.create(name, propertyClass, propertyType, getter, setter, exceptionHandler); + } + + /** + * Creates a LambdaMetafactory based accessor for the given property. + * + * @return the accessor, or null when the property type has no lambda based accessor + */ + private PropertyAccessor createLambdaAccessor( + String name, Class propertyClass, + Type propertyType, + Method getter, + Method setter, + BiFunction exceptionHandler + ) { + if (propertyClass == boolean.class) { + return create(name, getBoolean(getter), setBoolean(setter)); + } + if (JDKUtils.JVM_VERSION == 8) { + if (propertyClass == byte.class) { + return create(name, getByte(getter), setByte(setter)); } - if (propertyClass == float.class) { - return create(name, getFloat(getter), setFloat(setter)); + if (propertyClass == short.class) { + return create(name, getShort(getter), setShort(setter)); } - if (propertyClass == double.class) { - return create(name, getDouble(getter), setDouble(setter)); + if (propertyClass == char.class) { + return create(name, getChar(getter), setChar(setter)); } - if (!propertyClass.isPrimitive()) { - if (propertyType == null) { - if (getter != null) { - propertyType = getter.getGenericReturnType(); - } else { - Type[] parameterTypes = setter.getGenericParameterTypes(); - if (parameterTypes.length == 1) { - propertyType = parameterTypes[0]; - } else if (parameterTypes.length == 2 && String.class.equals(parameterTypes[0])) { - propertyType = parameterTypes[1]; - } + } + if (propertyClass == int.class) { + return create(name, getInt(getter), setInt(setter)); + } + if (propertyClass == long.class) { + return create(name, getLong(getter), setLong(setter)); + } + if (propertyClass == float.class) { + return create(name, getFloat(getter), setFloat(setter)); + } + if (propertyClass == double.class) { + return create(name, getDouble(getter), setDouble(setter)); + } + if (!propertyClass.isPrimitive()) { + if (propertyType == null) { + if (getter != null) { + propertyType = getter.getGenericReturnType(); + } else { + Type[] parameterTypes = setter.getGenericParameterTypes(); + if (parameterTypes.length == 1) { + propertyType = parameterTypes[0]; + } else if (parameterTypes.length == 2 && String.class.equals(parameterTypes[0])) { + propertyType = parameterTypes[1]; } } - return create(name, propertyClass, propertyType, getObject(getter), setObject(name, setter), exceptionHandler); } + return create(name, propertyClass, propertyType, getObject(getter), setObject(name, setter), exceptionHandler); } - - return super.create(name, propertyClass, propertyType, getter, setter, exceptionHandler); + return null; } /** @@ -500,8 +541,7 @@ public Function getObject(Method method) { if (method == null) { return null; } - Class declaringClass = method.getDeclaringClass(); - MethodHandles.Lookup lookup = JDKUtils.trustedLookup(declaringClass); + MethodHandles.Lookup lookup = lookup(method); try { MethodHandle handle = lookup.unreflect(method); return (Function) LambdaMetafactory.metafactory( diff --git a/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java b/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java index c105cd215a..9a92435fcc 100644 --- a/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java +++ b/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java @@ -228,16 +228,21 @@ public class JDKUtils { MethodHandles.Lookup trustedLookup = null; if (!ANDROID) { + // MethodHandles.Lookup.IMPL_LOOKUP is read straight from memory, which bypasses class + // initialization. Calling MethodHandles.lookup() first forces MethodHandles.Lookup to be + // initialized, otherwise the field is still null and every trustedLookup() call degrades + // to a lookup without private access, breaking LambdaMetafactory. see issue #7691 + MethodHandles.Lookup callerLookup = MethodHandles.lookup(); try { Class lookupClass = MethodHandles.Lookup.class; Field implLookup = lookupClass.getDeclaredField("IMPL_LOOKUP"); long fieldOffset = UNSAFE.staticFieldOffset(implLookup); - trustedLookup = (MethodHandles.Lookup) UNSAFE.getObject(lookupClass, fieldOffset); + trustedLookup = (MethodHandles.Lookup) UNSAFE.getObject(UNSAFE.staticFieldBase(implLookup), fieldOffset); } catch (Throwable ignored) { // ignored } if (trustedLookup == null) { - trustedLookup = MethodHandles.lookup(); + trustedLookup = callerLookup; } } IMPL_LOOKUP = trustedLookup; diff --git a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7691.java b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7691.java new file mode 100644 index 0000000000..401b9921af --- /dev/null +++ b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7691.java @@ -0,0 +1,246 @@ +package com.alibaba.fastjson2.issues_7000; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.introspect.PropertyAccessor; +import com.alibaba.fastjson2.introspect.PropertyAccessorFactoryLambda; +import com.alibaba.fastjson2.util.JDKUtils; +import org.junit.jupiter.api.Test; + +import java.lang.invoke.LambdaConversionException; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.util.Date; +import java.util.function.Function; +import java.util.function.ToIntFunction; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Issue7691 { + public static class People { + private String name; + private Integer age; + private Date birthday; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Date getBirthday() { + return birthday; + } + + public void setBirthday(Date birthday) { + this.birthday = birthday; + } + } + + public static class Bean { + private int age; + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + } + + static class NonPublicBean { + private String name; + private int value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + } + + /** + * Reproduces the degraded lookup of #7691: {@code JDKUtils.trustedLookup} fell back to + * {@code IMPL_LOOKUP.in(beanClass)}, which drops the PRIVATE bit, and LambdaMetafactory then + * rejects the call with {@code LambdaConversionException: Invalid caller}. + */ + static class NoPrivateAccessFactory + extends PropertyAccessorFactoryLambda { + @Override + protected MethodHandles.Lookup lookup(Class declaringClass) { + return MethodHandles.publicLookup().in(declaringClass); + } + } + + /** + * Has a lookup with private access, so the lambda path is entered, but the lambda creation itself + * fails the way LambdaMetafactory does on an affected JVM. + */ + static class LambdaFailingFactory + extends PropertyAccessorFactoryLambda { + @Override + protected MethodHandles.Lookup lookup(Class declaringClass) { + return MethodHandles.lookup().in(declaringClass); + } + + @Override + public Function getObject(Method method) { + throw lambdaFailure(method); + } + + @Override + public ToIntFunction getInt(Method method) { + throw lambdaFailure(method); + } + + private static RuntimeException lambdaFailure(Method method) { + return new RuntimeException( + "Failed to create lambda for method: " + method, + new LambdaConversionException("Invalid caller: " + method.getDeclaringClass().getName())); + } + } + + /** + * The invariant the whole lambda path depends on: trustedLookup must have private access, + * otherwise LambdaMetafactory rejects every getter/setter. It was violated in #7691 because + * IMPL_LOOKUP was read before MethodHandles.Lookup was initialized, which only happens when + * nothing has used MethodHandles before fastjson2 is loaded - never the case in a test JVM. + */ + @Test + public void testTrustedLookupHasPrivateAccess() { + MethodHandles.Lookup lookup = JDKUtils.trustedLookup(People.class); + assertTrue((lookup.lookupModes() & MethodHandles.Lookup.PRIVATE) != 0, "lookupModes=" + lookup.lookupModes()); + } + + @Test + public void testFallbackWithoutPrivateAccess() throws Exception { + assertFallback(new NoPrivateAccessFactory()); + } + + @Test + public void testFallbackOnLambdaConversionFailure() throws Exception { + assertFallback(new LambdaFailingFactory()); + } + + /** + * The factory must return a working accessor instead of throwing + * {@code RuntimeException: Failed to create lambda for method}. + */ + private static void assertFallback(PropertyAccessorFactoryLambda factory) throws Exception { + Method getName = People.class.getMethod("getName"); + Method setName = People.class.getMethod("setName", String.class); + PropertyAccessor name = factory.create("name", String.class, String.class, getName, setName, null); + assertNotNull(name); + + People people = new People(); + name.setObject(people, "lis"); + assertEquals("lis", people.getName()); + assertEquals("lis", name.getObject(people)); + + Method getAge = Bean.class.getMethod("getAge"); + Method setAge = Bean.class.getMethod("setAge", int.class); + PropertyAccessor age = factory.create("age", int.class, int.class, getAge, setAge, null); + assertNotNull(age); + + Bean bean = new Bean(); + age.setIntValue(bean, 42); + assertEquals(42, bean.getAge()); + assertEquals(42, age.getIntValue(bean)); + } + + @Test + public void testToJSONString() { + People people = new People(); + people.setName("lis"); + people.setAge(20); + people.setBirthday(new Date()); + + String json = JSON.toJSONString(people); + assertNotNull(json); + assertTrue(json.contains("\"name\":\"lis\"")); + assertTrue(json.contains("\"age\":20")); + assertTrue(json.contains("\"birthday\":")); + } + + @Test + public void testRoundTrip() { + People people = new People(); + people.setName("lis"); + people.setAge(20); + people.setBirthday(new Date()); + + String json = JSON.toJSONString(people); + People p2 = JSON.parseObject(json, People.class); + assertEquals("lis", p2.getName()); + assertEquals(20, p2.getAge()); + assertNotNull(p2.getBirthday()); + } + + @Test + public void testToJSONStringPrimitiveInt() { + Bean bean = new Bean(); + bean.setAge(1); + + String json = JSON.toJSONString(bean); + assertEquals("{\"age\":1}", json); + } + + @Test + public void testRoundTripPrimitiveInt() { + Bean bean = new Bean(); + bean.setAge(42); + + String json = JSON.toJSONString(bean); + Bean b2 = JSON.parseObject(json, Bean.class); + assertEquals(42, b2.getAge()); + } + + @Test + public void testNonPublicBean() { + NonPublicBean bean = new NonPublicBean(); + bean.setName("test"); + bean.setValue(99); + + String json = JSON.toJSONString(bean); + assertTrue(json.contains("\"name\":\"test\"")); + assertTrue(json.contains("\"value\":99")); + + NonPublicBean b2 = JSON.parseObject(json, NonPublicBean.class); + assertEquals("test", b2.getName()); + assertEquals(99, b2.getValue()); + } + + @Test + public void testJSONObjectToJSONString() { + People people = new People(); + people.setName("lis"); + people.setAge(20); + + String json = JSONObject.toJSONString(people); + assertNotNull(json); + assertTrue(json.contains("\"name\":\"lis\"")); + } +}