Add support for interface type in MemoizingProxyTest
This commit is contained in:
@@ -10,12 +10,16 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.util;
|
||||
|
||||
import static net.bytebuddy.matcher.ElementMatchers.*;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.isStatic;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.not;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
@@ -26,17 +30,19 @@ 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.FieldAccessor;
|
||||
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.IgnoreForBinding;
|
||||
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;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
|
||||
/**
|
||||
* Utility to instrument a given class, memoizing all it's zero-argument method invocations.
|
||||
@@ -46,7 +52,8 @@ import net.bytebuddy.matcher.ElementMatcher.Junction;
|
||||
public class MemoizingProxy {
|
||||
|
||||
public interface Builder<T> {
|
||||
T build(Object... args);
|
||||
T newInstance(Object... args);
|
||||
T delegateTo(T delegate);
|
||||
}
|
||||
|
||||
private static final Junction<MethodDescription> CACHABLE_METHODS =
|
||||
@@ -59,17 +66,18 @@ public class MemoizingProxy {
|
||||
|
||||
private static final String F_CACHE = "__MemoizingProxy__cache";
|
||||
private static final String F_DURATION = "__MemoizingProxy__duration";
|
||||
private static final String F_DELEGATE = "__MemoizingProxy__delegate";
|
||||
|
||||
private static final MethodDelegation.WithCustomProperties METHOD_DELEGATION = MethodDelegation.withDefaultConfiguration()
|
||||
.withBinders(FieldProxy.Binder.install(IFieldProxy.class));
|
||||
|
||||
public static class ConstructorInterceptor {
|
||||
public static class ClassConstructorInterceptor {
|
||||
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 {
|
||||
public static class SuperMethodInterceptor {
|
||||
@RuntimeType
|
||||
public static Object intercept(
|
||||
@Origin(cache = true) Method method,
|
||||
@@ -91,6 +99,40 @@ public class MemoizingProxy {
|
||||
}
|
||||
}
|
||||
|
||||
public static class DelegateMethodInterceptor {
|
||||
|
||||
@RuntimeType
|
||||
public static Object intercept(
|
||||
@Origin(cache = true) Method method,
|
||||
@FieldValue(F_CACHE) Cache<String,Result> cache,
|
||||
@FieldValue(F_DELEGATE) Object delegate,
|
||||
@AllArguments Object[] args
|
||||
) throws Exception {
|
||||
if (args.length==0) {
|
||||
synchronized (cache) {
|
||||
String mname = method.getName();
|
||||
Result r = cache.get(mname, () -> new Result(() -> {
|
||||
return callTheDelegate(method, delegate, args);
|
||||
}));
|
||||
return r.get();
|
||||
}
|
||||
} else { //has arguments do not cache
|
||||
return callTheDelegate(method, delegate, args);
|
||||
}
|
||||
}
|
||||
|
||||
@IgnoreForBinding
|
||||
private static Object callTheDelegate(Method method, Object delegate, Object[] args) throws Exception {
|
||||
try {
|
||||
return method.invoke(delegate, args);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw ExceptionUtil.exception(e.getTargetException());
|
||||
} catch (Throwable e) {
|
||||
throw ExceptionUtil.exception(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class Result {
|
||||
|
||||
Throwable e;
|
||||
@@ -113,27 +155,72 @@ public class MemoizingProxy {
|
||||
}
|
||||
|
||||
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);
|
||||
if (klass.isInterface()) {
|
||||
Assert.isLegal(argTypes.length==0, "Should not provide constructor argument types for interface type "+klass.getSimpleName());
|
||||
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)
|
||||
.defineField(F_DELEGATE, klass, Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL)
|
||||
.method(ElementMatchers.isAbstract()).intercept(MethodDelegation.to(DelegateMethodInterceptor.class))
|
||||
.defineConstructor(Visibility.PUBLIC).withParameters(klass).intercept(
|
||||
MethodCall.invoke(Object.class.getConstructor())
|
||||
.andThen(FieldAccessor.ofField(F_DELEGATE).setsArgumentAt(0))
|
||||
.andThen(METHOD_DELEGATION.to(ClassConstructorInterceptor.class))
|
||||
);
|
||||
|
||||
Constructor<? extends T> constructor = builder.make().load(klass.getClassLoader()).getLoaded().getConstructor(klass);
|
||||
return new Builder<T>() {
|
||||
|
||||
@Override
|
||||
public T newInstance(Object... args) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Can't use 'newInstance' because '"+klass.getSimpleName()+" is an interface.\n"+
|
||||
"Use 'delegateTo' instead."
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public T delegateTo(T delegate) {
|
||||
try {
|
||||
return constructor.newInstance(delegate);
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtil.unchecked(e);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
} else {
|
||||
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(SuperMethodInterceptor.class))
|
||||
.defineConstructor(Visibility.PUBLIC).withParameters(argTypes).intercept(
|
||||
MethodCall.invoke(klass.getConstructor(argTypes)).withAllArguments()
|
||||
.andThen(METHOD_DELEGATION.to(ClassConstructorInterceptor.class))
|
||||
);
|
||||
|
||||
Constructor<? extends T> constructor = builder.make().load(klass.getClassLoader()).getLoaded().getConstructor(argTypes);
|
||||
return new Builder<T>() {
|
||||
@Override
|
||||
public T newInstance(Object... args) {
|
||||
try {
|
||||
return constructor.newInstance(args);
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtil.unchecked(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T delegateTo(T delegate) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Can't use 'delegateTo' because '"+klass.getSimpleName()+" is not an interface.\n"+
|
||||
"Use 'newInstance' instead."
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,78 +229,10 @@ public class MemoizingProxy {
|
||||
@Deprecated
|
||||
public static <T> T create(Class<T> klass, Duration duration, Class<?>[] argTypes, Object... args) {
|
||||
try {
|
||||
return builder(klass, duration, argTypes).build(args);
|
||||
return builder(klass, duration, argTypes).newInstance(args);
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtil.unchecked(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.springframework.ide.vscode.commons.util.MemoizingProxy.Builder;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
public class MemoizingProxyTest {
|
||||
public class MemoizingProxyClassTest {
|
||||
|
||||
TestSubject proxy;
|
||||
|
||||
@@ -164,8 +164,8 @@ public class MemoizingProxyTest {
|
||||
|
||||
//Using the new 'builder' api allows re-using the same class (if used properly)
|
||||
Builder<TestSubject> builder = MemoizingProxy.builder(TestSubject.class, Duration.ofMinutes(1), CONSTRUCTOR_ARG_TYPES);
|
||||
proxy1 = builder.build("Freddy", 12);
|
||||
proxy2 = builder.build("Johny", 45);
|
||||
proxy1 = builder.newInstance("Freddy", 12);
|
||||
proxy2 = builder.newInstance("Johny", 45);
|
||||
assertFalse(proxy1.equals(proxy2)); // different instance...
|
||||
assertEquals(proxy1.getClass(), proxy2.getClass()); //same class
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2019 Pivotal, Inc.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* https://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.util;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.ide.vscode.commons.util.MemoizingProxy.Builder;
|
||||
import org.springframework.ide.vscode.commons.util.MemoizingProxyInterfaceTest.TestInterface;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
public class MemoizingProxyInterfaceTest {
|
||||
|
||||
public interface TestInterface {
|
||||
int throwsError() throws IOException;
|
||||
String getMyName();
|
||||
String getMessage(String someArgument);
|
||||
String getName();
|
||||
int getAge();
|
||||
}
|
||||
|
||||
TestInterface proxy;
|
||||
private TestSubject delegate;
|
||||
|
||||
public static class TestSubject implements TestInterface {
|
||||
|
||||
List<String> invocations = new ArrayList<>();
|
||||
String name;
|
||||
int age;
|
||||
|
||||
public TestSubject(String name, int otherConstructorArg) {
|
||||
this.name = name;
|
||||
this.age = otherConstructorArg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int throwsError() throws IOException {
|
||||
invocations.add("throwsError");
|
||||
throw new IOException("Problem");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
invocations.add("getName");
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage(String someArgument) {
|
||||
invocations.add("getMessage");
|
||||
return getName() + someArgument;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMyName() {
|
||||
invocations.add("getMyName");
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
}
|
||||
|
||||
private void defaultTestSubject() throws Exception {
|
||||
this.delegate = new TestSubject("Johny", 45);
|
||||
this.proxy = MemoizingProxy.builder(TestInterface.class, Duration.ofMinutes(1)).delegateTo(delegate);
|
||||
}
|
||||
|
||||
private void assertInvocations(String...expectedInvocations) {
|
||||
assertEquals(ImmutableList.copyOf(expectedInvocations), delegate.invocations);
|
||||
delegate.invocations.clear();
|
||||
}
|
||||
|
||||
private void sleep(int millis) {
|
||||
long endTime = System.currentTimeMillis()+millis;
|
||||
long timeLeft = endTime - System.currentTimeMillis();
|
||||
while (timeLeft>0) {
|
||||
try {
|
||||
Thread.sleep(timeLeft);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
timeLeft = endTime - System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorCalled() throws Exception {
|
||||
defaultTestSubject();
|
||||
assertEquals(proxy.getName(), "Johny"); //Constructor was called so name should be set
|
||||
assertEquals(proxy.getAge(), 45); //Constructor was called so name should be set
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroArgMethodCached() throws Exception {
|
||||
defaultTestSubject();
|
||||
assertEquals(proxy.getName(), "Johny");
|
||||
assertInvocations("getName");
|
||||
assertEquals(proxy.getName(), "Johny");
|
||||
assertInvocations(/*NONE*/);
|
||||
}
|
||||
|
||||
@Test public void exceptionsCached() throws Exception {
|
||||
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 {
|
||||
defaultTestSubject();
|
||||
assertInvocations(/*NONE*/);
|
||||
|
||||
assertEquals(proxy.getMessage(" whatever"), "Johny whatever");
|
||||
assertInvocations("getMessage", "getName");
|
||||
|
||||
assertEquals(proxy.getMessage(" whatever"), "Johny whatever");
|
||||
assertInvocations("getMessage", "getName");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callsViaThisNotCached() throws Exception {
|
||||
defaultTestSubject();
|
||||
assertInvocations(/*NONE*/);
|
||||
|
||||
assertEquals(proxy.getMessage(" whatever"), "Johny whatever");
|
||||
assertInvocations("getMessage", "getName");
|
||||
|
||||
assertEquals(proxy.getMyName(), "Johny");
|
||||
assertInvocations("getMyName", "getName");
|
||||
}
|
||||
|
||||
@Test public void cacheExpires() throws Exception {
|
||||
this.delegate = new TestSubject("Johny", 45);
|
||||
this.proxy = MemoizingProxy.builder(TestInterface.class, Duration.ofMillis(10)).delegateTo(delegate);
|
||||
assertEquals("Johny", proxy.getMyName());
|
||||
assertInvocations("getMyName", "getName");
|
||||
sleep(20);
|
||||
assertEquals("Johny", proxy.getMyName());
|
||||
assertInvocations("getMyName", "getName");
|
||||
}
|
||||
|
||||
@Test public void proxyClassReused() throws Exception {
|
||||
Builder<TestInterface> builder = MemoizingProxy.builder(TestInterface.class, Duration.ofMinutes(1));
|
||||
TestSubject delegate1 = new TestSubject("Freddy", 12);
|
||||
TestInterface proxy1 = builder.delegateTo(delegate1);
|
||||
TestSubject delegate2 = new TestSubject("Johny", 45);
|
||||
TestInterface proxy2 = builder.delegateTo(delegate2);
|
||||
|
||||
assertFalse(proxy1.equals(proxy2)); // different instance...
|
||||
assertEquals(proxy1.getClass(), proxy2.getClass()); //same class
|
||||
}
|
||||
|
||||
@Test public void multiThreaded() throws Exception {
|
||||
defaultTestSubject();
|
||||
ExecutorService manyThreads = Executors.newFixedThreadPool(100);
|
||||
Future<?>[] futures = new Future<?>[1000];
|
||||
|
||||
for (int i = 0; i < futures.length; i++) {
|
||||
futures[i] = manyThreads.submit(() -> {
|
||||
assertEquals("Johny", proxy.getMyName());
|
||||
});
|
||||
}
|
||||
|
||||
for (Future<?> future : futures) {
|
||||
future.get();
|
||||
}
|
||||
|
||||
//Should only have one call to each method all the rest should hit the cache
|
||||
assertInvocations("getMyName", "getName");
|
||||
|
||||
manyThreads.shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user