Skip to content

[DE-770] refactoring JacksonUtils with dynamic proxy #539

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

Merged
merged 2 commits into from
Jan 30, 2024
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
124 changes: 124 additions & 0 deletions core/src/main/java/com/arangodb/internal/ShadedProxy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.arangodb.internal;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;

public class ShadedProxy {
private static final Logger LOG = LoggerFactory.getLogger(ShadedProxy.class);
private static final ClassLoader classLoader = ShadedProxy.class.getClassLoader();

@SuppressWarnings("unchecked")
public static <T> T of(Class<T> i, Object target) {
return (T) Proxy.newProxyInstance(
classLoader,
new Class[]{i},
new ShadedInvocationHandler(i, target));
}

public static Optional<Object> getTarget(Object o) {
if (Proxy.isProxyClass(o.getClass())) {
InvocationHandler h = Proxy.getInvocationHandler(o);
if (h instanceof ShadedInvocationHandler) {
return Optional.of(((ShadedInvocationHandler) h).target);
}
}
return Optional.empty();
}

private static class ShadedInvocationHandler implements InvocationHandler {
private final Map<ProxyMethod, Method> targetMethods = new HashMap<>();
private final Map<ProxyMethod, Class<?>> proxiedReturnTypes = new HashMap<>();
private final Object target;

ShadedInvocationHandler(Class<?> i, Object target) {
this.target = target;
Map<ProxyMethod, Method> iMethods = new HashMap<>();
for (Method method : i.getDeclaredMethods()) {
iMethods.put(new ProxyMethod(method), method);
}

Method[] methods;
if (target instanceof Class<?>) {
// proxy for static methods
methods = ((Class<?>) target).getMethods();
} else {
methods = target.getClass().getMethods();
}

for (Method method : methods) {
ProxyMethod pm = new ProxyMethod(method);
Method iMethod = iMethods.get(pm);
if (iMethod != null) {
LOG.trace("adding {}", iMethod);
targetMethods.put(pm, method);
Class<?> mRet = method.getReturnType();
Class<?> iRet = iMethod.getReturnType();
if (!mRet.equals(iRet)) {
LOG.trace("adding proxied return type {}", iRet);
proxiedReturnTypes.put(pm, iRet);
}
}
}
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
ProxyMethod pm = new ProxyMethod(method);
Method targetMethod = targetMethods.get(pm);
LOG.trace("Proxying invocation \n\t of: {} \n\t to: {}", method, targetMethod);
Class<?> returnProxy = proxiedReturnTypes.get(pm);
Object[] realArgs;
if (args == null) {
realArgs = null;
} else {
realArgs = new Object[args.length];
for (int i = 0; i < args.length; i++) {
realArgs[i] = ShadedProxy.getTarget(args[i]).orElse(args[i]);
}
}
Object res = targetMethod.invoke(target, realArgs);
if (returnProxy != null) {
LOG.trace("proxying return type \n\t of: {} \n\t to: {}", targetMethod.getReturnType(), returnProxy);
return ShadedProxy.of(returnProxy, res);
} else {
return res;
}
}

private static class ProxyMethod {
private final String name;
private final String simpleReturnType;
private final String[] simpleParameterTypes;

public ProxyMethod(Method method) {
name = method.getName();
simpleReturnType = method.getReturnType().getSimpleName();
simpleParameterTypes = new String[method.getParameterTypes().length];
for (int i = 0; i < method.getParameterTypes().length; i++) {
simpleParameterTypes[i] = method.getParameterTypes()[i].getSimpleName();
}
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProxyMethod that = (ProxyMethod) o;
return Objects.equals(name, that.name) && Objects.equals(simpleReturnType, that.simpleReturnType) && Arrays.equals(simpleParameterTypes, that.simpleParameterTypes);
}

@Override
public int hashCode() {
int result = Objects.hash(name, simpleReturnType);
result = 31 * result + Arrays.hashCode(simpleParameterTypes);
return result;
}
}
}

}
139 changes: 82 additions & 57 deletions core/src/main/java/com/arangodb/internal/serde/JacksonUtils.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,66 @@
package com.arangodb.internal.serde;

