Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow (de)serializing records using Bean(De)SerializerModifier even when reflection is unavailable #3417

Merged
merged 4 commits into from
May 1, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
<artifactId>jackson-core</artifactId>
<version>${jackson.version.core}</version>
</dependency>
<dependency>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a brief comment referring to this PR, purpose.

<groupId>org.graalvm.sdk</groupId>
<artifactId>graal-sdk</artifactId>
<version>22.0.0.2</version>
<scope>provided</scope>
</dependency>

<!-- and for testing we need a few libraries
libs for which we use reflection for code, but direct dep for testing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,10 @@ protected Object deserializeFromObjectUsingNonDefault(JsonParser p,
return ctxt.handleMissingInstantiator(raw, null, p,
"non-static inner classes like this can only by instantiated using default, no-argument constructor");
}
if (NativeImageUtil.needsReflectionConfiguration(raw)) {
return ctxt.handleMissingInstantiator(raw, null, p,
"cannot deserialize from Object value (no delegate- or property-based Creator): this appears to be a native image, in which case you may need to configure reflection for the class that is to be deserialized");
}
return ctxt.handleMissingInstantiator(raw, getValueInstantiator(), p,
"cannot deserialize from Object value (no delegate- or property-based Creator)");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.fasterxml.jackson.databind.introspect;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

Expand Down Expand Up @@ -527,10 +529,10 @@ public RecordNaming(MapperConfig<?> config, AnnotatedClass forClass) {
// trickier: regular fields are ok (handled differently), but should
// we also allow getter discovery? For now let's do so
"get", "is", null);
_fieldNames = new HashSet<>();
for (String name : JDK14Util.getRecordFieldNames(forClass.getRawType())) {
_fieldNames.add(name);
}
String[] recordFieldNames = JDK14Util.getRecordFieldNames(forClass.getRawType());
_fieldNames = recordFieldNames == null ?
Collections.emptySet() :
new HashSet<>(Arrays.asList(recordFieldNames));
}

@Override
Expand Down
76 changes: 50 additions & 26 deletions src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.databind.util.NativeImageUtil;

