Optimise MemoizingProxy
Allow re-use of proxy class instead of re-creating it every time.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
|
||||
<project xmlns="https://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>commons-util</artifactId>
|
||||
@@ -49,13 +50,11 @@
|
||||
<artifactId>reactor-core</artifactId>
|
||||
<version>${reactor-version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cglib</groupId>
|
||||
<artifactId>cglib</artifactId>
|
||||
<version>${cglib-version}</version>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- HTM -> Markdown converter -->
|
||||
<dependency>
|
||||
<groupId>com.kotcrab.remark</groupId>
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.util;
|
||||
|
||||
import static net.bytebuddy.matcher.ElementMatchers.*;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -19,9 +21,22 @@ import java.util.concurrent.TimeUnit;
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
|
||||
import net.sf.cglib.proxy.Enhancer;
|
||||
import net.sf.cglib.proxy.MethodInterceptor;
|
||||
import net.sf.cglib.proxy.MethodProxy;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.description.method.MethodDescription;
|
||||
import net.bytebuddy.description.modifier.Visibility;
|
||||
import net.bytebuddy.dynamic.DynamicType;
|
||||
import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
|
||||
import net.bytebuddy.implementation.MethodCall;
|
||||
import net.bytebuddy.implementation.MethodDelegation;
|
||||
import net.bytebuddy.implementation.MethodDelegation.WithCustomProperties;
|
||||
import net.bytebuddy.implementation.bind.annotation.AllArguments;
|
||||
import net.bytebuddy.implementation.bind.annotation.FieldProxy;
|
||||
import net.bytebuddy.implementation.bind.annotation.FieldValue;
|
||||
import net.bytebuddy.implementation.bind.annotation.Origin;
|
||||
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
|
||||
import net.bytebuddy.implementation.bind.annotation.SuperCall;
|
||||
import net.bytebuddy.jar.asm.Opcodes;
|
||||
import net.bytebuddy.matcher.ElementMatcher.Junction;
|
||||
|
||||
/**
|
||||
* Utility to instrument a given class, memoizing all it's zero-argument method invocations.
|
||||
@@ -29,7 +44,53 @@ import net.sf.cglib.proxy.MethodProxy;
|
||||
* it caches exception results as well as regularly returned values.
|
||||
*/
|
||||
public class MemoizingProxy {
|
||||
|
||||
public interface Builder<T> {
|
||||
T build(Object... args);
|
||||
}
|
||||
|
||||
private static final Junction<MethodDescription> CACHABLE_METHODS =
|
||||
takesArguments(0).and(not(isStatic()).and(isPublic()));
|
||||
|
||||
public interface IFieldProxy {
|
||||
Object getValue();
|
||||
void setValue(Object value);
|
||||
}
|
||||
|
||||
private static final String F_CACHE = "__MemoizingProxy__cache";
|
||||
private static final String F_DURATION = "__MemoizingProxy__duration";
|
||||
|
||||
private static final MethodDelegation.WithCustomProperties METHOD_DELEGATION = MethodDelegation.withDefaultConfiguration()
|
||||
.withBinders(FieldProxy.Binder.install(IFieldProxy.class));
|
||||
|
||||
public static class ConstructorInterceptor {
|
||||
public static void intercept(@FieldProxy(F_CACHE) IFieldProxy fCache, @FieldValue(F_DURATION) long duration) {
|
||||
fCache.setValue(CacheBuilder.newBuilder().expireAfterWrite(duration, TimeUnit.MILLISECONDS).build());
|
||||
}
|
||||
}
|
||||
|
||||
public static class MethodInterceptor {
|
||||
@RuntimeType
|
||||
public static Object intercept(
|
||||
@Origin(cache = true) Method method,
|
||||
@FieldValue(F_CACHE) Cache<String,Result> cache,
|
||||
@SuperCall Callable<?> zuper,
|
||||
@AllArguments Object[] args
|
||||
) throws Exception {
|
||||
synchronized (cache) {
|
||||
String mname = method.getName();
|
||||
Result r = cache.get(mname, () -> new Result(() -> {
|
||||
try {
|
||||
return zuper.call();
|
||||
} catch (Throwable e) {
|
||||
throw ExceptionUtil.exception(e);
|
||||
}
|
||||
}));
|
||||
return r.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class Result {
|
||||
|
||||
Throwable e;
|
||||
@@ -51,78 +112,108 @@ public class MemoizingProxy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Memoizes all zero-argument public methods for a given duration.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T create(Class<T> klass, Duration duration, Class<?>[] argTypes, Object... args) {
|
||||
Enhancer enhancer = new Enhancer();
|
||||
enhancer.setSuperclass(klass);
|
||||
enhancer.setCallback(new MethodInterceptor() {
|
||||
Cache<String, Result> cache = CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(duration.toMillis(), TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
if (Modifier.isPublic(method.getModifiers()) && (args==null || args.length==0)) {
|
||||
synchronized (cache) {
|
||||
String mname = method.getName();
|
||||
Result r = cache.get(mname, () -> new Result(() -> {
|
||||
try {
|
||||
return proxy.invokeSuper(obj, args);
|
||||
} catch (Throwable e) {
|
||||
throw ExceptionUtil.exception(e);
|
||||
}
|
||||
}));
|
||||
return r.get();
|
||||
}
|
||||
} else {
|
||||
return proxy.invokeSuper(obj, args);
|
||||
public static <T> Builder<T> builder(Class<T> klass, Duration duration, Class<?>... argTypes) throws Exception {
|
||||
DynamicType.Builder<T> builder = new ByteBuddy()
|
||||
.subclass(klass, ConstructorStrategy.Default.NO_CONSTRUCTORS)
|
||||
.defineField(F_DURATION, long.class, Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC).value(duration.toMillis())
|
||||
.defineField(F_CACHE, Cache.class, Opcodes.ACC_PRIVATE)
|
||||
.method(CACHABLE_METHODS).intercept(MethodDelegation.to(MethodInterceptor.class))
|
||||
.defineConstructor(Visibility.PUBLIC).withParameters(argTypes).intercept(
|
||||
MethodCall.invoke(klass.getConstructor(argTypes)).withAllArguments()
|
||||
.andThen(METHOD_DELEGATION.to(ConstructorInterceptor.class))
|
||||
);
|
||||
|
||||
Constructor<? extends T> constructor = builder.make().load(klass.getClassLoader()).getLoaded().getConstructor(argTypes);
|
||||
return new Builder<T>() {
|
||||
@Override
|
||||
public T build(Object... args) {
|
||||
try {
|
||||
return constructor.newInstance(args);
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtil.unchecked(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (T) enhancer.create(argTypes, args);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static class MemoizingProxyHandler implements MethodInterceptor {
|
||||
|
||||
private final Object original;
|
||||
private final Cache<String, Result> cache;
|
||||
|
||||
public MemoizingProxyHandler(Object original, Duration cacheExpiresAfter) {
|
||||
this.original = original;
|
||||
this.cache = CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(cacheExpiresAfter.toMillis(), TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
/**
|
||||
* Deprecated: use the 'builder' method instead
|
||||
*/
|
||||
@Deprecated
|
||||
public static <T> T create(Class<T> klass, Duration duration, Class<?>[] argTypes, Object... args) {
|
||||
try {
|
||||
return builder(klass, duration, argTypes).build(args);
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtil.unchecked(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
if (Modifier.isPublic(method.getModifiers()) && (args == null || args.length < 2)) {
|
||||
synchronized (cache) {
|
||||
String mname = method.getName();
|
||||
|
||||
if (args != null && args.length == 1) {
|
||||
mname += "-" + args[0].toString();
|
||||
}
|
||||
|
||||
Result r = cache.get(mname, () -> new Result(() -> {
|
||||
try {
|
||||
return method.invoke(original, args);
|
||||
} catch (Throwable e) {
|
||||
throw ExceptionUtil.exception(e);
|
||||
}
|
||||
}));
|
||||
return r.get();
|
||||
}
|
||||
} else {
|
||||
return method.invoke(original, args);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Enhancer enhancer = new Enhancer();
|
||||
// enhancer.setSuperclass(klass);
|
||||
// enhancer.setCallback(new MethodInterceptor() {
|
||||
// Cache<String, Result> cache = CacheBuilder.newBuilder()
|
||||
// .expireAfterWrite(duration.toMillis(), TimeUnit.MILLISECONDS)
|
||||
// .build();
|
||||
//
|
||||
// public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
// if (Modifier.isPublic(method.getModifiers()) && (args==null || args.length==0)) {
|
||||
// synchronized (cache) {
|
||||
// String mname = method.getName();
|
||||
// Result r = cache.get(mname, () -> new Result(() -> {
|
||||
// try {
|
||||
// return proxy.invokeSuper(obj, args);
|
||||
// } catch (Throwable e) {
|
||||
// throw ExceptionUtil.exception(e);
|
||||
// }
|
||||
// }));
|
||||
// return r.get();
|
||||
// }
|
||||
// } else {
|
||||
// return proxy.invokeSuper(obj, args);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// return (T) enhancer.create(argTypes, args);
|
||||
// }
|
||||
|
||||
|
||||
// public static class MemoizingProxyHandler implements MethodInterceptor {
|
||||
//
|
||||
// private final Object original;
|
||||
// private final Cache<String, Result> cache;
|
||||
//
|
||||
// public MemoizingProxyHandler(Object original, Duration cacheExpiresAfter) {
|
||||
// this.original = original;
|
||||
// this.cache = CacheBuilder.newBuilder()
|
||||
// .expireAfterWrite(cacheExpiresAfter.toMillis(), TimeUnit.MILLISECONDS)
|
||||
// .build();
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
||||
// if (Modifier.isPublic(method.getModifiers()) && (args == null || args.length < 2)) {
|
||||
// synchronized (cache) {
|
||||
// String mname = method.getName();
|
||||
//
|
||||
// if (args != null && args.length == 1) {
|
||||
// mname += "-" + args[0].toString();
|
||||
// }
|
||||
//
|
||||
// Result r = cache.get(mname, () -> new Result(() -> {
|
||||
// try {
|
||||
// return method.invoke(original, args);
|
||||
// } catch (Throwable e) {
|
||||
// throw ExceptionUtil.exception(e);
|
||||
// }
|
||||
// }));
|
||||
// return r.get();
|
||||
// }
|
||||
// } else {
|
||||
// return method.invoke(original, args);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
package org.springframework.ide.vscode.commons.util;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -38,6 +40,11 @@ public class MemoizingProxyTest {
|
||||
this.otherConstructorArg = otherConstructorArg;
|
||||
}
|
||||
|
||||
public int throwsError() throws IOException {
|
||||
invocations.add("throwsError");
|
||||
throw new IOException("Problem");
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
invocations.add("getName");
|
||||
return name;
|
||||
@@ -53,6 +60,11 @@ public class MemoizingProxyTest {
|
||||
return getName();
|
||||
}
|
||||
}
|
||||
|
||||
private TestSubject defaultTestSubject() {
|
||||
return MemoizingProxy.create(TestSubject.class, Duration.ofMinutes(1), CONSTRUCTOR_ARG_TYPES, "Johny", 45);
|
||||
}
|
||||
|
||||
private void assertInvocations(String...expectedInvocations) {
|
||||
assertEquals(ImmutableList.copyOf(expectedInvocations), proxy.invocations);
|
||||
proxy.invocations.clear();
|
||||
@@ -76,23 +88,40 @@ public class MemoizingProxyTest {
|
||||
|
||||
@Test
|
||||
public void constructorCalled() throws Exception {
|
||||
this.proxy = MemoizingProxy.create(TestSubject.class, Duration.ofMinutes(1), CONSTRUCTOR_ARG_TYPES, "Johny", 45);
|
||||
this.proxy = defaultTestSubject();
|
||||
assertEquals(proxy.name, "Johny"); //Constructor was called so name should be set
|
||||
assertEquals(proxy.otherConstructorArg, 45); //Constructor was called so name should be set
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroArgMethodCached() throws Exception {
|
||||
this.proxy = MemoizingProxy.create(TestSubject.class, Duration.ofMinutes(1), CONSTRUCTOR_ARG_TYPES, "Johny", 45);
|
||||
this.proxy = defaultTestSubject();
|
||||
assertEquals(proxy.getName(), "Johny");
|
||||
assertInvocations("getName");
|
||||
assertEquals(proxy.getName(), "Johny");
|
||||
assertInvocations(/*NONE*/);
|
||||
}
|
||||
|
||||
@Test public void exceptionsCached() throws Exception {
|
||||
this.proxy = defaultTestSubject();
|
||||
callMethodThatThrows();
|
||||
assertInvocations("throwsError");
|
||||
callMethodThatThrows();
|
||||
assertInvocations(/*NONE*/);
|
||||
}
|
||||
|
||||
private void callMethodThatThrows() {
|
||||
try {
|
||||
this.proxy.throwsError();
|
||||
fail("should have thrown");
|
||||
} catch (IOException e) {
|
||||
assertEquals("Problem", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void methodWithArgumentNotCached() throws Exception {
|
||||
this.proxy = MemoizingProxy.create(TestSubject.class, Duration.ofMinutes(1), CONSTRUCTOR_ARG_TYPES, "Johny", 45);
|
||||
this.proxy = defaultTestSubject();
|
||||
assertInvocations(/*NONE*/);
|
||||
|
||||
assertEquals(proxy.getMessage(" whatever"), "Johny whatever");
|
||||
@@ -105,7 +134,7 @@ public class MemoizingProxyTest {
|
||||
|
||||
@Test
|
||||
public void callsViaThisCached() throws Exception {
|
||||
this.proxy = MemoizingProxy.create(TestSubject.class, Duration.ofMinutes(1), CONSTRUCTOR_ARG_TYPES, "Johny", 45);
|
||||
this.proxy = defaultTestSubject();
|
||||
assertInvocations(/*NONE*/);
|
||||
|
||||
assertEquals(proxy.getMessage(" whatever"), "Johny whatever");
|
||||
@@ -126,7 +155,7 @@ public class MemoizingProxyTest {
|
||||
|
||||
|
||||
@Test public void multiThreaded() throws Exception {
|
||||
this.proxy = MemoizingProxy.create(TestSubject.class, Duration.ofMinutes(1), CONSTRUCTOR_ARG_TYPES, "Johny", 45);
|
||||
this.proxy = defaultTestSubject();
|
||||
ExecutorService manyThreads = Executors.newFixedThreadPool(100);
|
||||
Future<?>[] futures = new Future<?>[1000];
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@
|
||||
<jackson-2-version>2.5.0</jackson-2-version>
|
||||
<jersey-2-version>2.10</jersey-2-version>
|
||||
<lsp4j-version>0.7.2</lsp4j-version>
|
||||
<cglib-version>3.2.7</cglib-version>
|
||||
<!-- NOTE: Reactor version must match version used by the CF client -->
|
||||
<cloudfoundry-client-version>3.8.0.RELEASE</cloudfoundry-client-version>
|
||||
<reactor-version>3.1.5.RELEASE</reactor-version>
|
||||
|
||||
Reference in New Issue
Block a user