import com.arangodb.internal.ShadedProxy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public final class JacksonUtils {
private static final Logger LOG = LoggerFactory.getLogger(JacksonUtils.class);

private JacksonUtils() {
}

public interface Version {
int getMajorVersion();

int getMinorVersion();

String toString();
}

public interface StreamReadConstraints {

interface Static {
Builder builder();
}

interface Builder {
Builder maxNumberLength(final int maxNumLen);

Builder maxStringLength(int maxStringLen);

Builder maxNestingDepth(int maxNestingDepth);

Builder maxNameLength(int maxNameLen);

Builder maxDocumentLength(long maxDocLen);

StreamReadConstraints build();
}
}

public interface StreamWriteConstraints {
interface Static {
Builder builder();
}

interface Builder {
Builder maxNestingDepth(int maxNestingDepth);

StreamWriteConstraints build();
}
}

public interface JsonFactory {
Version version();

@SuppressWarnings("UnusedReturnValue")
JsonFactory setStreamReadConstraints(StreamReadConstraints src);

@SuppressWarnings("UnusedReturnValue")
JsonFactory setStreamWriteConstraints(StreamWriteConstraints swc);
}

/**
* Configure JsonFactory with permissive StreamReadConstraints and StreamWriteConstraints.
* It uses reflection to avoid compilation errors with older Jackson versions.
Expand All @@ -29,74 +77,51 @@ public static void tryConfigureJsonFactory(Object jf) {
}

private static void configureJsonFactory(Object jf) throws Exception {
// using reflection because these configuration are not supported in older Jackson versions
if (isAtLeastVersion(jf, 2, 15)) {
LOG.debug("Configuring StreamReadConstraints ...");
List<Invocation> readConf = new ArrayList<>();
readConf.add(new Invocation("maxNumberLength", int.class, Integer.MAX_VALUE));
readConf.add(new Invocation("maxStringLength", int.class, Integer.MAX_VALUE));
readConf.add(new Invocation("maxNestingDepth", int.class, Integer.MAX_VALUE));
if (isAtLeastVersion(jf, 2, 16)) {
readConf.add(new Invocation("maxNameLength", int.class, Integer.MAX_VALUE));
readConf.add(new Invocation("maxDocumentLength", long.class, Long.MAX_VALUE));
JsonFactory proxy = ShadedProxy.of(JsonFactory.class, jf);
Version version = proxy.version();
LOG.debug("Detected Jackson version: {}", version);

// get pkg name dynamically, to support shaded Jackson
String basePkg = jf.getClass().getPackage().getName();

if (isAtLeastVersion(version, 2, 15)) {
Class<?> srcClass = Class.forName(basePkg + "." + StreamReadConstraints.class.getSimpleName());
StreamReadConstraints.Builder builder = ShadedProxy.of(StreamReadConstraints.Static.class, srcClass)
.builder()
.maxNumberLength(Integer.MAX_VALUE)
.maxStringLength(Integer.MAX_VALUE)
.maxNestingDepth(Integer.MAX_VALUE);
if (isAtLeastVersion(version, 2, 16)) {
builder = builder
.maxNameLength(Integer.MAX_VALUE)
.maxDocumentLength(Long.MAX_VALUE);
} else {
LOG.debug("Skipping configuring StreamReadConstraints maxNameLength");
LOG.debug("Skipping configuring StreamReadConstraints maxDocumentLength");
}
configureStreamConstraints(jf, "StreamReadConstraints", readConf);
proxy.setStreamReadConstraints(builder.build());
} else {
LOG.debug("Skipping configuring StreamReadConstraints");
}

if (isAtLeastVersion(jf, 2, 16)) {
if (isAtLeastVersion(version, 2, 16)) {
LOG.debug("Configuring StreamWriteConstraints ...");
List<Invocation> writeConf = new ArrayList<>();
writeConf.add(new Invocation("maxNestingDepth", int.class, Integer.MAX_VALUE));
configureStreamConstraints(jf, "StreamWriteConstraints", writeConf);
Class<?> swcClass = Class.forName(basePkg + "." + StreamWriteConstraints.class.getSimpleName());
StreamWriteConstraints swc = ShadedProxy.of(StreamWriteConstraints.Static.class, swcClass)
.builder()
.maxNestingDepth(Integer.MAX_VALUE)
.build();
proxy.setStreamWriteConstraints(swc);
} else {
LOG.debug("Skipping configuring StreamWriteConstraints");
}
}

private static boolean isAtLeastVersion(Object jf, int major, int minor) throws Exception {
Class<?> packageVersionClass = Class.forName(jf.getClass().getPackage().getName() + ".json.PackageVersion");
Object version = packageVersionClass.getDeclaredField("VERSION").get(null);

Class<?> versionClass = Class.forName(jf.getClass().getPackage().getName() + ".Version");
int currentMajor = (int) versionClass.getDeclaredMethod("getMajorVersion").invoke(version);
int currentMinor = (int) versionClass.getDeclaredMethod("getMinorVersion").invoke(version);

LOG.debug("Detected Jackson version: {}.{}", currentMajor, currentMinor);

@SuppressWarnings("SameParameterValue")
private static boolean isAtLeastVersion(Version version, int major, int minor) {
int currentMajor = version.getMajorVersion();
int currentMinor = version.getMinorVersion();
return currentMajor > major || (currentMajor == major && currentMinor >= minor);
}

private static void configureStreamConstraints(Object jf, String className, List<Invocation> conf) throws Exception {
// get pkg name dynamically, to support shaded Jackson
String basePkg = jf.getClass().getPackage().getName();
Class<?> streamConstraintsClass = Class.forName(basePkg + "." + className);
Class<?> builderClass = Class.forName(basePkg + "." + className + "$Builder");
Method buildMethod = builderClass.getDeclaredMethod("build");
Method builderMethod = streamConstraintsClass.getDeclaredMethod("builder");
Object builder = builderMethod.invoke(null);
for (Invocation i : conf) {
Method method = builderClass.getDeclaredMethod(i.method, i.argType);
method.invoke(builder, i.arg);
}
Object streamReadConstraints = buildMethod.invoke(builder);
Method setStreamReadConstraintsMethod = jf.getClass().getDeclaredMethod("set" + className, streamConstraintsClass);
setStreamReadConstraintsMethod.invoke(jf, streamReadConstraints);
}

private static class Invocation {
final String method;
final Class<?> argType;
final Object arg;

Invocation(String method, Class<?> argType, Object arg) {
this.method = method;
this.argType = argType;
this.arg = arg;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Args=\
-H:ResourceConfigurationResources=${.}/resource-config.json,${.}/resource-config-spi.json \
-H:ReflectionConfigurationResources=${.}/reflect-config.json,${.}/reflect-config-serde.json,${.}/reflect-config-spi.json,${.}/reflect-config-mp-config.json \
-H:SerializationConfigurationResources=${.}/serialization-config.json \
-H:DynamicProxyConfigurationResources=${.}/proxy-config.json \
--initialize-at-build-time=\
org.slf4j \
--initialize-at-run-time=\
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"interfaces":["com.arangodb.internal.serde.JacksonUtils$JsonFactory"]
},
{
"interfaces":["com.arangodb.internal.serde.JacksonUtils$StreamReadConstraints"]
},
{
"interfaces":["com.arangodb.internal.serde.JacksonUtils$StreamReadConstraints$Builder"]
},
{
"interfaces":["com.arangodb.internal.serde.JacksonUtils$StreamReadConstraints$Static"]
},
{
"interfaces":["com.arangodb.internal.serde.JacksonUtils$StreamWriteConstraints"]
},
{
"interfaces":["com.arangodb.internal.serde.JacksonUtils$StreamWriteConstraints$Builder"]
},
{
"interfaces":["com.arangodb.internal.serde.JacksonUtils$StreamWriteConstraints$Static"]
},
{
"interfaces":["com.arangodb.internal.serde.JacksonUtils$Version"]
}
]
Loading