diff --git a/src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java index 73c65100a6..e29dbeb611 100644 --- a/src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java @@ -1,5 +1,6 @@ package tools.jackson.databind.deser; +import java.lang.reflect.Modifier; import java.util.*; import com.fasterxml.jackson.annotation.*; @@ -938,19 +939,16 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext JavaType type = resolveMemberAndTypeAnnotations(ctxt, mutator, propType0); // Does the Method specify the deserializer to use? If so, let's use it. TypeDeserializer typeDeser = (TypeDeserializer) type.getTypeHandler(); - SettableBeanProperty prop; - if (mutator instanceof AnnotatedMethod) { - prop = new MethodProperty(propDef, type, typeDeser, - beanDescRef.getClassAnnotations(), (AnnotatedMethod) mutator); - } else { - // 08-Sep-2016, tatu: wonder if we should verify it is `AnnotatedField` to be safe? - prop = new FieldProperty(propDef, type, typeDeser, - beanDescRef.getClassAnnotations(), (AnnotatedField) mutator); + // 05-May-2025, tatu: [databind#5090]/[databind#2083] Need to skip these for some reason + if (isFinalField(mutator)) { + return null; } ValueDeserializer deser = findDeserializerFromAnnotation(ctxt, mutator); if (deser == null) { deser = (ValueDeserializer) type.getValueHandler(); } + SettableBeanProperty prop = new MethodProperty(propDef, type, typeDeser, + beanDescRef.getClassAnnotations(), mutator); if (deser != null) { deser = ctxt.handlePrimaryContextualization(deser, prop, type); prop = prop.withValueDeserializer(deser); @@ -967,6 +965,11 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext return prop; } + private boolean isFinalField(AnnotatedMember am) { + return am instanceof AnnotatedField + && Modifier.isFinal(am.getMember().getModifiers()); + } + /** * Method that will construct a regular bean property setter using * the given setter method. diff --git a/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java b/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java index e55df09920..35498359ee 100644 --- a/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java +++ b/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java @@ -633,6 +633,7 @@ protected void _throwAsJacksonE(JsonParser p, Throwable e, Object value) protected void _throwAsJacksonE(JsonParser p, Throwable e) throws JacksonException { + ClassUtil.throwIfError(e); ClassUtil.throwIfRTE(e); ClassUtil.throwIfJacksonE(e); diff --git a/src/main/java/tools/jackson/databind/deser/impl/FieldProperty.java b/src/main/java/tools/jackson/databind/deser/impl/FieldProperty.java deleted file mode 100644 index 9818f215b2..0000000000 --- a/src/main/java/tools/jackson/databind/deser/impl/FieldProperty.java +++ /dev/null @@ -1,213 +0,0 @@ -package tools.jackson.databind.deser.impl; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonParser; -import tools.jackson.core.JsonToken; -import tools.jackson.databind.*; -import tools.jackson.databind.deser.NullValueProvider; -import tools.jackson.databind.deser.SettableBeanProperty; -import tools.jackson.databind.introspect.AnnotatedField; -import tools.jackson.databind.introspect.AnnotatedMember; -import tools.jackson.databind.introspect.BeanPropertyDefinition; -import tools.jackson.databind.jsontype.TypeDeserializer; -import tools.jackson.databind.util.Annotations; -import tools.jackson.databind.util.ClassUtil; - -/** - * This concrete sub-class implements property that is set - * directly assigning to a Field. - */ -public final class FieldProperty - extends SettableBeanProperty -{ - final protected AnnotatedField _annotated; - - /** - * Actual field to set when deserializing this property. - * Transient since there is no need to persist; only needed during - * construction of objects. - */ - final protected transient Field _field; - - /** - * @since 2.9 - */ - final protected boolean _skipNulls; - - public FieldProperty(BeanPropertyDefinition propDef, JavaType type, - TypeDeserializer typeDeser, Annotations contextAnnotations, AnnotatedField field) - { - super(propDef, type, typeDeser, contextAnnotations); - _annotated = field; - _field = field.getAnnotated(); - _skipNulls = NullsConstantProvider.isSkipper(_nullProvider); - } - - protected FieldProperty(FieldProperty src, ValueDeserializer deser, - NullValueProvider nva) { - super(src, deser, nva); - _annotated = src._annotated; - _field = src._field; - _skipNulls = NullsConstantProvider.isSkipper(nva); - } - - protected FieldProperty(FieldProperty src, PropertyName newName) { - super(src, newName); - _annotated = src._annotated; - _field = src._field; - _skipNulls = src._skipNulls; - } - - /** - * Constructor used for JDK Serialization when reading persisted object - */ - protected FieldProperty(FieldProperty src) - { - super(src); - _annotated = src._annotated; - Field f = _annotated.getAnnotated(); - if (f == null) { - throw new IllegalArgumentException("Missing field (broken JDK (de)serialization?)"); - } - _field = f; - _skipNulls = src._skipNulls; - } - - @Override - public SettableBeanProperty withName(PropertyName newName) { - return new FieldProperty(this, newName); - } - - @Override - public SettableBeanProperty withValueDeserializer(ValueDeserializer deser) { - if (_valueDeserializer == deser) { - return this; - } - // 07-May-2019, tatu: As per [databind#2303], must keep VD/NVP in-sync if they were - NullValueProvider nvp = (_valueDeserializer == _nullProvider) ? deser : _nullProvider; - return new FieldProperty(this, deser, nvp); - } - - @Override - public SettableBeanProperty withNullProvider(NullValueProvider nva) { - return new FieldProperty(this, _valueDeserializer, nva); - } - - @Override - public void fixAccess(DeserializationConfig config) { - ClassUtil.checkAndFixAccess(_field, - config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)); - } - - /* - /********************************************************** - /* BeanProperty impl - /********************************************************** - */ - - @Override - public A getAnnotation(Class acls) { - return (_annotated == null) ? null : _annotated.getAnnotation(acls); - } - - @Override public AnnotatedMember getMember() { return _annotated; } - - /* - /********************************************************** - /* Overridden methods - /********************************************************** - */ - - @Override - public void deserializeAndSet(JsonParser p, - DeserializationContext ctxt, Object instance) throws JacksonException - { - Object value; - if (p.hasToken(JsonToken.VALUE_NULL)) { - if (_skipNulls) { - return; - } - value = _nullProvider.getNullValue(ctxt); - } else if (_valueTypeDeserializer == null) { - value = _valueDeserializer.deserialize(p, ctxt); - // 04-May-2018, tatu: [databind#2023] Coercion from String (mostly) can give null - if (value == null) { - if (_skipNulls) { - return; - } - value = _nullProvider.getNullValue(ctxt); - } - } else { - value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); - } - try { - _field.set(instance, value); - } catch (Exception e) { - _throwAsJacksonE(p, e, value); - } - } - - @Override - public Object deserializeSetAndReturn(JsonParser p, - DeserializationContext ctxt, Object instance) throws JacksonException - { - Object value; - if (p.hasToken(JsonToken.VALUE_NULL)) { - if (_skipNulls) { - return instance; - } - value = _nullProvider.getNullValue(ctxt); - } else if (_valueTypeDeserializer == null) { - value = _valueDeserializer.deserialize(p, ctxt); - // 04-May-2018, tatu: [databind#2023] Coercion from String (mostly) can give null - if (value == null) { - if (_skipNulls) { - return instance; - } - value = _nullProvider.getNullValue(ctxt); - } - } else { - value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); - } - try { - _field.set(instance, value); - } catch (Exception e) { - _throwAsJacksonE(p, e, value); - } - return instance; - } - - @Override - public void set(DeserializationContext ctxt, Object instance, Object value) - { - if (value == null) { - if (_skipNulls) { - return; - } - } - try { - _field.set(instance, value); - } catch (Exception e) { - _throwAsJacksonE(ctxt.getParser(), e, value); - } - } - - @Override - public Object setAndReturn(DeserializationContext ctxt, Object instance, Object value) - { - if (value == null) { - if (_skipNulls) { - return instance; - } - } - try { - _field.set(instance, value); - } catch (Exception e) { - _throwAsJacksonE(ctxt.getParser(), e, value); - } - return instance; - } -} diff --git a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java index 26439b4e49..a4849ef918 100644 --- a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java +++ b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java @@ -1,7 +1,9 @@ package tools.jackson.databind.deser.impl; import java.lang.annotation.Annotation; -import java.lang.reflect.Method; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; @@ -12,31 +14,37 @@ import tools.jackson.databind.introspect.*; import tools.jackson.databind.jsontype.TypeDeserializer; import tools.jackson.databind.util.Annotations; +import tools.jackson.databind.util.internal.UnreflectHandleSupplier; + +import static java.lang.invoke.MethodType.methodType; /** * This concrete sub-class implements property that is set - * using regular "setter" method. + * using a {@link MethodHandle} to the setter, which is either + * a setter method or a field setter. */ public final class MethodProperty extends SettableBeanProperty { - protected final AnnotatedMethod _annotated; + protected final AnnotatedMember _annotated; /** - * Setter method for modifying property value; used for - * "regular" method-accessible properties. + * Setter MethodHandle holder for modifying property value. + */ + protected final SetterHolder _setter = new SetterHolder(methodType(void.class, Object.class, Object.class)); + /** + * Setter MethodHandle holder for modifying property value and returning the modified bean. */ - protected final transient Method _setter; + protected final SetterHolder _setterReturn = new SetterHolder(methodType(Object.class, Object.class, Object.class)); protected final boolean _skipNulls; public MethodProperty(BeanPropertyDefinition propDef, JavaType type, TypeDeserializer typeDeser, - Annotations contextAnnotations, AnnotatedMethod method) + Annotations contextAnnotations, AnnotatedMember annotated) { super(propDef, type, typeDeser, contextAnnotations); - _annotated = method; - _setter = method.getAnnotated(); + _annotated = annotated; _skipNulls = NullsConstantProvider.isSkipper(_nullProvider); } @@ -44,24 +52,12 @@ protected MethodProperty(MethodProperty src, ValueDeserializer deser, NullValueProvider nva) { super(src, deser, nva); _annotated = src._annotated; - _setter = src._setter; _skipNulls = NullsConstantProvider.isSkipper(nva); } protected MethodProperty(MethodProperty src, PropertyName newName) { super(src, newName); _annotated = src._annotated; - _setter = src._setter; - _skipNulls = src._skipNulls; - } - - /** - * Constructor used for JDK Serialization when reading persisted object - */ - protected MethodProperty(MethodProperty src, Method m) { - super(src); - _annotated = src._annotated; - _setter = m; _skipNulls = src._skipNulls; } @@ -133,8 +129,8 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } try { - _setter.invoke(instance, value); - } catch (Exception e) { + _setter.get().invokeExact(instance, value); + } catch (Throwable e) { _throwAsJacksonE(p, e, value); } } @@ -162,9 +158,9 @@ public Object deserializeSetAndReturn(JsonParser p, value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } try { - Object result = _setter.invoke(instance, value); + Object result = _setterReturn.get().invokeExact(instance, value); return (result == null) ? instance : result; - } catch (Exception e) { + } catch (Throwable e) { _throwAsJacksonE(p, e, value); return null; } @@ -180,8 +176,8 @@ public final void set(DeserializationContext ctxt, Object instance, Object value } } try { - _setter.invoke(instance, value); - } catch (Exception e) { + _setter.get().invokeExact(instance, value); + } catch (Throwable e) { _throwAsJacksonE(ctxt.getParser(), e, value); } } @@ -196,11 +192,28 @@ public Object setAndReturn(DeserializationContext ctxt, } } try { - Object result = _setter.invoke(instance, value); + Object result = _setterReturn.get().invokeExact(instance, value); return (result == null) ? instance : result; - } catch (Exception e) { + } catch (Throwable e) { _throwAsJacksonE(ctxt.getParser(), e, value); return null; } } + + class SetterHolder extends UnreflectHandleSupplier { + SetterHolder(MethodType asType) { + super(asType); + } + + @Override + protected MethodHandle unreflect() throws IllegalAccessException { + if (_annotated instanceof AnnotatedMethod) { + AnnotatedMethod am = (AnnotatedMethod) _annotated; + return MethodHandles.lookup().unreflect(am.getAnnotated()); + } else { + AnnotatedField af = (AnnotatedField) _annotated; + return MethodHandles.lookup().unreflectSetter(af.getAnnotated()); + } + } + } } diff --git a/src/main/java/tools/jackson/databind/introspect/AnnotatedConstructor.java b/src/main/java/tools/jackson/databind/introspect/AnnotatedConstructor.java index 77130960e6..4a452f99cb 100644 --- a/src/main/java/tools/jackson/databind/introspect/AnnotatedConstructor.java +++ b/src/main/java/tools/jackson/databind/introspect/AnnotatedConstructor.java @@ -1,5 +1,8 @@ package tools.jackson.databind.introspect; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Member; import java.lang.reflect.Parameter; @@ -8,11 +11,17 @@ import tools.jackson.databind.JavaType; import tools.jackson.databind.util.ClassUtil; +import tools.jackson.databind.util.internal.UnreflectHandleSupplier; + +import static java.lang.invoke.MethodType.methodType; public final class AnnotatedConstructor extends AnnotatedWithParams { protected final Constructor _constructor; + private final InvokerHolder _invokerNullary = new InvokerHolder(methodType(Object.class)); + private final InvokerHolder _invokerUnary = new InvokerHolder(methodType(Object.class, Object.class)); + private final InvokerHolder _invokerFixedArity = new InvokerHolder(null); /* /********************************************************************** @@ -91,19 +100,29 @@ public Parameter[] getNativeParameters() { @Override public final Object call() throws Exception { - // 31-Mar-2021, tatu: Note! This is faster than calling without arguments - // because JDK in its wisdom would otherwise allocate `new Object[0]` to pass - return _constructor.newInstance((Object[]) null); + try { + return _invokerNullary.get().invokeExact(); + } catch (Throwable e) { + throw ClassUtil.sneakyThrow(e); + } } @Override public final Object call(Object[] args) throws Exception { - return _constructor.newInstance(args); + try { + return _invokerFixedArity.get().invokeWithArguments(args); + } catch (Throwable e) { + throw ClassUtil.sneakyThrow(e); + } } @Override public final Object call1(Object arg) throws Exception { - return _constructor.newInstance(arg); + try { + return _invokerUnary.get().invokeExact(arg); + } catch (Throwable e) { + throw ClassUtil.sneakyThrow(e); + } } /* @@ -163,4 +182,17 @@ public boolean equals(Object o) { AnnotatedConstructor other = (AnnotatedConstructor) o; return Objects.equals(_constructor, other._constructor); } + + class InvokerHolder extends UnreflectHandleSupplier { + private static final long serialVersionUID = 1L; + + InvokerHolder(MethodType asType) { + super(asType); + } + + @Override + protected MethodHandle unreflect() throws IllegalAccessException { + return MethodHandles.lookup().unreflectConstructor(_constructor); + } + } } diff --git a/src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java b/src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java index 6279615545..148aa3771d 100644 --- a/src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java +++ b/src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java @@ -10,6 +10,7 @@ import java.util.List; import tools.jackson.databind.JavaType; +import tools.jackson.databind.MapperFeature; import tools.jackson.databind.cfg.MapperConfig; import tools.jackson.databind.introspect.AnnotatedClass.Creators; import tools.jackson.databind.util.ClassUtil; @@ -65,25 +66,18 @@ Creators collect(Class primaryMixIn) /* And then... let's remove all constructors that are deemed * ignorable after all annotations have been properly collapsed. */ + // AnnotationIntrospector is null if annotations not enabled; if so, can skip: if (_collectAnnotations) { - if (_defaultConstructor != null) { - if (_intr.hasIgnoreMarker(_config, _defaultConstructor)) { - _defaultConstructor = null; - } - } - // count down to allow safe removal - for (int i = constructors.size(); --i >= 0; ) { - if (_intr.hasIgnoreMarker(_config, constructors.get(i))) { - constructors.remove(i); - } - } - for (int i = factories.size(); --i >= 0; ) { - if (_intr.hasIgnoreMarker(_config, factories.get(i))) { - factories.remove(i); - } + if (_defaultConstructor != null && _intr.hasIgnoreMarker(_config, _defaultConstructor)) { + _defaultConstructor = null; } + constructors.removeIf(constructor -> + _intr.hasIgnoreMarker(_config, constructor)); + factories.removeIf(factory -> + _intr.hasIgnoreMarker(_config, factory)); } + return new AnnotatedClass.Creators(_defaultConstructor, constructors, factories); } diff --git a/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java b/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java index 26ca34c220..4997a1cf4a 100644 --- a/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java +++ b/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java @@ -1,15 +1,30 @@ package tools.jackson.databind.introspect; -import java.lang.reflect.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; import java.util.Objects; import tools.jackson.databind.JavaType; import tools.jackson.databind.util.ClassUtil; +import tools.jackson.databind.util.internal.UnreflectHandleSupplier; + +import static java.lang.invoke.MethodType.methodType; + +import static tools.jackson.databind.util.ClassUtil.sneakyThrow; public final class AnnotatedMethod extends AnnotatedWithParams { final protected transient Method _method; + final protected MethodHolder _invokerFixedArity = new MethodHolder(null); + final protected MethodHolder _invokerNullary = new MethodHolder(methodType(Object.class)); + final protected MethodHolder _invokerUnary = new MethodHolder(methodType(Object.class, Object.class)); // // Simple lazy-caching: @@ -73,27 +88,50 @@ public Class getRawType() { @Override public final Object call() throws Exception { - // 31-Mar-2021, tatu: Note! This is faster than calling without arguments - // because JDK in its wisdom would otherwise allocate `new Object[0]` to pass - return _method.invoke(null); + try { + return _invokerNullary.get().invokeExact(); + } catch (final Throwable e) { + throw sneakyThrow(e); + } } @Override public final Object call(Object[] args) throws Exception { - return _method.invoke(null, args); + try { + return _invokerFixedArity.get().invokeWithArguments(args); + } catch (final Throwable e) { + throw sneakyThrow(e); + } } @Override public final Object call1(Object arg) throws Exception { - return _method.invoke(null, arg); + try { + Object ret = _invokerUnary.get().invokeExact(arg); + return ret == null ? arg : ret; + } catch (final Throwable e) { + throw sneakyThrow(e); + } } public final Object callOn(Object pojo) throws Exception { - return _method.invoke(pojo, (Object[]) null); + try { + return _invokerUnary.get().invokeExact(pojo); + } catch (Throwable e) { + throw sneakyThrow(e); + } } public final Object callOnWith(Object pojo, Object... args) throws Exception { - return _method.invoke(pojo, args); + try { + MethodHandle invoker = _invokerFixedArity.get(); + if (!Modifier.isStatic(_method.getModifiers())) { + invoker = invoker.bindTo(pojo); + } + return invoker.invokeWithArguments(args); + } catch (Throwable e) { + throw sneakyThrow(e); + } } /* @@ -212,4 +250,15 @@ public boolean equals(Object o) { AnnotatedMethod other = (AnnotatedMethod) o; return Objects.equals(_method, other._method); } + + class MethodHolder extends UnreflectHandleSupplier { + MethodHolder(MethodType asType) { + super(asType); + } + + @Override + protected MethodHandle unreflect() throws IllegalAccessException { + return MethodHandles.lookup().unreflect(_method); + } + } } diff --git a/src/main/java/tools/jackson/databind/introspect/AnnotatedMethodCollector.java b/src/main/java/tools/jackson/databind/introspect/AnnotatedMethodCollector.java index c16141c26a..0f48cb370a 100644 --- a/src/main/java/tools/jackson/databind/introspect/AnnotatedMethodCollector.java +++ b/src/main/java/tools/jackson/databind/introspect/AnnotatedMethodCollector.java @@ -3,6 +3,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; import java.util.*; import tools.jackson.databind.JavaType; @@ -123,15 +124,7 @@ private void _addMemberMethods(TypeResolutionContext tc, Method old = b.method; if (old == null) { // had "mix-over", replace b.method = m; -// } else if (old.getDeclaringClass().isInterface() && !m.getDeclaringClass().isInterface()) { - } else if (Modifier.isAbstract(old.getModifiers()) - && !Modifier.isAbstract(m.getModifiers())) { - // 06-Jan-2010, tatu: Except that if method we saw first is - // from an interface, and we now find a non-interface definition, we should - // use this method, but with combination of annotations. - // This helps (or rather, is essential) with JAXB annotations and - // may also result in faster method calls (interface calls are slightly - // costlier than regular method calls) + } else if (shouldReplace(old, m)) { b.method = m; // 23-Aug-2017, tatu: [databind#1705] Also need to change the type resolution context if so // (note: mix-over case above shouldn't need it) @@ -141,6 +134,45 @@ private void _addMemberMethods(TypeResolutionContext tc, } } + // 06-Jan-2010, tatu: Except that if method we saw first is + // from an interface, and we now find a non-interface definition, we should + // use this method, but with combination of annotations. + // This helps (or rather, is essential) with JAXB annotations and + // may also result in faster method calls (interface calls are slightly + // costlier than regular method calls) + // 03-Jan-2021, scs: But we should probably prefer more public interface methods + // (and respect class visibility too!) rather than relying on setAccessible + private boolean shouldReplace(Method current, Method replace) { + if (accessLevel(replace) > accessLevel(current)) { + return true; + } + if (Proxy.isProxyClass(current.getDeclaringClass()) + && !Proxy.isProxyClass(replace.getDeclaringClass())) { + return true; + } + return Modifier.isAbstract(current.getModifiers()) + && !Modifier.isAbstract(replace.getModifiers()); + } + + private static int accessLevel(Method m) { + return Math.min( + accessLevel(m.getModifiers()), + accessLevel(m.getDeclaringClass().getModifiers())); + } + + private static int accessLevel(int modifiers) { + if (Modifier.isPublic(modifiers)) { + return 3; + } + if (Modifier.isProtected(modifiers)) { + return 2; + } + if (Modifier.isPrivate(modifiers)) { + return 0; + } + return 1; + } + protected void _addMethodMixIns(TypeResolutionContext tc, Class targetClass, Map methods, Class mixInCls) { diff --git a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java index 74871c1123..16d757352f 100644 --- a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java +++ b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java @@ -1,6 +1,8 @@ package tools.jackson.databind.ser; import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; @@ -23,6 +25,11 @@ import tools.jackson.databind.util.Annotations; import tools.jackson.databind.util.ClassUtil; import tools.jackson.databind.util.NameTransformer; +import tools.jackson.databind.util.internal.UnreflectHandleSupplier; + +import static java.lang.invoke.MethodType.methodType; + +import static tools.jackson.databind.util.ClassUtil.sneakyThrow; /** * Base bean property handler class, which implements common parts of @@ -106,20 +113,10 @@ public class BeanPropertyWriter protected final AnnotatedMember _member; /** - * Accessor method used to get property value, for method-accessible - * properties. Null if and only if {@link #_field} is null. - *

