Repository: FasterXML/jackson-databind
Pull request #5046: Replace core reflection usage with MethodHandles
Author: login: stevenschlansker
Created: 2025-03-23T23:57:02Z | Merged: 2025-04-07T01:28:17Z

This should improve performance due to the new design of method handles.
Core reflection has to evaluate everything at the time that you call `method.invoke` - it has to do access checks, determine what parameter conversions are necessary, etc - and does not have any local state to save the result of that work.

With MethodHandle, you do more of the work up-front - determine access, determine what parameter boxing or casts are necessary - and store that computed state in a new MethodHandle.  Later, when you `invokeExact`, the method handle is able to make the call much faster.


Diff:
diff --git a/src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java
index 202e7cdcc1..925bb16e0c 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.*;
@@ -935,14 +936,15 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext
         // 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,
-                    beanDesc.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,
-                    beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
+        // 06-04-2025, scs: we cannot always see members if e.g. they are in a different module that does not
+        // allow our access, so filter such cases out here.
+        if (!ClassUtil.checkAndFixAccess(mutator.getMember(), ctxt.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS))) {
+            return null;
         }
+        if (isFinalField(mutator)) {
+            return null;
+        }
+        prop = new MethodProperty(propDef, type, typeDeser, beanDesc.getClassAnnotations(), mutator);
         ValueDeserializer<?> deser = findDeserializerFromAnnotation(ctxt, mutator);
         if (deser == null) {
             deser = (ValueDeserializer<?>) type.getValueHandler();
@@ -963,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 5e7ce21d4e..511f7f0f13 100644
--- a/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java
+++ b/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java
@@ -609,7 +609,7 @@ public SettableBeanProperty unwrapped(DeserializationContext ctxt, NameTransform
     /**********************************************************************
      */
 
-    protected void _throwAsJacksonE(JsonParser p, Exception e, Object value)
+    protected void _throwAsJacksonE(JsonParser p, Throwable e, Object value)
         throws JacksonException
     {
         if (e instanceof IllegalArgumentException) {
@@ -632,8 +632,9 @@ protected void _throwAsJacksonE(JsonParser p, Exception e, Object value)
         _throwAsJacksonE(p, e);
     }
 
-    protected void _throwAsJacksonE(JsonParser p, Exception e) throws JacksonException
+    protected void _throwAsJacksonE(JsonParser p, Throwable e) throws JacksonException
     {
+        ClassUtil.throwIfError(e);
         ClassUtil.throwIfRTE(e);
         ClassUtil.throwIfJacksonE(e);
         // let's wrap the innermost problem
@@ -643,7 +644,7 @@ protected void _throwAsJacksonE(JsonParser p, Exception e) throws JacksonExcepti
 
     // 10-Oct-2015, tatu: _Should_ be deprecated, too, but its remaining
     //   callers cannot actually provide a JsonParser
-    protected void _throwAsJacksonE(Exception e, Object value) throws JacksonException {
+    protected void _throwAsJacksonE(Throwable e, Object value) throws JacksonException {
         _throwAsJacksonE((JsonParser) null, e, value);
     }
 
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 7b31f46fae..0000000000
--- a/src/main/java/tools/jackson/databind/deser/impl/FieldProperty.java
+++ /dev/null
@@ -1,227 +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
-{
-    private static final long serialVersionUID = 1L;
-
-    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 extends Annotation> A getAnnotation(Class<A> 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) {
-            // 15-Sep-2015, tatu: How could we get a ref to JsonParser?
-            _throwAsJacksonE(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) {
-            // 15-Sep-2015, tatu: How could we get a ref to JsonParser?
-            _throwAsJacksonE(e, value);
-        }
-        return instance;
-    }
-
-    /*
-    /**********************************************************
-    /* JDK serialization handling
-    /**********************************************************
-     */
-
-    Object readResolve() {
-        return new FieldProperty(this);
-    }
-}
\ No newline at end of file
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 ea285af212..7499ee447a 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;
@@ -15,30 +17,36 @@
 
 /**
  * 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
 {
     private static final long serialVersionUID = 1;
 
-    protected final AnnotatedMethod _annotated;
+    protected final AnnotatedMember _annotated;
 
     /**
-     * Setter method for modifying property value; used for
-     * "regular" method-accessible properties.
+     * Setter MethodHandle for modifying property value.
      */
-    protected final transient Method _setter;
+    protected final transient MethodHandle _setter;
+    /**
+     * Setter MetohdHandle for modifying property value and returning the modified bean.
+     */
+    protected final transient MethodHandle _setterReturn;
 
     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;
+        final MethodHandle setter = setterFor(annotated);
+        _setter = setter.asType(MethodType.methodType(void.class, Object.class, Object.class));
+        _setterReturn = setter.asType(MethodType.methodType(Object.class, Object.class, Object.class));
         _skipNulls = NullsConstantProvider.isSkipper(_nullProvider);
     }
 
@@ -47,6 +55,7 @@ protected MethodProperty(MethodProperty src, ValueDeserializer<?> deser,
         super(src, deser, nva);
         _annotated = src._annotated;
         _setter = src._setter;
+        _setterReturn = src._setterReturn;
         _skipNulls = NullsConstantProvider.isSkipper(nva);
     }
 
@@ -54,18 +63,35 @@ protected MethodProperty(MethodProperty src, PropertyName newName) {
         super(src, newName);
         _annotated = src._annotated;
         _setter = src._setter;
+        _setterReturn = src._setterReturn;
         _skipNulls = src._skipNulls;
     }
 
     /**
      * Constructor used for JDK Serialization when reading persisted object
      */
-    protected MethodProperty(MethodProperty src, Method m) {
+    protected MethodProperty(MethodProperty src, AnnotatedMember annotated) {
         super(src);
         _annotated = src._annotated;
-        _setter = m;
+        final MethodHandle setter = setterFor(annotated);
+        _setter = setter.asType(MethodType.methodType(void.class, Object.class, Object.class));
+        _setterReturn = setter.asType(MethodType.methodType(Object.class, Object.class, Object.class));
         _skipNulls = src._skipNulls;
     }
+    
+    private static MethodHandle setterFor(AnnotatedMember member) {
+        try {
+            if (member instanceof AnnotatedMethod) {
+                AnnotatedMethod am = (AnnotatedMethod) member;
+                return MethodHandles.lookup().unreflect(am.getAnnotated());
+            } else {
+                AnnotatedField af = (AnnotatedField) member;
+                return MethodHandles.lookup().unreflectSetter(af.getAnnotated());
+            }
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+    }
 
     @Override
     public SettableBeanProperty withName(PropertyName newName) {
@@ -135,8 +161,8 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
             value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
         }
         try {
-            _setter.invoke(instance, value);
-        } catch (Exception e) {
+            _setter.invokeExact(instance, value);
+        } catch (Throwable e) {
             _throwAsJacksonE(p, e, value);
         }
     }
@@ -164,9 +190,9 @@ public Object deserializeSetAndReturn(JsonParser p,
             value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
         }
         try {
-            Object result = _setter.invoke(instance, value);
+            Object result = _setterReturn.invokeExact(instance, value);
             return (result == null) ? instance : result;
-        } catch (Exception e) {
+        } catch (Throwable e) {
             _throwAsJacksonE(p, e, value);
             return null;
         }
@@ -182,8 +208,8 @@ public final void set(DeserializationContext ctxt, Object instance, Object value
             }
         }
         try {
-            _setter.invoke(instance, value);
-        } catch (Exception e) {
+            _setter.invokeExact(instance, value);
+        } catch (Throwable e) {
             // 15-Sep-2015, tatu: How could we get a ref to JsonParser?
             _throwAsJacksonE(e, value);
         }
@@ -199,9 +225,9 @@ public Object setAndReturn(DeserializationContext ctxt,
             }
         }
         try {
-            Object result = _setter.invoke(instance, value);
+            Object result = _setterReturn.invokeExact(instance, value);
             return (result == null) ? instance : result;
-        } catch (Exception e) {
+        } catch (Throwable e) {
             // 15-Sep-2015, tatu: How could we get a ref to JsonParser?
             _throwAsJacksonE(e, value);
             return null;
@@ -215,6 +241,6 @@ public Object setAndReturn(DeserializationContext ctxt,
      */
 
     Object readResolve() {
-        return new MethodProperty(this, _annotated.getAnnotated());
+        return new MethodProperty(this, _annotated);
     }
 }
diff --git a/src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java b/src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java
index 6279615545..48d8285d5c 100644
--- a/src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java
+++ b/src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java
@@ -66,24 +66,17 @@ Creators collect(Class<?> primaryMixIn)
          * 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 (_collectAnnotations && _defaultConstructor != null && _intr.hasIgnoreMarker(_config, _defaultConstructor)) {
+            _defaultConstructor = null;
         }
+
+        // 06-04-2025, scs: some constructors or factories might not be accessible due to
+        // module access rules, so filter them out now
+        constructors.removeIf(constructor ->
+                constructor == null || (_collectAnnotations && _intr.hasIgnoreMarker(_config, constructor)));
+        factories.removeIf(factory ->
+                factory == null || (_collectAnnotations && _intr.hasIgnoreMarker(_config, factory)));
+
         return new AnnotatedClass.Creators(_defaultConstructor, constructors, factories);
     }
 
@@ -190,6 +183,9 @@ private List<AnnotatedMethod> _findPotentialFactories(JavaType type,
             if (!_isIncludableFactoryMethod(m)) {
                 continue;
             }
+            if (!ClassUtil.checkAndFixAccess(m, false)) {
+                continue;
+            }
             // all factory methods are fine:
             //int argCount = m.getParameterTypes().length;
             if (candidates == null) {
@@ -348,14 +344,14 @@ protected AnnotatedMethod constructFactoryCreator(Method m,
     {
         final int paramCount = m.getParameterCount();
         if (!_collectAnnotations) { // when annotation processing is disabled
-            return new AnnotatedMethod(typeResCtxt, m, _emptyAnnotationMap(),
+            return AnnotatedMethod.of(typeResCtxt, m, _emptyAnnotationMap(),
                     _emptyAnnotationMaps(paramCount));
         }
         if (paramCount == 0) { // common enough we can slightly optimize
-            return new AnnotatedMethod(typeResCtxt, m, collectAnnotations(m, mixin),
+            return AnnotatedMethod.of(typeResCtxt, m, collectAnnotations(m, mixin),
                     NO_ANNOTATION_MAPS);
         }
-        return new AnnotatedMethod(typeResCtxt, m, collectAnnotations(m, mixin),
+        return AnnotatedMethod.of(typeResCtxt, m, collectAnnotations(m, mixin),
                 collectAnnotations(m.getParameterAnnotations(),
                         (mixin == null) ? null : mixin.getParameterAnnotations()));
     }
diff --git a/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java b/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java
index 26ca34c220..45c5dc3c3d 100644
--- a/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java
+++ b/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java
@@ -1,15 +1,27 @@
 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 static tools.jackson.databind.util.ClassUtil.sneakyThrow;
+
 public final class AnnotatedMethod
     extends AnnotatedWithParams
 {
     final protected transient Method _method;
+    final protected transient MethodHandle _invoker;
+    final protected transient MethodHandle _nullaryInvoker;
+    final protected transient MethodHandle _unaryConstructorInvoker;
 
     // // Simple lazy-caching:
 
@@ -21,19 +33,55 @@ public final class AnnotatedMethod
     /**********************************************************************
      */
 
-    public AnnotatedMethod(TypeResolutionContext ctxt, Method method,
-            AnnotationMap classAnn, AnnotationMap[] paramAnnotations)
+    private AnnotatedMethod(TypeResolutionContext ctxt, Method method,
+            AnnotationMap classAnn, AnnotationMap[] paramAnnotations, MethodHandles.Lookup lookup)
     {
         super(ctxt, classAnn, paramAnnotations);
         if (method == null) {
             throw new IllegalArgumentException("Cannot construct AnnotatedMethod with null Method");
         }
         _method = method;
+
+        MethodHandle invoker;
+        try {
+            invoker = lookup.unreflect(_method);
+        } catch (final IllegalAccessException e) {
+            invoker = MethodHandles.dropArguments(
+                    MethodHandles.throwException(Object.class, IllegalAccessException.class)
+                            .bindTo(e),
+                    0, Object.class);
+        }
+        _invoker = invoker;
+        boolean staticMethod = Modifier.isStatic(method.getModifiers());
+        int argCount = method.getParameterCount() + (staticMethod ? 0 : 1);
+        _nullaryInvoker = argCount > 0 ? null : invoker.asType(MethodType.methodType(Object.class));
+        _unaryConstructorInvoker = argCount != 1 ? null : invoker.asType(MethodType.methodType(Object.class, Object.class));
+    }
+
+    public static AnnotatedMethod of(TypeResolutionContext ctxt, Method method,
+            AnnotationMap classAnn, AnnotationMap[] paramAnnotations)
+    {
+        Objects.requireNonNull(method);
+        MethodHandles.Lookup lookup = MethodHandles.lookup();
+        if (!ClassUtil.isJDKClass(method.getDeclaringClass())) {
+            try {
+                lookup = MethodHandles.privateLookupIn(method.getDeclaringClass(), lookup);
+            } catch (final IllegalAccessException ign) {
+                lookup = lookup.in(method.getDeclaringClass());
+            }
+        } else {
+            lookup = lookup.in(method.getDeclaringClass());
+        }
+        if (lookup.lookupModes() == 0) {
+            // disallowed cross-module access
+            return null;
+        }
+        return new AnnotatedMethod(ctxt, method, classAnn, paramAnnotations, lookup);
     }
 
     @Override
     public AnnotatedMethod withAnnotations(AnnotationMap ann) {
-        return new AnnotatedMethod(_typeContext, _method, ann, _paramAnnotations);
+        return AnnotatedMethod.of(_typeContext, _method, ann, _paramAnnotations);
     }
 
     @Override
@@ -73,27 +121,56 @@ 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 _nullaryInvoker.invokeExact();
+        } catch (final Throwable e) {
+            throw sneakyThrow(e);
+        }
     }
 
     @Override
     public final Object call(Object[] args) throws Exception {
-        return _method.invoke(null, args);
+        try {
+            return _invoker.invokeWithArguments(args);
+        } catch (final Throwable e) {
+            throw sneakyThrow(e);
+        }
     }
 
     @Override
     public final Object call1(Object arg) throws Exception {
-        return _method.invoke(null, arg);
+        if (_unaryConstructorInvoker == null) {
+            throw new UnsupportedOperationException("method " + _method + " requires arg count != 1");
+        }
+        try {
+            Object ret = _unaryConstructorInvoker.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);
+        if (_nullaryInvoker == null) {
+            throw new UnsupportedOperationException("method " + _method + " requires args, but none provided");
+        }
+        try {
+            return _nullaryInvoker.invoke(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 = _invoker;
+            if (!Modifier.isStatic(_method.getModifiers())) {
+                invoker = invoker.bindTo(pojo);
+            }
+            return invoker.invokeWithArguments(args);
+        } catch (Throwable e) {
+            throw sneakyThrow(e);
+        }
     }
 
     /*
diff --git a/src/main/java/tools/jackson/databind/introspect/AnnotatedMethodCollector.java b/src/main/java/tools/jackson/databind/introspect/AnnotatedMethodCollector.java
index c16141c26a..94e2ae40f0 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;
@@ -110,6 +111,9 @@ private void _addMemberMethods(TypeResolutionContext tc,
             if (!_isIncludableMemberMethod(m)) {
                 continue;
             }
+            if (!ClassUtil.checkAndFixAccess(m, false)) {
+                continue;
+            }
             final MemberKey key = new MemberKey(m);
             MethodBuilder b = methods.get(key);
             if (b == null) {
@@ -124,14 +128,7 @@ private void _addMemberMethods(TypeResolutionContext tc,
                 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 +138,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<MemberKey,MethodBuilder> methods, Class<?> mixInCls)
     {
@@ -199,7 +235,7 @@ public AnnotatedMethod build() {
             }
             // 12-Apr-2017, tatu: Note that parameter annotations are NOT collected -- we could
             //   collect them if that'd make sense but...
-            return new AnnotatedMethod(typeContext, method, annotations.asAnnotationMap(), null);
+            return AnnotatedMethod.of(typeContext, method, annotations.asAnnotationMap(), null);
         }
     }
 }
diff --git a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java
index bdf67dd591..c08b93f0cd 100644
--- a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java
+++ b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java
@@ -1,6 +1,9 @@
 package tools.jackson.databind.ser;
 
 import java.lang.annotation.Annotation;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.HashMap;
@@ -24,6 +27,8 @@
 import tools.jackson.databind.util.ClassUtil;
 import tools.jackson.databind.util.NameTransformer;
 
+import static tools.jackson.databind.util.ClassUtil.sneakyThrow;
+
 /**
  * Base bean property handler class, which implements common parts of
  * reflection-based functionality for accessing a property value and serializing
@@ -113,15 +118,7 @@ public class BeanPropertyWriter
      * <p>
      * `transient` (and non-final) only to support JDK serializability.
      */
-    protected transient Method _accessorMethod;
-
-    /**
-     * Field that contains the property value for field-accessible properties.
-     * Null if and only if {@link #_accessorMethod} is null.
-     * <p>
-     * `transient` (and non-final) only to support JDK serializability.
-     */
-    protected transient Field _field;
+    protected transient MethodHandle _accessor;
 
     /*
     /**********************************************************************
@@ -218,17 +215,20 @@ 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;
+        try {
+            if (member instanceof AnnotatedField) {
+                ClassUtil.checkAndFixAccess(member.getMember(), false);
+                _accessor = MethodHandles.lookup().unreflectGetter((Field) member.getMember()).asType(MethodType.methodType(Object.class, Object.class));
+            } else if (member instanceof AnnotatedMethod) {
+                ClassUtil.checkAndFixAccess(member.getMember(), false);
+                _accessor = MethodHandles.lookup().unreflect((Method) member.getMember()).asType(MethodType.methodType(Object.class, Object.class));
+            } else {
+                // 01-Dec-2014, tatu: Used to be illegal, but now explicitly allowed
+                // for virtual props
+                _accessor = null;
+            }
+        } catch (ReflectiveOperationException e) {
+            throw sneakyThrow(e);
         }
         _suppressNulls = suppressNulls;
         _suppressableValue = suppressableValue;
@@ -258,8 +258,7 @@ protected BeanPropertyWriter() {
         _typeSerializer = null;
         _cfgSerializationType = null;
 
-        _accessorMethod = null;
-        _field = null;
+        _accessor = null;
         _suppressNulls = false;
         _suppressableValue = null;
 
@@ -286,8 +285,7 @@ protected BeanPropertyWriter(BeanPropertyWriter base, PropertyName name) {
         _declaredType = base._declaredType;
 
         _member = base._member;
-        _accessorMethod = base._accessorMethod;
-        _field = base._field;
+        _accessor = base._accessor;
 
         _serializer = base._serializer;
         _nullSerializer = base._nullSerializer;
@@ -313,8 +311,7 @@ protected BeanPropertyWriter(BeanPropertyWriter base, SerializedString name) {
         _member = base._member;
         _contextAnnotations = base._contextAnnotations;
         _declaredType = base._declaredType;
-        _accessorMethod = base._accessorMethod;
-        _field = base._field;
+        _accessor = base._accessor;
         _serializer = base._serializer;
         _nullSerializer = base._nullSerializer;
         if (base._internalSettings != null) {
@@ -420,17 +417,19 @@ public void fixAccess(SerializationConfig config) {
      * to just change Field/Method value directly.
      */
     Object readResolve() {
-        if (_member instanceof AnnotatedField) {
-            _accessorMethod = null;
-            _field = (Field) _member.getMember();
-        } else if (_member instanceof AnnotatedMethod) {
-            _accessorMethod = (Method) _member.getMember();
-            _field = null;
-        }
-        if (_serializer == null) {
-            _dynamicSerializers = PropertySerializerMap.emptyForProperties();
+        try {
+            if (_member instanceof AnnotatedField) {
+                _accessor = MethodHandles.lookup().unreflectGetter((Field) _member.getMember());
+            } else if (_member instanceof AnnotatedMethod) {
+                _accessor = MethodHandles.lookup().unreflect((Method) _member.getMember());
+            }
+            if (_serializer == null) {
+                _dynamicSerializers = PropertySerializerMap.emptyForProperties();
+            }
+            return this;
+        } catch (ReflectiveOperationException e) {
+            throw sneakyThrow(e);
         }
-        return this;
     }
 
     /*
@@ -610,8 +609,12 @@ public void serializeAsProperty(Object bean, JsonGenerator g, SerializationConte
         throws Exception
     {
         // inlined 'get()'
-        final Object value = (_accessorMethod == null) ? _field.get(bean)
-                : _accessorMethod.invoke(bean, (Object[]) null);
+        final Object value;
+        try {
+            value = _accessor.invokeExact(bean);
+        } catch (Throwable e) {
+            throw sneakyThrow(e);
+        }
 
         // Null handling is bit different, check that first
         if (value == null) {
@@ -685,8 +688,12 @@ public void serializeAsElement(Object bean, JsonGenerator g, SerializationContex
         throws Exception
     {
         // inlined 'get()'
-        final Object value = (_accessorMethod == null) ? _field.get(bean)
-                : _accessorMethod.invoke(bean, (Object[]) null);
+        final Object value;
+        try {
+            value = _accessor.invokeExact(bean);
+        } catch (Throwable e) {
+            throw sneakyThrow(e);
+        }
         if (value == null) { // nulls need specialized handling
             if (_nullSerializer != null) {
                 _nullSerializer.serialize(null, g, ctxt);
@@ -803,8 +810,11 @@ protected ValueSerializer<Object> _findAndAddDynamic(PropertySerializerMap map,
      * change the behavior
      */
     public final Object get(Object bean) throws Exception {
-        return (_accessorMethod == null) ? _field.get(bean) : _accessorMethod
-                .invoke(bean, (Object[]) null);
+        try {
+            return _accessor.invokeExact(bean);
+        } catch (Throwable e) {
+            throw sneakyThrow(e);
+        }
     }
 
     /**
@@ -854,13 +864,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");
         }
diff --git a/src/main/java/tools/jackson/databind/util/ClassUtil.java b/src/main/java/tools/jackson/databind/util/ClassUtil.java
index 604c5cb594..87cf85fbac 100644
--- a/src/main/java/tools/jackson/databind/util/ClassUtil.java
+++ b/src/main/java/tools/jackson/databind/util/ClassUtil.java
@@ -24,6 +24,13 @@ public final class ClassUtil
 
     private final static Iterator<Object> EMPTY_ITERATOR = Collections.emptyIterator();
 
+    private final static List<String> JDK_PREFIXES = Arrays.asList(
+            "java.",
+            "javax.",
+            "jdk.",
+            "sun.",
+            "com.sun.");
+
     /*
     /**********************************************************************
     /* Simple factory methods
@@ -361,6 +368,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 <E extends Throwable> 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
@@ -824,44 +839,39 @@ public static Class<?> primitiveType(Class<?> type)
      * @param evenIfAlreadyPublic Whether to always try to make accessor
      *   accessible, even if {@code public} (true),
      *   or only if needed to force by-pass of non-{@code public} access (false)
+ *   @return true if object is now accessible, false if not (currently only JDK types)
      */
-    public static void checkAndFixAccess(Member member, boolean evenIfAlreadyPublic)
+    public static boolean 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);
+                return true;
+            } else {
+                return isPublic;
             }
-        } 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 e) {
+            return false;
+        } 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 false;
             }
-            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 +982,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/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java b/src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
index a13dfe4a98..319fe524cf 100644
--- a/src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
+++ b/src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
@@ -2,6 +2,7 @@
 
 import java.util.*;
 
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 import com.fasterxml.jackson.annotation.*;
@@ -22,6 +23,7 @@ public UnmodifiableCollectionMixin(final Collection<?> collection) { }
     }
 
     @Test
+    @Disabled // XXX(scs) What to do about this test?
     public void testMixinWithUnmmodifiableCollection() throws Exception
     {
         ObjectMapper mapper = jsonMapperBuilder()


Reviews:
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T23:58:02Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T00:03:35Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T00:14:25Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T02:04:52Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T02:05:08Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T02:10:44Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T02:17:23Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T16:40:21Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T16:41:00Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T17:53:58Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T18:09:14Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T20:24:36Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T20:31:38Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T20:32:17Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T20:36:21Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T20:38:46Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T03:21:24Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T03:24:43Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T03:31:20Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T03:20:34Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T03:21:08Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T03:22:10Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T03:22:53Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T03:25:20Z
user: login: cowtowncoder
body: Looks pretty good, only minor final touches and I can merge it.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-04T03:33:45Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T23:32:08Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T23:40:46Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T01:21:01Z
user: login: pjfanning
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-10-16T14:13:19Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-10-16T15:10:00Z
user: login: stevenschlansker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-10-16T18:39:44Z
user: login: cowtowncoder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-10-17T19:36:59Z

Review comments:
diff_hunk: @@ -142,6 +142,11 @@
       <artifactId>junit-jupiter-api</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
path: pom.xml
user: login: stevenschlansker
body: I got some errors about `junit-jupiter-params` being used but not declared e.g. `EnumNamingStrategiesTest` - do we need a dependency here?
created_at: 2025-03-23T23:58:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,24 +66,15 @@ Creators collect(Class<?> primaryMixIn)
          * 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 (_collectAnnotations && _defaultConstructor != null && _intr.hasIgnoreMarker(_config, _defaultConstructor)) {
+            _defaultConstructor = null;
         }
+
+        constructors.removeIf(constructor ->
path: src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java
user: login: stevenschlansker
body: `removeIf` should perform better than `remove` in a loop - the `ArrayList` implementation collects a bitmask of elements to remove and then does it in a single copy, rather than repeated shifting
created_at: 2025-03-24T00:03:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,6 +23,7 @@ public UnmodifiableCollectionMixin(final Collection<?> collection) { }
     }
 
     @Test
+    @Disabled // XXX(scs) What to do about this test?
path: src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
user: login: stevenschlansker
body: This test is kinda crazy - it deserializes an `java.util.Collections$UnmodifiableCollection` which is not a public type, and the arguments don't even match the constructor signature... I'm not sure how it is supposed to work
created_at: 2025-03-24T00:14:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,24 +66,15 @@ Creators collect(Class<?> primaryMixIn)
          * 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 (_collectAnnotations && _defaultConstructor != null && _intr.hasIgnoreMarker(_config, _defaultConstructor)) {
+            _defaultConstructor = null;
         }
+
+        constructors.removeIf(constructor ->
path: src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java
user: login: cowtowncoder
body: Sounds good -- I can merge this separately?
created_at: 2025-03-24T02:04:52Z
author_association: MEMBER
diff_hunk: @@ -142,6 +142,11 @@
       <artifactId>junit-jupiter-api</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
path: pom.xml
user: login: cowtowncoder
body: Let me check and possible merge separately
created_at: 2025-03-24T02:05:08Z
author_association: MEMBER
diff_hunk: @@ -142,6 +142,11 @@
       <artifactId>junit-jupiter-api</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
path: pom.xml
user: login: cowtowncoder
body: Merged. Guessing it came as transitive dependency, but module-info.java declared requirement anyway.
created_at: 2025-03-24T02:10:44Z
author_association: MEMBER
diff_hunk: @@ -22,6 +23,7 @@ public UnmodifiableCollectionMixin(final Collection<?> collection) { }
     }
 
     @Test
+    @Disabled // XXX(scs) What to do about this test?
path: src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
user: login: cowtowncoder
body: Ability to access JDK internal types worked for JDK 8, but things are changing.

But as to this:

> arguments don't even match the constructor signature

I don't remember details but this test has been passing on JDKs up to 21.

Mix-in matching only consider number of parameters and type-erased types of parameters (trying to match what JVM/JDK do). How does actual Constructor differ when you see it?



created_at: 2025-03-24T02:17:23Z
author_association: MEMBER
diff_hunk: @@ -142,6 +142,11 @@
       <artifactId>junit-jupiter-api</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
path: pom.xml
user: login: stevenschlansker
body: Thanks!
created_at: 2025-03-24T16:40:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,24 +66,15 @@ Creators collect(Class<?> primaryMixIn)
          * 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 (_collectAnnotations && _defaultConstructor != null && _intr.hasIgnoreMarker(_config, _defaultConstructor)) {
+            _defaultConstructor = null;
         }
+
+        constructors.removeIf(constructor ->
path: src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java
user: login: stevenschlansker
body: Sounds good. There is a slight behavior change here too - since some methods are not accessible, even if we try to `setAccessible`, the `null` filter is now unconditional whether we are looking for `@JsonIgnore` or not.
created_at: 2025-03-24T16:41:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,6 +23,7 @@ public UnmodifiableCollectionMixin(final Collection<?> collection) { }
     }
 
     @Test
+    @Disabled // XXX(scs) What to do about this test?
path: src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
user: login: stevenschlansker
body: Well what surprised me I guess is that the constructor itself takes a `Collection` -- but I guess Jackson is smart enough to first allocate e.g. `ArrayList` for the elements, then forcibly create an `UnmodifiableCollection`.

I think in order to play by the rules, we'd need to somehow map `java.util.Collections$UnmodifiableCollection` and all its friends to their static factory methods like `Collections.unmodifiableCollection` rather than forcing open the constructor. I'm not sure how to do that other than to create a special rule that maps all the `Collections` factory methods. Probably same for `synchronized`. Do you have another idea or better suggestion?
created_at: 2025-03-24T17:53:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,6 +23,7 @@ public UnmodifiableCollectionMixin(final Collection<?> collection) { }
     }
 
     @Test
+    @Disabled // XXX(scs) What to do about this test?
path: src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
user: login: stevenschlansker
body: It's also weird because we end up writing the non-public class name `java.util.Collections$UnmodifiableCollection` to the wire, which is not a stable public name, and in theory could change release to release.

Do users really expect to serialize collections and re-construct the unmodifiable / synchronized wrappers? Seems like a very fragile expectation to me...
created_at: 2025-03-24T18:09:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,24 +66,15 @@ Creators collect(Class<?> primaryMixIn)
          * 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 (_collectAnnotations && _defaultConstructor != null && _intr.hasIgnoreMarker(_config, _defaultConstructor)) {
+            _defaultConstructor = null;
         }
+
+        constructors.removeIf(constructor ->
path: src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java
user: login: cowtowncoder
body: Also ignore my comment on merging, I misunderstood the context (should have read PR).

created_at: 2025-03-24T20:24:36Z
author_association: MEMBER
diff_hunk: @@ -22,6 +23,7 @@ public UnmodifiableCollectionMixin(final Collection<?> collection) { }
     }
 
     @Test
+    @Disabled // XXX(scs) What to do about this test?
path: src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
user: login: cowtowncoder
body: As the background, I _think_ this was to support use case that happened to be working, so gnarly implementation / test case was added to guard against regression.
I am not sure it is reasonable to expect class-name-based polymorphic handling to work seamlessly on modern JDKs -- I don't think we should have super-elaborate implementation.
(plus here we are relying on specific class name for case where I'm sure JDK is not required to keep it. But relying on class names for serialization is fragile no matter what I guess).



created_at: 2025-03-24T20:31:37Z
author_association: MEMBER
diff_hunk: @@ -22,6 +23,7 @@ public UnmodifiableCollectionMixin(final Collection<?> collection) { }
     }
 
     @Test
+    @Disabled // XXX(scs) What to do about this test?
path: src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
user: login: cowtowncoder
body: But, beyond this, whether users expect this -- I don't know. Some probably do. Do we need to try really hard to maintain such invariants. Probably not.
created_at: 2025-03-24T20:32:17Z
author_association: MEMBER
diff_hunk: @@ -22,6 +23,7 @@ public UnmodifiableCollectionMixin(final Collection<?> collection) { }
     }
 
     @Test
+    @Disabled // XXX(scs) What to do about this test?
path: src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
user: login: stevenschlansker
body: Ok, thanks for considering this. Personally I would vote that this is too weird of a use case to support - the mapping from the non-public collection class to its public static factory is not mechanical, we would need to maintain a mapping by hand or clever algorithm, and Jackson 3 is a good time to consider dropping some legacy features that might be ill-advised in the new world of "secure by default" Java.

JSON is not really intended to be Java serialization - which preserves the entire object graph - JSON really is "here are the elements of the collection", not, "use this specific private implementation of Java collections".

If a user comes back and complains that something reasonable does not work, we could pick it back up then. Does that sound good to you? Or would you have a different preferred outcome here?
created_at: 2025-03-24T20:36:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,6 +23,7 @@ public UnmodifiableCollectionMixin(final Collection<?> collection) { }
     }
 
     @Test
+    @Disabled // XXX(scs) What to do about this test?
path: src/test/java/tools/jackson/databind/mixins/MixinForCreators2795Test.java
user: login: cowtowncoder
body: I agree with that. While it would be nice in some ways to support all existing use cases, there must be some limit. And this, I think, falls beyond that limit. So let's leave it `@Disabled` and go with that.
created_at: 2025-03-24T20:38:46Z
author_association: MEMBER
diff_hunk: @@ -21,19 +33,57 @@ public final class AnnotatedMethod
     /**********************************************************************
      */
 
-    public AnnotatedMethod(TypeResolutionContext ctxt, Method method,
-            AnnotationMap classAnn, AnnotationMap[] paramAnnotations)
+    private AnnotatedMethod(TypeResolutionContext ctxt, Method method,
+            AnnotationMap classAnn, AnnotationMap[] paramAnnotations, MethodHandles.Lookup lookup)
     {
         super(ctxt, classAnn, paramAnnotations);
         if (method == null) {
             throw new IllegalArgumentException("Cannot construct AnnotatedMethod with null Method");
         }
         _method = method;
+
+        MethodHandle invoker;
+        try {
+            invoker = lookup.unreflect(_method);
+        } catch (final IllegalAccessException e) {
+            invoker = MethodHandles.dropArguments(
+                    MethodHandles.throwException(Object.class, IllegalAccessException.class)
+                            .bindTo(e),
+                    0, Object.class);
+        }
+        _invoker = invoker;
+        boolean staticMethod = Modifier.isStatic(method.getModifiers());
+        int argCount = method.getParameterCount() + (staticMethod ? 0 : 1);
+        _nullaryInvoker = argCount > 0 ? null : invoker.asType(MethodType.methodType(Object.class));
+        _unaryConstructorInvoker = argCount != 1 ? null : invoker.asType(MethodType.methodType(Object.class, Object.class));
+    }
+
+    public static AnnotatedMethod of(TypeResolutionContext ctxt, Method method,
+            AnnotationMap classAnn, AnnotationMap[] paramAnnotations)
+    {
+        if (method == null) {
path: src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java
user: login: cowtowncoder
body: Can just `Objects.requireNonNull()` since it's internal failure if called with null.
created_at: 2025-03-27T03:21:24Z
author_association: MEMBER
diff_hunk: @@ -73,27 +123,56 @@ 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 _nullaryInvoker.invokeExact();
+        } catch (final Throwable e) {
+            throw sneakyThrow(e);
path: src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java
user: login: cowtowncoder
body: I guess MethodHandle.invokeXxx() report throwing `Throwable`, but is there anything beyond `Exception` and `Error` (unchecked)?
created_at: 2025-03-27T03:24:43Z
author_association: MEMBER
diff_hunk: @@ -73,27 +123,56 @@ 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 _nullaryInvoker.invokeExact();
+        } catch (final Throwable e) {
+            throw sneakyThrow(e);
path: src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java
user: login: stevenschlansker
body: No, it just is passing through whatever is thrown from the called method - there should be no 'new' throwables here, all the e.g. access checks were done at MethodHandle creation time.
created_at: 2025-03-27T03:31:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -935,14 +938,13 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext
         // 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,
-                    beanDesc.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,
-                    beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
+        if (!ClassUtil.checkAndFixAccess(mutator.getMember(), ctxt.getConfig().isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS))) {
path: src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java
user: login: cowtowncoder
body: I think `ctxt.getConfig().isEnabled(...)` can be shortened to `ctxt.isEnabled(...)` (there should be convenience accessor).

But aside from that, could you add a brief comment explaining reasons why code short-circuits here?
(cannot access I assume but... is that then quietly swallowing cases causing surprises?)

created_at: 2025-04-04T03:20:33Z
author_association: MEMBER
diff_hunk: @@ -963,6 +965,11 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext
         return prop;
     }
 
+    private boolean finalField(AnnotatedMember am) {
path: src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java
user: login: cowtowncoder
body: naming: `finalField()` -> `isFinalField()`
created_at: 2025-04-04T03:21:07Z
author_association: MEMBER
diff_hunk: @@ -22,23 +24,26 @@ public final class MethodProperty
 {
path: src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java
user: login: cowtowncoder
body: Since this now handles Setters and Fields too, class Javadoc needs to be changed?
created_at: 2025-04-04T03:22:09Z
author_association: MEMBER
diff_hunk: @@ -22,23 +24,26 @@ public final class MethodProperty
 {
     private static final long serialVersionUID = 1;
 
-    protected final AnnotatedMethod _annotated;
+    protected final AnnotatedMember _annotated;
 
     /**
      * Setter method for modifying property value; used for
path: src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java
user: login: cowtowncoder
body: This needs changing too if used for Fields as well as Setter methods.
created_at: 2025-04-04T03:22:53Z
author_association: MEMBER
diff_hunk: @@ -66,24 +66,15 @@ Creators collect(Class<?> primaryMixIn)
          * 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 (_collectAnnotations && _defaultConstructor != null && _intr.hasIgnoreMarker(_config, _defaultConstructor)) {
+            _defaultConstructor = null;
         }
+
+        constructors.removeIf(constructor ->
+                constructor == null || (_collectAnnotations && _intr.hasIgnoreMarker(_config, constructor)));
path: src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java
user: login: cowtowncoder
body: Why is null check needed? It wasn't there earlier?
created_at: 2025-04-04T03:25:20Z
author_association: MEMBER
diff_hunk: @@ -66,24 +66,15 @@ Creators collect(Class<?> primaryMixIn)
          * 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 (_collectAnnotations && _defaultConstructor != null && _intr.hasIgnoreMarker(_config, _defaultConstructor)) {
+            _defaultConstructor = null;
         }
+
+        constructors.removeIf(constructor ->
+                constructor == null || (_collectAnnotations && _intr.hasIgnoreMarker(_config, constructor)));
path: src/main/java/tools/jackson/databind/introspect/AnnotatedCreatorCollector.java
user: login: stevenschlansker
body: It's now necessary because access checks have been pushed up from the point of use (when constructor is called) to earlier in deserializer creation (when constructors and factories are collected) so they are now filtered out earlier too. I added a comment.
created_at: 2025-04-06T23:32:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -935,14 +938,13 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext
         // 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,
-                    beanDesc.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,
-                    beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
+        if (!ClassUtil.checkAndFixAccess(mutator.getMember(), ctxt.getConfig().isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS))) {
path: src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java
user: login: stevenschlansker
body: Added a brief comment. I agree, some conditions regarding module visibility could quietly swallow members here, if we cannot call `setAccessible` on them. I think in those cases though the whole operation will fail anyway until you allow Jackson module to open your module with beans in it.

There might be some follow-up tweaking to this logic necessary. I got all the existing tests passing but I am sure there are new exciting corner cases to discover with module visibility.

Come to think of it, can we remove the `MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS` now? I believe with MethodHandles used, there should no longer be any performance benefit to calling `setAccessible` unconditionally. I am happy to file a follow-up PR if this is acceptable. If not, we could consider turning it off by default now.
created_at: 2025-04-06T23:40:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -935,14 +938,13 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext
         // 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,
-                    beanDesc.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,
-                    beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
+        if (!ClassUtil.checkAndFixAccess(mutator.getMember(), ctxt.getConfig().isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS))) {
path: src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java
user: login: cowtowncoder
body: Good idea -- I'll file an issue for changing defaults, will merge this PR and we'll go from there.

Actual removal of that feature is an option too, but at least start by changing its 
default.

**EDIT**: issue -> #5074
created_at: 2025-04-07T01:21:00Z
author_association: MEMBER
diff_hunk: @@ -935,14 +936,15 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext
         // 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,
-                    beanDesc.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,
-                    beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
+        // 06-04-2025, scs: we cannot always see members if e.g. they are in a different module that does not
path: src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java
user: login: pjfanning
body: Do we need to take a performance hit here? Why not let this fail if access is not setup? Null seems like a hack to me. The Java exceptions are fairly informative about the right add-opens are needed. @cowtowncoder wdyt? This looks like something we could do without. I only have a mobile to check this so the small screen view might not be the right way to check this PR.
created_at: 2025-10-16T14:13:19Z
author_association: MEMBER
diff_hunk: @@ -935,14 +936,15 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext
         // 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,
-                    beanDesc.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,
-                    beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
+        // 06-04-2025, scs: we cannot always see members if e.g. they are in a different module that does not
path: src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java
user: login: cowtowncoder
body: Not sure what performance hit you are thinking -- this is not perf sensitive path (it's during deserializer construction).

I agree it seems odd to return `null` but I don't remember reasoning over straight failure.
@stevenschlansker is author here.
One thing to note tho is that `ClassUtil.checkAndFixAccess` just blindly overrides access if I recall and does not attempt to determine if call would likely fail -- it is just based on configuration settings. That could probably be improved, but we cannot test call here (we are not deserializing, we are building deserializer) to make sure.

created_at: 2025-10-16T15:10:00Z
author_association: MEMBER
diff_hunk: @@ -935,14 +936,15 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext
         // 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,
-                    beanDesc.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,
-                    beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
+        // 06-04-2025, scs: we cannot always see members if e.g. they are in a different module that does not
path: src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java
user: login: stevenschlansker
body: From memory, the reason for this is because the access checks moved from "Method invoke time" to "MethodHandle create time" - this is one of the potential benefits of MH, lifting access / visibility checks from each-invocation to once-at-creation.

Some JDK classes in the modular world are not visible to user code - you're not allowed to crack open java.lang anymore, for example, without various `--add-opens` directives.

Jackson creates deserializers for types that are not used in some cases (I don't recall which) but basically this is skipping a failure in the case where we see some JDK type that we cannot crack open (Date IIRC?), but it does not matter, because the deserializer does not get used. So we skip an irrelevant failure.

I agree `null` is a bit weird. Improvements welcome, but the code is that way for a reason at least :) 
created_at: 2025-10-16T18:39:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -935,14 +936,15 @@ protected SettableBeanProperty constructSettableProperty(DeserializationContext
         // 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,
-                    beanDesc.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,
-                    beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
+        // 06-04-2025, scs: we cannot always see members if e.g. they are in a different module that does not
path: src/main/java/tools/jackson/databind/deser/BeanDeserializerFactory.java
user: login: cowtowncoder
body: Ok. @pjfanning if you wanted to try a PR with explicit fail, we could see if any of unit tests fail? (or just test locally).

@stevenschlansker Yeah I do not doubt there was (is) a reason (I vaguely recall discussions). But also do not remember context well enough to know the "right way" to go.


created_at: 2025-10-17T19:36:59Z
author_association: MEMBER

PR comments:
user: login: cowtowncoder
created_at: 2025-03-24T02:18:31Z
body: Also looks like Android compatibility is broken? (wrt animal-sniffer failure for JDK 17 build)
author_association: MEMBER
user: login: pjfanning
created_at: 2025-03-24T15:07:19Z
body: Java 17 compile issues
```
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java:614: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java:6[9](https://github.com/FasterXML/jackson-databind/actions/runs/14025726660/job/39263945347?pr=5046#step:6:10)3: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java:814: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/util/ClassUtil.java:859: Undefined reference: boolean java.lang.reflect.AccessibleObject.trySetAccessible()
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:161: Undefined reference: void java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:190: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:208: Undefined reference: void java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:225: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java:[12](https://github.com/FasterXML/jackson-databind/actions/runs/14025726660/job/39263945347?pr=5046#step:6:13)7: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact()
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java:148: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java:160: Undefined reference: Object java.lang.invoke.MethodHandle.invoke(Object)
Error:  Failed to execute goal org.codehaus.mojo:animal-sniffer-maven-plugin:1.24:check (default-cli) on project jackson-databind: Signature errors found. Verify them and ignore them with the proper annotation if needed. -> [Help 1]
Error:  
Error:  To see the full stack trace of the errors, re-run Maven with the -e switch.
Error:  Re-run Maven using the -X switch to enable full debug logging.
Error:  
Error:  For more information about the errors and possible solutions, please read the following articles:
Error:  [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
Error: Process completed with exit code 1.
```
author_association: MEMBER
user: login: iProdigy
created_at: 2025-03-24T15:30:03Z
body: > Java 17 compile issues

I believe those animal sniffer warnings are erroneous & can be suppressed: https://github.com/mojohaus/animal-sniffer/issues/67

also the min android sdk for MethodHandles is 26, which is already applied 
author_association: CONTRIBUTOR
user: login: pjfanning
created_at: 2025-03-24T15:32:49Z
body: > > Java 17 compile issues
> 
> I believe those animal sniffer warnings are erroneous & can be suppressed: [mojohaus/animal-sniffer#67](https://github.com/mojohaus/animal-sniffer/issues/67)
> 
> also the min android sdk for MethodHandles is 26, which is already applied

Can't we increase the min Android SDK?
author_association: MEMBER
user: login: iProdigy
created_at: 2025-03-24T15:38:26Z
body: > Can't we increase the min Android SDK?

To clarify, jackson 3 declares compatibility with sdk 34+ while 2.14 - 2.18 declares sdk 26+, both of which support MethodHandles - I don't follow your question

author_association: CONTRIBUTOR
user: login: pjfanning
created_at: 2025-03-24T15:45:09Z
body: I'd prefer to find out from animal-sniffer team why these methods are causing issues for it instead of just ignoring the issues that animal sniffer seems to think there are.


author_association: MEMBER
user: login: cowtowncoder
created_at: 2025-03-24T15:57:59Z
body: Maybe there are newer versions of Animal sniffer configs (dep version from pom.xml)?

author_association: MEMBER
user: login: yawkat
created_at: 2025-03-24T16:01:31Z
body: A new sniffer config is not sufficient, as PolymorphicSignature methods can have an unlimited number of signatures. It needs real plugin support.

I also don't think we can expect a response from the maintainers anytime soon, judging by that issue and the linked issues. Netty even removed the sniffer plugin because of it: https://github.com/netty/netty/pull/14032

I think the the only sensible path forward is to add an exclusion for MethodHandle for now
author_association: MEMBER
user: login: cowtowncoder
created_at: 2025-03-24T16:04:29Z
body: @yawkat Ok. I will update dependency just as routine maintenance (for 2.19 and master, both behind a bit), but not expecting this particular issue to be addressed.
It does sound like an override needed then.

See #5047.
author_association: MEMBER
user: login: stevenschlansker
created_at: 2025-03-24T16:37:10Z
body: I added an ignore for MethodHandle.
Now, the remaining AnimalSniffer problem is:
```
/home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/util/ClassUtil.java:859: Undefined reference: boolean java.lang.reflect.AccessibleObject.trySetAccessible()
```

I checked the definition and it seems that the method is part of the java spec since Java 9. Surely Android is past Java 9 baseline at this point - should I add another ignore for this one too?
author_association: CONTRIBUTOR
user: login: iProdigy
created_at: 2025-03-24T16:42:00Z
body: > Surely Android is past Java 9 baseline at this point - should I add another ignore for this one too?

android doesn't seem to implement that method on any SDK: https://developer.android.com/reference/java/lang/reflect/AccessibleObject

you'll have to switch to `setAccessible` (and catch the exception)
author_association: CONTRIBUTOR
user: login: stevenschlansker
created_at: 2025-03-24T16:45:58Z
body: Rats, that sucks. I'll make that update...
author_association: CONTRIBUTOR
user: login: stevenschlansker
created_at: 2025-03-24T17:08:57Z
body: Ok, I reworked the `ClassUtil.checkAndFixAccess` method. There is a behavior change - previously, if we could not `setAccessible` on a member, we would end up throwing an exception. Now, we just skip the property, since I kept running into things like random attempts to crack open e.g. `sun.util.calendar.ZoneInfo` which fails. LMK if this is a problem and we can discuss more.
author_association: CONTRIBUTOR
user: login: stevenschlansker
created_at: 2025-03-24T17:09:55Z
body: WTF, Android does not implement `InaccessibleObjectException` either?!
author_association: CONTRIBUTOR
user: login: cowtowncoder
created_at: 2025-03-24T20:28:25Z
body: > Ok, I reworked the `ClassUtil.checkAndFixAccess` method. There is a behavior change - previously, if we could not `setAccessible` on a member, we would end up throwing an exception. Now, we just skip the property, since I kept running into things like random attempts to crack open e.g. `sun.util.calendar.ZoneInfo` which fails. LMK if this is a problem and we can discuss more.

Hmm. Ok, that might be ok. Was thinking that maybe "sun.*" was not checked as system class by `ClassUtil.isJDKClass()` (and if so, could just add), but it is included.

I think it'd be good to try to avoid these calls to be sure (instead of having to catch exception) but that might be yet bigger undertaking.



author_association: MEMBER
user: login: stevenschlansker
created_at: 2025-03-24T20:38:29Z
body: > I think it'd be good to try to avoid these calls to be sure (instead of having to catch exception) but that might be yet bigger undertaking.

Yeah, that's why I tried to replace it with `trySetAccessible` - while it is still overriding visibility rules, at least it fails normally instead of requiring an exception thrown / catch.

The good news is this only happens during inspection, not at deserialization time, so the performance impact would be muted.
author_association: CONTRIBUTOR
user: login: cowtowncoder
created_at: 2025-03-26T02:05:22Z
body: @stevenschlansker Excellent work! I am bit hesitant to merge this before 3.0.0-rc2, but could be merged right after -- esp. assuming we could get some performance numbers, maybe for https://github.com/FasterXML/jackson-benchmarks/ (I'll see if I could run some myself).

**EDIT**: from quick test runs, it looks like there's maybe +5% for regular JSON read via databind for this PR vs 3.0.0-rc1. Hoping to test out writes too.
So maybe somewhat limited benefit at least for tested case.



author_association: MEMBER
user: login: yawkat
created_at: 2025-03-26T08:49:05Z
body: Maybe you can use a method handle to call trySetAccessible :smile: 
author_association: MEMBER
user: login: stevenschlansker
created_at: 2025-03-26T15:58:46Z
body: Thanks @cowtowncoder I am excited to finally get this work in :)
I rebased and squished the MR. Please let me know if there's any other tasks necessary pre-merge.

> I am bit hesitant to merge this before 3.0.0-rc2, but could be merged right after

That's fine by me. Were I making the call, I'd think that having the change in users' hands in an earlier RC for more testing would be better than delaying merging and finding problems closer to release - but again, 100% your call :)

> EDIT: from quick test runs, it looks like there's maybe +5% for regular JSON read via databind for this PR vs 3.0.0-rc1.
> Hoping to test out writes too.
> So maybe somewhat limited benefit at least for tested case.

I'm not too surprised. There was a time, a long time ago, when reflection was quite slow. The JVM has come a long way and now reflective calls are much, much faster than they used to be. That said, 5% is 5%, so I will take it :) especially since it comes with little downside.

> Maybe you can use a method handle to call trySetAccessible

@yawkat yes, that would work :) I think since this is called only during (de)serializer instantiation, though, it's not worth the effort for now.
author_association: CONTRIBUTOR
user: login: cowtowncoder
created_at: 2025-03-27T03:17:04Z
body: @stevenschlansker Yeah, I plan on having quite a few RCs out, so by "not next" just means 2-3 week delay. Sort of to isolate things. But I get the point of as-early-as-possible wrt weeding out bugs.

On performance, yeah, +5% is measurable and has some value.

But odd thing is this -- now testing serialization I see no difference whatsoever. That is... odd. Would suggest deserialization speed up would have something to do with setters (parameter) maybe?

Finally: one question on MethodHandles -- I noticed removal of some Field-based classes. I assume field access still exists; how is that now handled?
(I guess I should just spend time actually going through PR :) )
author_association: MEMBER
user: login: stevenschlansker
created_at: 2025-03-27T03:28:21Z
body: > Finally: one question on MethodHandles -- I noticed removal of some Field-based classes. I assume field access still exists; how is that now handled?

MethodHandles unify method calls and field get / set with [unreflectGetter](https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/invoke/MethodHandles.Lookup.html#unreflectGetter(java.lang.reflect.Field))
 and [unreflectSetter](https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/invoke/MethodHandles.Lookup.html#unreflectSetter(java.lang.reflect.Field)) which present field read / write as trivial getter and setter methods. So we just use that then treat everything as a method now.

author_association: CONTRIBUTOR
user: login: stevenschlansker
created_at: 2025-03-27T03:46:07Z
body: > But odd thing is this -- now testing serialization I see no difference whatsoever. That is... odd. Would suggest deserialization speed up would have something to do with setters (parameter) maybe?

It can be really hard to nail down performance on this kind of thing... my understanding from reading the literature is that it should help, but maybe hard to see among all the other work any particular workload does.

It's possible different shapes have different costs. For example when deserializing, `method.invoke(arg)` I think will always allocate an `Object[]` for varargs, whereas `methodHandle.invokeExact(arg)` should not need to do any varargs since it is a magic `@PolymorphicSignature` method
author_association: CONTRIBUTOR
user: login: cowtowncoder
created_at: 2025-04-07T01:38:31Z
body: @stevenschlansker Ok, looks like this also broke most downstream modules... :-(
Cascading builds at least gives that info in ~15 minutes after merge.

Will file issues, add a notes/refs here.

author_association: MEMBER
user: login: cowtowncoder
created_at: 2025-04-07T01:42:42Z
body: * Afterburner: https://github.com/FasterXML/jackson-modules-base/issues/294 
     * and it seems about all modules in this package :-(
author_association: MEMBER
user: login: cowtowncoder
created_at: 2025-04-07T01:58:17Z
body: Ugh. Seems like almost everything downstream fails, more or less catastrophically.

Will likely need to revert this PR later tonight.
author_association: MEMBER
user: login: stevenschlansker
created_at: 2025-04-07T04:11:48Z
body: :cry: 
author_association: CONTRIBUTOR
user: login: stevenschlansker
created_at: 2025-04-07T04:12:35Z
body: I'll try to find some time in the next week or two to check out the downstream modules and get those working.
I'm sure it's not trivial to set up, but it could be helpful to have a CI check that all of Jackson builds together.
author_association: CONTRIBUTOR
user: login: cowtowncoder
created_at: 2025-04-07T15:47:08Z
body: > I'll try to find some time in the next week or two to check out the downstream modules and get those working. I'm sure it's not trivial to set up, but it could be helpful to have a CI check that all of Jackson builds together.

I have been trying to think of that over time and haven't thought of good maintainable solution.
Cascading rebuild is the first and only improvement -- which at least tightens up feedback loop from days (a week or more, even) to less than an hour.

Resource-wise full rebuild job for all PRs just for databind might be doable, although not sure how heavy resource-wise it'd be wrt Github charging (FasterXML has paid, not free, account).



author_association: MEMBER
user: login: cowtowncoder
created_at: 2025-04-07T15:47:20Z
body: Will revert now.
author_association: MEMBER
user: login: cowtowncoder
created_at: 2025-04-07T16:07:00Z
body: Reverted for now. Although failure was wide, it is possible there might be just small number of actual issues so maybe focusing on solving problems by just one downstream repo (like `jackson-dataformat-xml`) would make sense.
author_association: MEMBER