From f93180001397c2b3c8e26a45d0d90b16bc194e4f Mon Sep 17 00:00:00 2001 From: Alan Zimmer <48699787+alzimmermsft@users.noreply.github.com> Date: Wed, 17 Mar 2021 18:34:36 -0700 Subject: [PATCH] Use Reflection to Configure Jackson 2.12 Features (#19918) Use MethodHandles when using Jackson 2.12 Features to Prevent Errors when Jackson 2.11 is Resolved --- .../json/jackson/JacksonJsonSerializer.java | 82 ++++++++++++++++++- .../core/util/serializer/JacksonAdapter.java | 64 ++++++++++++++- 2 files changed, 138 insertions(+), 8 deletions(-) diff --git a/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializer.java b/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializer.java index fbb24d046e64b..eafd084a72cc2 100644 --- a/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializer.java +++ b/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializer.java @@ -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; @@ -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; @@ -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); + } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java index 5e35ec47345b8..e94274d9411cd 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java @@ -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; @@ -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; @@ -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); /** @@ -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()))