- * `transient` (and non-final) only to support JDK serializability. + * Accessor method used to get property value. + * Wrapped in a holder since MethodHandle is not serializable. */ - protected transient Method _accessorMethod; - - /** - * Field that contains the property value for field-accessible properties. - * Null if and only if {@link #_accessorMethod} is null. - *

- * `transient` (and non-final) only to support JDK serializability. - */ - protected transient Field _field; + protected final GetterHolder _accessor = new GetterHolder(); /* /********************************************************************** @@ -216,18 +213,6 @@ public BeanPropertyWriter(BeanPropertyDefinition propDef, _typeSerializer = typeSer; _cfgSerializationType = serType; - if (member instanceof AnnotatedField) { - _accessorMethod = null; - _field = (Field) member.getMember(); - } else if (member instanceof AnnotatedMethod) { - _accessorMethod = (Method) member.getMember(); - _field = null; - } else { - // 01-Dec-2014, tatu: Used to be illegal, but now explicitly allowed - // for virtual props - _accessorMethod = null; - _field = null; - } _suppressNulls = suppressNulls; _suppressableValue = suppressableValue; @@ -256,8 +241,6 @@ protected BeanPropertyWriter() { _typeSerializer = null; _cfgSerializationType = null; - _accessorMethod = null; - _field = null; _suppressNulls = false; _suppressableValue = null; @@ -284,8 +267,6 @@ protected BeanPropertyWriter(BeanPropertyWriter base, PropertyName name) { _declaredType = base._declaredType; _member = base._member; - _accessorMethod = base._accessorMethod; - _field = base._field; _serializer = base._serializer; _nullSerializer = base._nullSerializer; @@ -311,8 +292,6 @@ protected BeanPropertyWriter(BeanPropertyWriter base, SerializedString name) { _member = base._member; _contextAnnotations = base._contextAnnotations; _declaredType = base._declaredType; - _accessorMethod = base._accessorMethod; - _field = base._field; _serializer = base._serializer; _nullSerializer = base._nullSerializer; if (base._internalSettings != null) { @@ -582,9 +561,7 @@ public Class[] getViews() { public void serializeAsProperty(Object bean, JsonGenerator g, SerializationContext ctxt) throws Exception { - // inlined 'get()' - final Object value = (_accessorMethod == null) ? _field.get(bean) - : _accessorMethod.invoke(bean, (Object[]) null); + final Object value = get(bean); // Null handling is bit different, check that first if (value == null) { @@ -657,9 +634,7 @@ public void serializeAsOmittedProperty(Object bean, JsonGenerator g, Serializati public void serializeAsElement(Object bean, JsonGenerator g, SerializationContext ctxt) throws Exception { - // inlined 'get()' - final Object value = (_accessorMethod == null) ? _field.get(bean) - : _accessorMethod.invoke(bean, (Object[]) null); + final Object value = get(bean); if (value == null) { // nulls need specialized handling if (_nullSerializer != null) { _nullSerializer.serialize(null, g, ctxt); @@ -771,9 +746,12 @@ protected ValueSerializer _findAndAddDynamic(PropertySerializerMap map, * Method that can be used to access value of the property this Object * describes, from given bean instance. */ - public Object get(Object bean) throws Exception { - return (_accessorMethod == null) ? _field.get(bean) : _accessorMethod - .invoke(bean, (Object[]) null); + public final Object get(Object bean) throws Exception { + try { + return _accessor.get().invokeExact(bean); + } catch (Throwable e) { + throw sneakyThrow(e); + } } /** @@ -823,13 +801,9 @@ protected boolean _handleSelfReference(Object bean, JsonGenerator g, public String toString() { StringBuilder sb = new StringBuilder(40); sb.append("property '").append(getName()).append("' ("); - if (_accessorMethod != null) { - sb.append("via method ") - .append(_accessorMethod.getDeclaringClass().getName()) - .append("#").append(_accessorMethod.getName()); - } else if (_field != null) { - sb.append("field \"").append(_field.getDeclaringClass().getName()) - .append("#").append(_field.getName()); + if (_accessor != null) { + sb.append("via methodhandle ") + .append(_accessor); } else { sb.append("virtual"); } @@ -842,4 +816,25 @@ public String toString() { sb.append(')'); return sb.toString(); } + + class GetterHolder extends UnreflectHandleSupplier { + private static final long serialVersionUID = 1L; + + public GetterHolder() { + super(methodType(Object.class, Object.class)); + } + + @Override + protected MethodHandle unreflect() throws IllegalAccessException { + if (_member instanceof AnnotatedField) { + return MethodHandles.lookup().unreflectGetter((Field) _member.getMember()); + } else if (_member instanceof AnnotatedMethod) { + return MethodHandles.lookup().unreflect((Method) _member.getMember()); + } else { + // 01-Dec-2014, tatu: Used to be illegal, but now explicitly allowed + // for virtual props + return null; + } + } + } } diff --git a/src/main/java/tools/jackson/databind/util/ClassUtil.java b/src/main/java/tools/jackson/databind/util/ClassUtil.java index f8d8fac84e..296c569434 100644 --- a/src/main/java/tools/jackson/databind/util/ClassUtil.java +++ b/src/main/java/tools/jackson/databind/util/ClassUtil.java @@ -3,6 +3,7 @@ import java.io.Closeable; import java.io.IOException; import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; import java.lang.reflect.*; import java.util.*; import java.util.stream.Stream; @@ -24,6 +25,13 @@ public final class ClassUtil private final static Iterator EMPTY_ITERATOR = Collections.emptyIterator(); + private final static List JDK_PREFIXES = Arrays.asList( + "java.", + "javax.", + "jdk.", + "sun.", + "com.sun."); + /* /********************************************************************** /* Simple factory methods @@ -361,6 +369,14 @@ public static void unwrapAndThrowAsIAE(Throwable t, String msg) throwAsIAE(getRootCause(t), msg); } + /** + * Throw an exception even if it's checked and not declared. + */ + @SuppressWarnings("unchecked") + public static RuntimeException sneakyThrow(Throwable throwable) throws E { + throw (E) throwable; + } + /** * Helper method that encapsulate logic in trying to close output generator * in case of failure; useful mostly in forcing flush()ing as otherwise @@ -830,38 +846,28 @@ public static void checkAndFixAccess(Member member, boolean evenIfAlreadyPublic) // We know all members are also accessible objects... AccessibleObject ao = (AccessibleObject) member; - // 14-Jan-2009, tatu: It seems safe and potentially beneficial to - // always to make it accessible (latter because it will force - // skipping checks we have no use for...), so let's always call it. try { // 15-Apr-2021, tatu: With JDK 14+ we will be hitting access limitations // esp. wrt JDK types so let's change a bit final Class declaringClass = member.getDeclaringClass(); boolean isPublic = Modifier.isPublic(member.getModifiers()) && Modifier.isPublic(declaringClass.getModifiers()); - if (!isPublic || (evenIfAlreadyPublic && !isJDKClass(declaringClass))) { + if (!isJDKClass(declaringClass) && (!isPublic || evenIfAlreadyPublic)) { ao.setAccessible(true); } - } catch (SecurityException se) { - // 17-Apr-2009, tatu: This can fail on platforms like - // Google App Engine); so let's only fail if we really needed it... - if (!ao.isAccessible()) { - Class declClass = member.getDeclaringClass(); - throw new IllegalArgumentException("Cannot access "+member+" (from class "+declClass.getName() - +"; failed to set access: "+exceptionMessage(se)); - } - // 14-Apr-2021, tatu: [databind#3118] Java 9/JPMS causes new fails... - // But while our baseline is Java 8, must check name - } catch (RuntimeException se) { - if ("InaccessibleObjectException".equals(se.getClass().getSimpleName())) { - throw new IllegalArgumentException(String.format( -"Failed to call `setAccess()` on %s '%s' (of class %s) due to `%s`, problem: %s", -member.getClass().getSimpleName(), member.getName(), -nameOf(member.getDeclaringClass()), -se.getClass().getName(), se.getMessage()), - se); + } catch (SecurityException ignore) { + } catch (RuntimeException e) { + // 2025-03-24, scs: Since Java module system, it is common to be unable to see + // members of other modules. We can't assume we can crack open arbitrary types anymore. + // We'd love to catch this more explicitly, but Android... >:( + if ("InaccessibleObjectException".equals(e.getClass().getSimpleName())) { + return; } - throw se; + throw new IllegalArgumentException(String.format( + "Failed to call `setAccess()` on %s '%s' (of class %s) due to `%s`, problem: %s", + member.getClass().getSimpleName(), member.getName(), + nameOf(member.getDeclaringClass()), + e.getClass().getName(), e.getMessage()), e); } } @@ -972,10 +978,12 @@ public static boolean isJacksonStdImpl(Class implClass) { */ public static boolean isJDKClass(Class rawType) { final String clsName = rawType.getName(); - return clsName.startsWith("java.") - || clsName.startsWith("javax.") - || clsName.startsWith("sun.") - ; + for (String prefix : JDK_PREFIXES) { + if (clsName.startsWith(prefix)) { + return true; + } + } + return false; } /** diff --git a/src/main/java/tools/jackson/databind/util/internal/UnreflectHandleSupplier.java b/src/main/java/tools/jackson/databind/util/internal/UnreflectHandleSupplier.java new file mode 100644 index 0000000000..23113ecb63 --- /dev/null +++ b/src/main/java/tools/jackson/databind/util/internal/UnreflectHandleSupplier.java @@ -0,0 +1,60 @@ +package tools.jackson.databind.util.internal; + +import java.io.Serializable; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import java.util.function.Supplier; + +import tools.jackson.databind.util.ClassUtil; + +/** + * Lazy memoized holder for MethodHandles. + * Defers binding of the method handle until after access checks are suppressed + * (which happens in a virtual method call after construction) and avoids serialization of + * MethodHandle. + */ +public abstract class UnreflectHandleSupplier implements Supplier { + private final MethodType asType; + private boolean initialized; + private Supplier delegate = this::initialize; + + public UnreflectHandleSupplier(MethodType asType) { + this.asType = asType; + } + + @Override + public MethodHandle get() { + return delegate.get(); + } + + synchronized MethodHandle initialize() { + if (!initialized) { + MethodHandle mh; + try { + mh = postprocess(unreflect()); + } catch (IllegalAccessException e) { + throw ClassUtil.sneakyThrow(e); + } + delegate = () -> mh; + initialized = true; + } + return delegate.get(); + } + + protected MethodHandle postprocess(MethodHandle mh) { + if (mh == null) { + return mh; + } + if (asType == null) { + return mh.asFixedArity(); + } + return mh.asType(asType); + } + + protected abstract MethodHandle unreflect() throws IllegalAccessException; + + @Override + public String toString() { + return get().toString(); + } +} diff --git a/src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java b/src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java deleted file mode 100644 index a13dfe4a98..0000000000 --- a/src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java +++ /dev/null @@ -1,42 +0,0 @@ -package tools.jackson.databind.mixins; - -import java.util.*; - -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.annotation.*; - -import tools.jackson.databind.*; -import tools.jackson.databind.testutil.DatabindTestUtil; -import tools.jackson.databind.testutil.NoCheckSubTypeValidator; - -import static org.junit.jupiter.api.Assertions.*; - -// [databind#2795]: Regression in 2.11.0, no mix-ins for JDK collections -public class MixinForCreators2795Test extends DatabindTestUtil -{ - @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) - static class UnmodifiableCollectionMixin { - @JsonCreator(mode = JsonCreator.Mode.DELEGATING) - public UnmodifiableCollectionMixin(final Collection collection) { } - } - - @Test - public void testMixinWithUnmmodifiableCollection() throws Exception - { - ObjectMapper mapper = jsonMapperBuilder() - .addMixIn(Collections.unmodifiableCollection(Collections.emptyList()).getClass(), - UnmodifiableCollectionMixin.class) - .activateDefaultTypingAsProperty(new NoCheckSubTypeValidator(), - DefaultTyping.NON_FINAL, "@class") - .build(); - - final List strings = Arrays.asList("1", "2"); - final Collection unmodifiableCollection = Collections.unmodifiableCollection(strings); - final byte[] bytes = mapper.writeValueAsBytes(unmodifiableCollection); - - final Collection result = mapper.readValue(bytes, Collection.class); - - assertEquals(2, result.size()); - } -}