/**
* Helper class to support some of JDK 14 (and later) features
Expand Down Expand Up @@ -76,6 +77,10 @@ public static RecordAccessor instance() {
public String[] getRecordFieldNames(Class<?> recordType) throws IllegalArgumentException
{
final Object[] components = recordComponents(recordType);
if (components == null) {
// not a record, or no reflective access on native image
return null;
}
final String[] names = new String[components.length];
for (int i = 0; i < components.length; i++) {
try {
Expand All @@ -92,6 +97,10 @@ public String[] getRecordFieldNames(Class<?> recordType) throws IllegalArgumentE
public RawTypeName[] getRecordFields(Class<?> recordType) throws IllegalArgumentException
{
final Object[] components = recordComponents(recordType);
if (components == null) {
// not a record, or no reflective access on native image
return null;
}
final RawTypeName[] results = new RawTypeName[components.length];
for (int i = 0; i < components.length; i++) {
String name;
Expand Down Expand Up @@ -120,10 +129,14 @@ protected Object[] recordComponents(Class<?> recordType) throws IllegalArgumentE
try {
return (Object[]) RECORD_GET_RECORD_COMPONENTS.invoke(recordType);
} catch (Exception e) {
if (NativeImageUtil.isUnsupportedFeatureError(e)) {
return null;
}
throw new IllegalArgumentException("Failed to access RecordComponents of type "
+ClassUtil.nameOf(recordType));
}
}

}

static class RawTypeName {
Expand Down Expand Up @@ -153,37 +166,43 @@ static class CreatorLocator {
_config = ctxt.getConfig();

_recordFields = RecordAccessor.instance().getRecordFields(beanDesc.getBeanClass());
final int argCount = _recordFields.length;

// And then locate the canonical constructor; must be found, if not, fail
// altogether (so we can figure out what went wrong)
AnnotatedConstructor primary = null;

// One special case: empty Records, empty constructor is separate case
if (argCount == 0) {
primary = beanDesc.findDefaultConstructor();
_constructors = Collections.singletonList(primary);
} else {
if (_recordFields == null) {
// not a record, or no reflective access on native image
_constructors = beanDesc.getConstructors();
main_loop:
for (AnnotatedConstructor ctor : _constructors) {
if (ctor.getParameterCount() != argCount) {
continue;
}
for (int i = 0; i < argCount; ++i) {
if (!ctor.getRawParameterType(i).equals(_recordFields[i].rawType)) {
continue main_loop;
_primaryConstructor = null;
} else {
final int argCount = _recordFields.length;

// And then locate the canonical constructor; must be found, if not, fail
// altogether (so we can figure out what went wrong)
AnnotatedConstructor primary = null;

// One special case: empty Records, empty constructor is separate case
if (argCount == 0) {
primary = beanDesc.findDefaultConstructor();
_constructors = Collections.singletonList(primary);
} else {
_constructors = beanDesc.getConstructors();
main_loop:
for (AnnotatedConstructor ctor : _constructors) {
if (ctor.getParameterCount() != argCount) {
continue;
}
for (int i = 0; i < argCount; ++i) {
if (!ctor.getRawParameterType(i).equals(_recordFields[i].rawType)) {
continue main_loop;
}
}
primary = ctor;
break;
}
primary = ctor;
break;
}
if (primary == null) {
throw new IllegalArgumentException("Failed to find the canonical Record constructor of type "
+ClassUtil.getTypeDescription(_beanDesc.getType()));
}
_primaryConstructor = primary;
}
if (primary == null) {
throw new IllegalArgumentException("Failed to find the canonical Record constructor of type "
+ClassUtil.getTypeDescription(_beanDesc.getType()));
}
_primaryConstructor = primary;
}

public AnnotatedConstructor locate(List<String> names)
Expand All @@ -205,6 +224,11 @@ public AnnotatedConstructor locate(List<String> names)
}
}

if (_recordFields == null) {
// not a record, or no reflective access on native image
return null;
}

// By now we have established that the canonical constructor is the one to use
// and just need to gather implicit names to return
for (RawTypeName field : _recordFields) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.databind.util.Converter;
import com.fasterxml.jackson.databind.util.IgnorePropertiesUtil;
import com.fasterxml.jackson.databind.util.NativeImageUtil;

/**
* Factory class that can provide serializers for any regular Java beans
Expand Down Expand Up @@ -476,7 +477,10 @@ protected JsonSerializer<Object> constructBeanOrAddOnSerializer(SerializerProvid
}
if (ser == null) { // Means that no properties were found
// 21-Aug-2020, tatu: Empty Records should be fine tho
if (type.isRecordType()) {
// 18-Mar-2022, yawkat: Record will also appear empty when missing reflection info.
// needsReflectionConfiguration will check that a constructor is present, else we fall back to the empty
// bean error msg
if (type.isRecordType() && !NativeImageUtil.needsReflectionConfiguration(type.getRawClass())) {
return builder.createDummy();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.ToEmptyObjectSerializer;
import com.fasterxml.jackson.databind.util.NativeImageUtil;

@SuppressWarnings("serial")
public class UnknownSerializer
Expand Down Expand Up @@ -43,8 +44,15 @@ public void serializeWithType(Object value, JsonGenerator gen, SerializerProvide

protected void failForEmpty(SerializerProvider prov, Object value)
throws JsonMappingException {
prov.reportBadDefinition(handledType(), String.format(
"No serializer found for class %s and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)",
value.getClass().getName()));
Class<?> cl = value.getClass();
if (NativeImageUtil.needsReflectionConfiguration(cl)) {
prov.reportBadDefinition(handledType(), String.format(
"No serializer found for class %s and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS). This appears to be a native image, in which case you may need to configure reflection for the class that is to be serialized",
cl.getName()));
} else {
prov.reportBadDefinition(handledType(), String.format(
"No serializer found for class %s and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)",
cl.getName()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.fasterxml.jackson.databind.util;

import org.graalvm.nativeimage.ImageInfo;

import java.lang.reflect.InvocationTargetException;

/**
* Utilities for graal native image support.
*/
public class NativeImageUtil {
private static final boolean RUNNING_IN_SVM;

static {
yawkat marked this conversation as resolved.
Show resolved Hide resolved
boolean runningInSvm;
try {
// check whether ImageInfo is available
ImageInfo.inImageCode();
runningInSvm = true;
} catch (NoClassDefFoundError ignored) {
runningInSvm = false;
}
RUNNING_IN_SVM = runningInSvm;
}

private NativeImageUtil() {
}

/**
* Check whether we're running in substratevm native image runtime mode. This check cannot be a constant, because
* the static initializer may run early during build time
*/
private static boolean isRunningInNativeImage() {
return RUNNING_IN_SVM && ImageInfo.inImageRuntimeCode();
}

/**
* Check whether the given error is a substratevm UnsupportedFeatureError
*/
public static boolean isUnsupportedFeatureError(Throwable e) {
if (!isRunningInNativeImage()) {
return false;
}
if (e instanceof InvocationTargetException) {
e = e.getCause();
}
return e.getClass().getName().equals("com.oracle.svm.core.jdk.UnsupportedFeatureError");
}

/**
* Check whether the given class is likely missing reflection configuration (running in native image, and no
* members visible in reflection).
*/
public static boolean needsReflectionConfiguration(Class<?> cl) {
if (!isRunningInNativeImage()) {
return false;
}
// records list their fields but not other members
return (cl.getDeclaredFields().length == 0 || ClassUtil.isRecordType(cl)) &&
cl.getDeclaredMethods().length == 0 &&
cl.getDeclaredConstructors().length == 0;
}
}