From f438b29533b94dbf71ca4ee58782df6cf46cc4b1 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Fri, 21 Jun 2019 12:26:52 -0700 Subject: [PATCH] Add support for interface type in MemoizingProxyTest --- .../vscode/commons/util/MemoizingProxy.java | 209 ++++++++++-------- ...Test.java => MemoizingProxyClassTest.java} | 6 +- .../util/MemoizingProxyInterfaceTest.java | 207 +++++++++++++++++ 3 files changed, 324 insertions(+), 98 deletions(-) rename headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/{MemoizingProxyTest.java => MemoizingProxyClassTest.java} (97%) create mode 100644 headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/MemoizingProxyInterfaceTest.java diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/MemoizingProxy.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/MemoizingProxy.java index f4b1d09c1..b099fe92d 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/MemoizingProxy.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/MemoizingProxy.java @@ -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 build(Object... args); + T newInstance(Object... args); + T delegateTo(T delegate); } private static final Junction 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 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 Builder builder(Class klass, Duration duration, Class... argTypes) throws Exception { - DynamicType.Builder 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 constructor = builder.make().load(klass.getClassLoader()).getLoaded().getConstructor(argTypes); - return new Builder() { - @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 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 constructor = builder.make().load(klass.getClassLoader()).getLoaded().getConstructor(klass); + return new Builder() { + + @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 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 constructor = builder.make().load(klass.getClassLoader()).getLoaded().getConstructor(argTypes); + return new Builder() { + @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 create(Class 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 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 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); -// } -// } -// -// } - } diff --git a/headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/MemoizingProxyTest.java b/headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/MemoizingProxyClassTest.java similarity index 97% rename from headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/MemoizingProxyTest.java rename to headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/MemoizingProxyClassTest.java index c13df8800..b694abf4b 100644 --- a/headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/MemoizingProxyTest.java +++ b/headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/MemoizingProxyClassTest.java @@ -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 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 } diff --git a/headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/MemoizingProxyInterfaceTest.java b/headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/MemoizingProxyInterfaceTest.java new file mode 100644 index 000000000..cebfa6e21 --- /dev/null +++ b/headless-services/commons/commons-util/src/test/java/org/springframework/ide/vscode/commons/util/MemoizingProxyInterfaceTest.java @@ -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 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 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(); + } + +}