Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,8 @@ Fabian Lange (@CodingFabian)
Burak KALAYCI (@kalayciburak)
* Fixed #6175: Parse object-form `QName` after `As.PROPERTY` type id
[3.3.0]
* Fixed #6187: Make `InvokerHolder` access in `AnnotatedConstructor` lazy
[3.3.0]

@laech
* Reported #6178: `JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_VALUES` does not work
Expand Down
3 changes: 3 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ Versions: 3.x (for earlier see VERSION-2.x)
`java.time.Month`
(reported by @laech)
(fix by @Dongnyoung)
#6187: Make `InvokerHolder` access in `AnnotatedConstructor` lazy
(requested by @cowtowncoder)
(fix by @kalayciburak)
#6188: `ThrowableDeserializer` does not resolve standard `Throwable` property
names against `PropertyNamingStrategy`
(fix by @cowtowncoder, w/ Claude code)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package tools.jackson.databind.introspect;

import java.io.Serial;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Parameter;
Expand All @@ -12,17 +10,22 @@

import tools.jackson.databind.JavaType;
import tools.jackson.databind.util.ClassUtil;
import tools.jackson.databind.util.internal.UnreflectHandleSupplier;

import static java.lang.invoke.MethodType.methodType;

public final class AnnotatedConstructor
extends AnnotatedWithParams
{
protected final Constructor<?> _constructor;
private final InvokerHolder _invokerNullary = new InvokerHolder(methodType(Object.class));
private final InvokerHolder _invokerUnary = new InvokerHolder(methodType(Object.class, Object.class));
private final InvokerHolder _invokerFixedArity = new InvokerHolder(null);

/**
* Lazily resolved invocation handles, one per arity we support;
* {@code volatile} so a racy first use is safely published (duplicate
* resolution is harmless, handles are equivalent).
*/
protected volatile MethodHandle _invokerNullary;
protected volatile MethodHandle _invokerUnary;
protected volatile MethodHandle _invokerFixedArity;

// // Simple lazy-caching:

Expand Down Expand Up @@ -114,7 +117,7 @@ public Parameter[] getNativeParameters() {
@Override
public final Object call() throws Exception {
try {
return _invokerNullary.get().invokeExact();
return invokerNullary().invokeExact();
} catch (Throwable e) {
throw ClassUtil.sneakyThrow(e);
}
Expand All @@ -123,7 +126,7 @@ public final Object call() throws Exception {
@Override
public final Object call(Object[] args) throws Exception {
try {
return _invokerFixedArity.get().invokeWithArguments(args);
return invokerFixedArity().invokeWithArguments(args);
} catch (Throwable e) {
throw ClassUtil.sneakyThrow(e);
}
Expand All @@ -132,12 +135,47 @@ public final Object call(Object[] args) throws Exception {
@Override
public final Object call1(Object arg) throws Exception {
try {
return _invokerUnary.get().invokeExact(arg);
return invokerUnary().invokeExact(arg);
} catch (Throwable e) {
throw ClassUtil.sneakyThrow(e);
}
}

private MethodHandle invokerNullary() throws IllegalAccessException {
MethodHandle h = _invokerNullary;
if (h == null) {
h = unreflect().asType(methodType(Object.class));
_invokerNullary = h;
}
return h;
}

private MethodHandle invokerUnary() throws IllegalAccessException {
MethodHandle h = _invokerUnary;
if (h == null) {
h = unreflect().asType(methodType(Object.class, Object.class));
_invokerUnary = h;
}
return h;
}

private MethodHandle invokerFixedArity() throws IllegalAccessException {
MethodHandle h = _invokerFixedArity;
if (h == null) {
h = unreflect().asFixedArity();
_invokerFixedArity = h;
}
return h;
}

/**
* Note: caller is expected to have called {@code ClassUtil.checkAndFixAccess()}
* already; access checks are suppressed for an accessible {@link Constructor}.
*/
private MethodHandle unreflect() throws IllegalAccessException {
return MethodHandles.lookup().unreflectConstructor(_constructor);
}

/*
/**********************************************************************
/* AnnotatedMember impl
Expand Down Expand Up @@ -195,18 +233,4 @@ public boolean equals(Object o) {
AnnotatedConstructor other = (AnnotatedConstructor) o;
return Objects.equals(_constructor, other._constructor);
}

class InvokerHolder extends UnreflectHandleSupplier {
@Serial
private static final long serialVersionUID = 1L;

InvokerHolder(MethodType asType) {
super(asType);
}

@Override
protected MethodHandle unreflect() throws IllegalAccessException {
return MethodHandles.lookup().unreflectConstructor(_constructor);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@
import tools.jackson.databind.util.ClassUtil;

/**
* Lazy memoized holder for MethodHandles.
* Defers binding of the method handle until after access checks are suppressed
* (which happens in a virtual method call after construction) and avoids serialization of
* MethodHandle.
* Lazy, memoized holder for a {@link MethodHandle} unreflected from a
* {@link java.lang.reflect.Member}.
*<p>
* Resolution is deferred until first actual use, for two reasons:
*<ul>
* <li>Access to non-public members is only enabled by
* {@code ClassUtil.checkAndFixAccess()}, which callers apply via
* {@code AnnotatedMember.fixAccess()} <i>after</i> the owning member has been
* constructed. Unreflecting during construction could hence fail.
* </li>
* <li>Many members are introspected but never actually invoked, so the handle
* lookup is often avoidable altogether.
* </li>
* </ul>
* The resolved handle is adapted once -- to the {@link MethodType} given to the
* constructor, or {@link MethodHandle#asFixedArity()} if that is {@code null} --
* and then memoized; resolution happens at most once per holder.
*/
public abstract class UnreflectHandleSupplier implements Supplier<MethodHandle> {
private final MethodType asType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;

Expand All @@ -29,6 +30,10 @@ public void setValue(String value) {
}
}

static class NoArgBean {
public NoArgBean() { }
}

private final ObjectMapper MAPPER = newJsonMapper();

// [databind#3187]
Expand Down Expand Up @@ -64,6 +69,60 @@ public void testAnnotatedConstructorRawParameterTypeIsCached() {
assertSame(paramTypes, constructor._paramClasses);
}

// [databind#6187]
@Test
public void annotatedConstructorDoesNotEagerlyConstructInvokers() {
DeserializationConfig context = MAPPER.deserializationConfig();
JavaType beanType = MAPPER.constructType(SomeBean.class);

AnnotatedClass instance = AnnotatedClassResolver.resolve(context, beanType, context);
AnnotatedConstructor constructor = instance.getConstructors().get(0);

assertNull(constructor._invokerNullary);
assertNull(constructor._invokerUnary);
assertNull(constructor._invokerFixedArity);
}

// [databind#6187]
@Test
public void annotatedConstructorBuildsOnlyTheInvokerItUses() throws Exception {
DeserializationConfig context = MAPPER.deserializationConfig();
JavaType beanType = MAPPER.constructType(SomeBean.class);

AnnotatedClass instance = AnnotatedClassResolver.resolve(context, beanType, context);
AnnotatedConstructor constructor = instance.getConstructors().get(0);

SomeBean created = (SomeBean) constructor.call(new Object[] { "x" });
assertEquals("x", created.getValue());
assertNull(constructor._invokerNullary);
assertNull(constructor._invokerUnary);
assertNotNull(constructor._invokerFixedArity);

SomeBean viaCall1 = (SomeBean) constructor.call1("y");
assertEquals("y", viaCall1.getValue());
assertNull(constructor._invokerNullary);
assertNotNull(constructor._invokerUnary);
assertNotNull(constructor._invokerFixedArity);
}

// [databind#6187]
@Test
public void annotatedConstructorNullaryCallBuildsOnlyNullaryInvoker() throws Exception {
DeserializationConfig context = MAPPER.deserializationConfig();
JavaType beanType = MAPPER.constructType(NoArgBean.class);

AnnotatedClass instance = AnnotatedClassResolver.resolve(context, beanType, context);
AnnotatedConstructor constructor = instance.getDefaultConstructor();
assertNotNull(constructor);

assertNull(constructor._invokerNullary);
Object created = constructor.call();
assertEquals(NoArgBean.class, created.getClass());
assertNotNull(constructor._invokerNullary);
assertNull(constructor._invokerUnary);
assertNull(constructor._invokerFixedArity);
}

// [databind#3187]
@Test
public void testAnnotatedMethodEquality() {
Expand Down
Loading