Skip to content

Commit

Permalink
Rewrite Internal Reflection Logic (Azure#36612)
Browse files Browse the repository at this point in the history
Rewrite Internal Reflection Logic
  • Loading branch information
alzimmermsft authored Sep 27, 2023
1 parent a2d1db2 commit 611ea33
Show file tree
Hide file tree
Showing 30 changed files with 1,026 additions and 652 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,9 @@
<!-- The casting problems do not exist in Azure use cases -->
<Match>
<Or>
<Class name="com.azure.core.implementation.MethodHandleReflectiveInvoker"/>
<Class name="com.azure.core.implementation.ReflectionUtilsMethodHandle"/>
<Class name="com.azure.core.implementation.TypeUtil"/>
<Class name="com.azure.core.implementation.ReflectionUtils"/>
</Or>
<Bug pattern="BC_UNCONFIRMED_CAST"/>
</Match>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package com.azure.core.serializer.json.gson.implementation;

import com.azure.core.implementation.ReflectiveInvoker;
import com.azure.core.implementation.ReflectionUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.json.JsonSerializable;
Expand All @@ -11,8 +12,6 @@
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;

/**
* Implementation of GSON's {@link TypeAdapter} that is capable of handling {@link JsonSerializable} types.
Expand All @@ -21,7 +20,7 @@ public class JsonSerializableTypeAdapter extends TypeAdapter<JsonSerializable<?>
private static final ClientLogger LOGGER = new ClientLogger(JsonSerializableTypeAdapter.class);

private final Class<? extends JsonSerializable<?>> jsonSerializableType;
private final MethodHandle readJson;
private final ReflectiveInvoker readJson;

/**
* Creates an instance of {@link JsonSerializableTypeAdapter}.
Expand All @@ -32,9 +31,8 @@ public class JsonSerializableTypeAdapter extends TypeAdapter<JsonSerializable<?>
public JsonSerializableTypeAdapter(Class<? extends JsonSerializable<?>> jsonSerializableType) {
this.jsonSerializableType = jsonSerializableType;
try {
MethodHandles.Lookup lookup = ReflectionUtils.getLookupToUse(jsonSerializableType);
this.readJson = lookup.unreflect(jsonSerializableType.getDeclaredMethod("fromJson",
com.azure.json.JsonReader.class));
this.readJson = ReflectionUtils.getMethodInvoker(jsonSerializableType,
jsonSerializableType.getDeclaredMethod("fromJson", com.azure.json.JsonReader.class));
} catch (Exception e) {
throw LOGGER.logExceptionAsError(new IllegalStateException(e));
}
Expand All @@ -49,13 +47,11 @@ public void write(JsonWriter out, JsonSerializable<?> value) throws IOException
public JsonSerializable<?> read(JsonReader in) throws IOException {
try {
return jsonSerializableType.cast(readJson.invokeWithArguments(new GsonJsonReader(in, null, true)));
} catch (Throwable e) {
if (e instanceof IOException) {
throw (IOException) e;
} else if (e instanceof Exception) {
throw new IOException(e);
} catch (Exception exception) {
if (exception instanceof IOException) {
throw (IOException) exception;
} else {
throw (Error) e;
throw new IOException(exception);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@

package com.azure.core.serializer.json.jackson.implementation;

import com.azure.core.implementation.ReflectiveInvoker;
import com.azure.core.implementation.ReflectionUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.core.util.logging.LogLevel;

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.security.PrivilegedAction;
import java.util.HashMap;
import java.util.Locale;
Expand All @@ -22,10 +20,10 @@
*/
final class HeaderCollectionHandler {
private static final int CACHE_SIZE_LIMIT = 10000;
private static final Map<Field, MethodHandle> FIELD_TO_SETTER_CACHE = new ConcurrentHashMap<>();
private static final Map<Field, ReflectiveInvoker> FIELD_TO_SETTER_INVOKER_CACHE = new ConcurrentHashMap<>();

// Dummy constant that indicates no setter was found for the Field.
private static final MethodHandle NO_SETTER_HANDLE = MethodHandles.identity(HeaderCollectionHandler.class);
private static final ReflectiveInvoker NO_SETTER_REFLECTIVE_INVOKER = ReflectionUtils.createNoOpInvoker();

private final String prefix;
private final int prefixLength;
Expand Down Expand Up @@ -86,24 +84,23 @@ private boolean usePublicSetter(Object deserializedHeaders, ClientLogger logger)
final String clazzSimpleName = clazz.getSimpleName();
final String fieldName = declaringField.getName();

MethodHandle setterHandler = getFromCache(declaringField, clazz, clazzSimpleName, fieldName, logger);
ReflectiveInvoker
setterReflectiveInvoker = getFromCache(declaringField, clazz, clazzSimpleName, fieldName, logger);

if (setterHandler == NO_SETTER_HANDLE) {
if (setterReflectiveInvoker == NO_SETTER_REFLECTIVE_INVOKER) {
return false;
}

try {
setterHandler.invokeWithArguments(deserializedHeaders, values);
logger.verbose("Set header collection {} on class {} using MethodHandle.", fieldName, clazzSimpleName);
setterReflectiveInvoker.invokeWithArguments(deserializedHeaders, values);
logger.log(LogLevel.VERBOSE, () ->
"Set header collection " + fieldName + " on class " + clazzSimpleName + " using reflection.");

return true;
} catch (Throwable ex) {
if (ex instanceof Error) {
throw (Error) ex;
}

logger.verbose("Failed to set header {} collection on class {} using MethodHandle.", fieldName,
clazzSimpleName, ex);
} catch (Exception ex) {
logger.log(LogLevel.VERBOSE, () ->
"Failed to set header " + fieldName + " collection on class " + clazzSimpleName + " using reflection.",
ex);
return false;
}
}
Expand All @@ -112,58 +109,27 @@ private static String getPotentialSetterName(String fieldName) {
return "set" + fieldName.substring(0, 1).toUpperCase(Locale.ROOT) + fieldName.substring(1);
}

private static MethodHandle getFromCache(Field key, Class<?> clazz, String clazzSimpleName,
private static ReflectiveInvoker getFromCache(Field key, Class<?> clazz, String clazzSimpleName,
String fieldName, ClientLogger logger) {
if (FIELD_TO_SETTER_CACHE.size() >= CACHE_SIZE_LIMIT) {
FIELD_TO_SETTER_CACHE.clear();
if (FIELD_TO_SETTER_INVOKER_CACHE.size() >= CACHE_SIZE_LIMIT) {
FIELD_TO_SETTER_INVOKER_CACHE.clear();
}

return FIELD_TO_SETTER_CACHE.computeIfAbsent(key, field -> {
MethodHandles.Lookup lookupToUse;
try {
lookupToUse = ReflectionUtils.getLookupToUse(clazz);
} catch (Exception ex) {
logger.verbose("Failed to retrieve MethodHandles.Lookup for field {}. Will attempt to make field accessible.", field, ex);

// In a previous implementation compute returned null here in an attempt to indicate that there is no
// setter for the field. Unfortunately, null isn't a valid indicator to computeIfAbsent that a
// computation has been performed and this cache would never effectively be a cache as compute would
// always be performed when there was no setter for the field.
//
// Now the implementation returns a dummy constant when there is no setter for the field. This now
// results in this case properly inserting into the cache and only running when a new type is seen or
// the cache is cleared due to reaching capacity.
return NO_SETTER_HANDLE;
}

return FIELD_TO_SETTER_INVOKER_CACHE.computeIfAbsent(key, field -> {
String setterName = getPotentialSetterName(fieldName);

try {
MethodHandle handle = lookupToUse.findVirtual(clazz, setterName,
MethodType.methodType(clazz, Map.class));
ReflectiveInvoker reflectiveInvoker = ReflectionUtils.getMethodInvoker(clazz, clazz.getDeclaredMethod(setterName,
Map.class));

logger.verbose("Using MethodHandle for setter {} on class {}.", setterName, clazzSimpleName);
logger.log(LogLevel.VERBOSE, () ->
"Using invoker for setter " + setterName + " on class " + clazzSimpleName + ".");

return handle;
} catch (ReflectiveOperationException ex) {
logger.verbose("Failed to retrieve MethodHandle for setter {} on class {}. "
+ "Will attempt to make field accessible. "
+ "Please consider adding public setter.", setterName,
clazzSimpleName, ex);
}

try {
Method setterMethod = clazz.getDeclaredMethod(setterName, Map.class);
MethodHandle handle = lookupToUse.unreflect(setterMethod);

logger.verbose("Using unreflected MethodHandle for setter {} on class {}.", setterName,
clazzSimpleName);

return handle;
} catch (ReflectiveOperationException ex) {
logger.verbose("Failed to unreflect MethodHandle for setter {} on class {}."
+ "Will attempt to make field accessible. "
+ "Please consider adding public setter.", setterName, clazzSimpleName, ex);
return reflectiveInvoker;
} catch (Exception ex) {
logger.log(LogLevel.VERBOSE, () ->
"Failed to retrieve invoker for setter " + setterName + " on class " + clazzSimpleName
+ ". Will attempt to make field accessible. Please consider adding public setter.", ex);
}

// In a previous implementation compute returned null here in an attempt to indicate that there is no setter
Expand All @@ -174,7 +140,7 @@ private static MethodHandle getFromCache(Field key, Class<?> clazz, String clazz
// Now the implementation returns a dummy constant when there is no setter for the field. This now results
// in this case properly inserting into the cache and only running when a new type is seen or the cache is
// cleared due to reaching capacity.
return NO_SETTER_HANDLE;
return NO_SETTER_REFLECTIVE_INVOKER;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@

package com.azure.core.serializer.json.jackson.implementation;

import com.azure.core.implementation.ReflectiveInvoker;
import com.azure.core.implementation.ReflectionUtils;
import com.azure.core.util.logging.ClientLogger;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;

/**
* Utility methods for Jackson Databind types when it's known that the version is 2.15+.
*/
Expand All @@ -18,39 +17,38 @@ final class JacksonDatabind215 {
private static final String STREAM_READ_CONSTRAINTS = "com.fasterxml.jackson.core.StreamReadConstraints";
private static final String STREAM_READ_CONSTRAINTS_BUILDER = STREAM_READ_CONSTRAINTS + "$Builder";

private static final MethodHandle CREATE_STREAM_READ_CONSTRAINTS_BUILDER;
private static final MethodHandle SET_MAX_STRING_LENGTH;
private static final MethodHandle BUILD_STREAM_READ_CONSTRAINTS;
private static final MethodHandle SET_STREAM_READ_CONSTRAINTS;
private static final ReflectiveInvoker CREATE_STREAM_READ_CONSTRAINTS_BUILDER;
private static final ReflectiveInvoker SET_MAX_STRING_LENGTH;
private static final ReflectiveInvoker BUILD_STREAM_READ_CONSTRAINTS;
private static final ReflectiveInvoker SET_STREAM_READ_CONSTRAINTS;

private static final boolean USE_JACKSON_215;

static {
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
ClassLoader thisClassLoader = JacksonDatabind215.class.getClassLoader();

MethodHandle createStreamReadConstraintsBuilder = null;
MethodHandle setMaxStringLength = null;
MethodHandle buildStreamReadConstraints = null;
MethodHandle setStreamReadConstraints = null;
ReflectiveInvoker createStreamReadConstraintsBuilder = null;
ReflectiveInvoker setMaxStringLength = null;
ReflectiveInvoker buildStreamReadConstraints = null;
ReflectiveInvoker setStreamReadConstraints = null;
boolean useJackson215 = false;
try {
Class<?> streamReadConstraints = Class.forName(STREAM_READ_CONSTRAINTS, true, thisClassLoader);
Class<?> streamReadConstraintsBuilder = Class.forName(STREAM_READ_CONSTRAINTS_BUILDER, true,
thisClassLoader);

createStreamReadConstraintsBuilder = publicLookup.unreflect(streamReadConstraints
.getDeclaredMethod("builder"));
setMaxStringLength = publicLookup.unreflect(streamReadConstraintsBuilder
.getDeclaredMethod("maxStringLength", int.class));
buildStreamReadConstraints = publicLookup.unreflect(streamReadConstraintsBuilder
.getDeclaredMethod("build"));
setStreamReadConstraints = publicLookup.unreflect(JsonFactory.class.getDeclaredMethod(
"setStreamReadConstraints", streamReadConstraints));
createStreamReadConstraintsBuilder = ReflectionUtils.getMethodInvoker(streamReadConstraints,
streamReadConstraints.getDeclaredMethod("builder"), false);
setMaxStringLength = ReflectionUtils.getMethodInvoker(streamReadConstraintsBuilder,
streamReadConstraintsBuilder.getDeclaredMethod("maxStringLength", int.class), false);
buildStreamReadConstraints = ReflectionUtils.getMethodInvoker(streamReadConstraintsBuilder,
streamReadConstraintsBuilder.getDeclaredMethod("build"), false);
setStreamReadConstraints = ReflectionUtils.getMethodInvoker(JsonFactory.class,
JsonFactory.class.getDeclaredMethod("setStreamReadConstraints", streamReadConstraints), false);
useJackson215 = true;
} catch (Throwable ex) {
if (ex instanceof LinkageError) {
LOGGER.info("Attempted to create MethodHandles for Jackson 2.15 features but failed. It's possible "
LOGGER.info("Attempted to create invokers for Jackson 2.15 features but failed. It's possible "
+ "that your application will run without error even with this failure. The Azure SDKs only set "
+ "updated StreamReadConstraints to allow for larger payloads to be handled.");
} else if (ex instanceof Error) {
Expand Down Expand Up @@ -79,18 +77,18 @@ static ObjectMapper mutateStreamReadConstraints(ObjectMapper objectMapper) {
}

try {
Object streamReadConstraintsBuilder = CREATE_STREAM_READ_CONSTRAINTS_BUILDER.invoke();
Object streamReadConstraintsBuilder = CREATE_STREAM_READ_CONSTRAINTS_BUILDER.invokeStatic();

SET_MAX_STRING_LENGTH.invoke(streamReadConstraintsBuilder, 50 * 1024 * 1024);
SET_STREAM_READ_CONSTRAINTS.invoke(objectMapper.tokenStreamFactory(),
BUILD_STREAM_READ_CONSTRAINTS.invoke(streamReadConstraintsBuilder));
SET_MAX_STRING_LENGTH.invokeWithArguments(streamReadConstraintsBuilder, 50 * 1024 * 1024);
SET_STREAM_READ_CONSTRAINTS.invokeWithArguments(objectMapper.tokenStreamFactory(),
BUILD_STREAM_READ_CONSTRAINTS.invokeWithArguments(streamReadConstraintsBuilder));

return objectMapper;
} catch (Throwable throwable) {
if (throwable instanceof Error) {
throw (Error) throwable;
} catch (Exception exception) {
if (exception instanceof RuntimeException) {
throw LOGGER.logExceptionAsError((RuntimeException) exception);
} else {
throw LOGGER.logExceptionAsError(new IllegalStateException(throwable));
throw LOGGER.logExceptionAsError(new IllegalStateException(exception));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package com.azure.core.serializer.json.jackson.implementation;

import com.azure.core.implementation.ReflectiveInvoker;
import com.azure.core.implementation.ReflectionUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.json.JsonReader;
Expand All @@ -12,14 +13,12 @@
import com.fasterxml.jackson.databind.JsonDeserializer;

import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;

public class JsonSerializableDeserializer extends JsonDeserializer<JsonSerializable<?>> {
private static final ClientLogger LOGGER = new ClientLogger(JsonSerializableDeserializer.class);

private final Class<? extends JsonSerializable<?>> jsonSerializableType;
private final MethodHandle readJson;
private final ReflectiveInvoker readJson;

/**
* Creates an instance of {@link JsonSerializableDeserializer}.
Expand All @@ -29,8 +28,8 @@ public class JsonSerializableDeserializer extends JsonDeserializer<JsonSerializa
public JsonSerializableDeserializer(Class<? extends JsonSerializable<?>> jsonSerializableType) {
this.jsonSerializableType = jsonSerializableType;
try {
MethodHandles.Lookup lookup = ReflectionUtils.getLookupToUse(jsonSerializableType);
this.readJson = lookup.unreflect(jsonSerializableType.getDeclaredMethod("fromJson", JsonReader.class));
this.readJson = ReflectionUtils.getMethodInvoker(jsonSerializableType,
jsonSerializableType.getDeclaredMethod("fromJson", JsonReader.class));
} catch (Exception e) {
throw LOGGER.logExceptionAsError(new IllegalStateException(e));
}
Expand All @@ -41,13 +40,11 @@ public JsonSerializable<?> deserialize(JsonParser p, DeserializationContext ctxt
try {
return jsonSerializableType.cast(readJson.invokeWithArguments(
new JacksonJsonReader(p, null, null, false, null)));
} catch (Throwable e) {
if (e instanceof IOException) {
throw (IOException) e;
} else if (e instanceof Exception) {
throw new IOException(e);
} catch (Exception exception) {
if (exception instanceof IOException) {
throw (IOException) exception;
} else {
throw (Error) e;
throw new IOException(exception);
}
}
}
Expand Down
Loading

0 comments on commit 611ea33

Please sign in to comment.