Skip to content

Commit

Permalink
Use Reflection to Configure Jackson 2.12 Features (Azure#19918)
Browse files Browse the repository at this point in the history
Use MethodHandles when using Jackson 2.12 Features to Prevent Errors when Jackson 2.11 is Resolved
  • Loading branch information
alzimmermsft committed Mar 18, 2021
1 parent cd30e0c commit f931800
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AccessorNamingStrategy;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.BeanUtil;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
Expand All @@ -34,6 +37,49 @@
* Jackson based implementation of the {@link JsonSerializer} and {@link MemberNameConverter} interfaces.
*/
public final class JacksonJsonSerializer implements JsonSerializer, MemberNameConverter {
private static final String ACCESSOR_NAMING_STRATEGY =
"com.fasterxml.jackson.databind.introspect.AccessorNamingStrategy";
private static final String ACCESSOR_NAMING_STRATEGY_PROVIDER = ACCESSOR_NAMING_STRATEGY + ".Provider";
private static final MethodHandle GET_ACCESSOR_NAMING;
private static final MethodHandle FOR_POJO;
private static final MethodHandle FIND_NAME_FOR_IS_GETTER;
private static final MethodHandle FIND_NAME_FOR_REGULAR_GETTER;
private static final boolean USE_REFLECTION_FOR_MEMBER_NAME;

static {
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

MethodHandle getAccessorNaming = null;
MethodHandle forPojo = null;
MethodHandle findNameForIsGetter = null;
MethodHandle findNameForRegularGetter = null;
boolean useReflectionForMemberName = false;

try {
Class<?> accessorNamingStrategyProviderClass = Class.forName(ACCESSOR_NAMING_STRATEGY_PROVIDER);
Class<?> accessorNamingStrategyClass = Class.forName(ACCESSOR_NAMING_STRATEGY);
getAccessorNaming = publicLookup.findVirtual(MapperConfig.class, "getAccessorNaming",
MethodType.methodType(accessorNamingStrategyProviderClass));
forPojo = publicLookup.findVirtual(accessorNamingStrategyProviderClass, "forPOJO",
MethodType.methodType(accessorNamingStrategyClass, MapperConfig.class, AnnotatedClass.class));
findNameForIsGetter = publicLookup.findVirtual(accessorNamingStrategyClass, "findNameForIsGetter",
MethodType.methodType(String.class, AnnotatedMethod.class, String.class));
findNameForRegularGetter = publicLookup.findVirtual(accessorNamingStrategyClass, "findNameForRegularGetter",
MethodType.methodType(String.class, AnnotatedMethod.class, String.class));
useReflectionForMemberName = true;
} catch (Throwable ex) {
new ClientLogger(JacksonJsonSerializer.class)
.verbose("Failed to retrieve MethodHandles used to get naming strategy. Falling back to BeanUtils.",
ex);
}

GET_ACCESSOR_NAMING = getAccessorNaming;
FOR_POJO = forPojo;
FIND_NAME_FOR_IS_GETTER = findNameForIsGetter;
FIND_NAME_FOR_REGULAR_GETTER = findNameForRegularGetter;
USE_REFLECTION_FOR_MEMBER_NAME = useReflectionForMemberName;
}

private final ClientLogger logger = new ClientLogger(JacksonJsonSerializer.class);

private final ObjectMapper mapper;
Expand Down Expand Up @@ -195,14 +241,42 @@ private String removePrefix(Method method) {

AnnotatedClass annotatedClass = AnnotatedClassResolver.resolve(config,
mapper.constructType(method.getDeclaringClass()), null);
AccessorNamingStrategy accessorNamingStrategy = config.getAccessorNaming().forPOJO(config, annotatedClass);

AnnotatedMethod annotatedMethod = new AnnotatedMethod(null, method, null, null);
String name = accessorNamingStrategy.findNameForIsGetter(annotatedMethod, annotatedMethod.getName());
String annotatedMethodName = annotatedMethod.getName();

String name = null;
if (USE_REFLECTION_FOR_MEMBER_NAME) {
name = removePrefixWithReflection(config, annotatedClass, annotatedMethod, annotatedMethodName, logger);
}

if (name == null) {
name = accessorNamingStrategy.findNameForRegularGetter(annotatedMethod, annotatedMethod.getName());
name = removePrefixWithBeanUtils(annotatedMethod);
}

return name;
}

private static String removePrefixWithReflection(MapperConfig<?> config, AnnotatedClass annotatedClass,
AnnotatedMethod method, String methodName, ClientLogger logger) {
try {
Object accessorNamingStrategy = FOR_POJO.invoke(GET_ACCESSOR_NAMING.invoke(config), config, annotatedClass);


String name = (String) FIND_NAME_FOR_IS_GETTER.invoke(accessorNamingStrategy, method, methodName);
if (name == null) {
name = (String) FIND_NAME_FOR_REGULAR_GETTER.invoke(accessorNamingStrategy, method, methodName);
}

return name;
} catch (Throwable ex) {
logger.verbose("Failed to find member name with AccessorNamingStrategy, returning null.", ex);
return null;
}
}

@SuppressWarnings("deprecation")
private static String removePrefixWithBeanUtils(AnnotatedMethod annotatedMethod) {
return BeanUtil.okNameForGetter(annotatedMethod, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
import com.fasterxml.jackson.databind.cfg.MapperBuilder;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
Expand All @@ -34,6 +32,9 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
Expand All @@ -59,6 +60,52 @@
public class JacksonAdapter implements SerializerAdapter {
private static final Pattern PATTERN = Pattern.compile("^\"*|\"*$");

private static final String MUTABLE_COERCION_CONFIG = "com.fasterxml.jackson.databind.cfg.MutableCoercionConfig";
private static final String COERCION_INPUT_SHAPE = "com.fasterxml.jackson.databind.cfg.CoercionInputShape";
private static final String COERCION_ACTION = "com.fasterxml.jackson.databind.cfg.CoercionAction";

private static final MethodHandle COERCION_CONFIG_DEFAULTS;
private static final MethodHandle SET_COERCION;
private static final Object COERCION_INPUT_SHAPE_EMPTY_STRING;
private static final Object COERCION_ACTION_AS_NULL;
private static final boolean USE_REFLECTION_TO_SET_COERCION;

static {
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

MethodHandle coercionConfigDefaults = null;
MethodHandle setCoercion = null;
Object coercionInputShapeEmptyString = null;
Object coercionActionAsNull = null;
boolean useReflectionToSetCoercion = false;

try {
Class<?> mutableCoercionConfig = Class.forName(MUTABLE_COERCION_CONFIG);
Class<?> coercionInputShapeClass = Class.forName(COERCION_INPUT_SHAPE);
Class<?> coercionActionClass = Class.forName(COERCION_ACTION);

coercionConfigDefaults = publicLookup.findVirtual(ObjectMapper.class, "coercionConfigDefaults",
MethodType.methodType(mutableCoercionConfig));
setCoercion = publicLookup.findVirtual(mutableCoercionConfig, "setCoercion",
MethodType.methodType(mutableCoercionConfig, coercionInputShapeClass, coercionActionClass));
coercionInputShapeEmptyString = publicLookup.findStaticGetter(coercionInputShapeClass, "EmptyString",
coercionInputShapeClass).invoke();
coercionActionAsNull = publicLookup.findStaticGetter(coercionActionClass, "AsNull", coercionActionClass)
.invoke();
useReflectionToSetCoercion = true;
} catch (Throwable ex) {
new ClientLogger(JacksonAdapter.class)
.verbose("Failed to retrieve MethodHandles used to set coercion configurations. "
+ "Setting coercion configurations will be skipped.", ex);
}

COERCION_CONFIG_DEFAULTS = coercionConfigDefaults;
SET_COERCION = setCoercion;
COERCION_INPUT_SHAPE_EMPTY_STRING = coercionInputShapeEmptyString;
COERCION_ACTION_AS_NULL = coercionActionAsNull;
USE_REFLECTION_TO_SET_COERCION = useReflectionToSetCoercion;
}

private final ClientLogger logger = new ClientLogger(JacksonAdapter.class);

/**
Expand Down Expand Up @@ -103,8 +150,17 @@ public JacksonAdapter() {
.enable(FromXmlParser.Feature.EMPTY_ELEMENT_AS_NULL)
.build();

this.xmlMapper.coercionConfigDefaults()
.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);

if (USE_REFLECTION_TO_SET_COERCION) {
try {
Object object = COERCION_CONFIG_DEFAULTS.invoke(this.xmlMapper);
SET_COERCION.invoke(object, COERCION_INPUT_SHAPE_EMPTY_STRING, COERCION_ACTION_AS_NULL);
} catch (Throwable e) {
logger.verbose("Failed to set coercion actions.", e);
}
} else {
logger.verbose("Didn't set coercion defaults as it wasn't found on the classpath.");
}

ObjectMapper flatteningMapper = initializeMapperBuilder(JsonMapper.builder())
.addModule(FlatteningSerializer.getModule(simpleMapper()))
Expand Down

0 comments on commit f931800

Please sign in to comment.