From 4b31ac99eed311a8221afe0bc91f002d6b684bee Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Thu, 12 Jul 2018 16:06:43 -0700 Subject: [PATCH] Better caching proxy using CGlib Allows internal calls to this.getBlah() to also be cached. --- .../boot/app/cli/AbstractSpringBootApp.java | 13 ++- .../boot/app/cli/NoArgumentsCacheHandler.java | 45 --------- .../boot/app/cli/RemoteSpringBootApp.java | 10 +- .../commons/boot/app/cli/SpringBootApp.java | 5 - .../commons/commons-util/pom.xml | 7 ++ .../vscode/commons/util/MemoizingProxy.java | 92 +++++++++++++++++++ headless-services/commons/pom.xml | 1 + 7 files changed, 111 insertions(+), 62 deletions(-) delete mode 100644 headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/NoArgumentsCacheHandler.java create mode 100644 headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/MemoizingProxy.java diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java index a56916507..bfee0dd01 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java @@ -78,15 +78,14 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { @Override public abstract boolean isSpringBootApp(); - @Override - public JMXConnector getJmxConnector() throws MalformedURLException, IOException { + protected JMXConnector getJmxConnector() throws MalformedURLException, IOException { JMXServiceURL serviceUrl = getJmxUrl(); JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, null); return jmxConnector; } @Override - public final List getActiveProfiles() { + public List getActiveProfiles() { try { String _env = getEnvironment(); if (_env != null) { @@ -161,7 +160,7 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { } @Override - public final LiveBeansModel getBeans() { + public LiveBeansModel getBeans() { try { String domain = getDomainForActuator(); String json = getBeansFromActuator(domain); @@ -200,7 +199,7 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { * @return JMX MBean domain containing actuator information, or null if not resolved. * @throws Exception when resolving domain from JMX */ - protected final String getDomainForActuator() throws Exception { + protected String getDomainForActuator() throws Exception { if (this.jmxMbeanActuatorDomain == null) { JMXConnector jmxConnector = null; try { @@ -252,7 +251,7 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { } } - protected final Object getActuatorDataFromAttribute(ObjectName objectName, String attribute) throws Exception { + protected Object getActuatorDataFromAttribute(ObjectName objectName, String attribute) throws Exception { JMXConnector jmxConnector = null; try { if (objectName != null) { @@ -273,7 +272,7 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { } } - protected final Object getActuatorDataFromOperation(ObjectName objectName, String operation) throws Exception { + protected Object getActuatorDataFromOperation(ObjectName objectName, String operation) throws Exception { JMXConnector jmxConnector = null; try { if (objectName != null) { diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/NoArgumentsCacheHandler.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/NoArgumentsCacheHandler.java deleted file mode 100644 index af61b0858..000000000 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/NoArgumentsCacheHandler.java +++ /dev/null @@ -1,45 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2018 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 - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Pivotal, Inc. - initial API and implementation - *******************************************************************************/ -package org.springframework.ide.vscode.commons.boot.app.cli; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.time.Duration; -import java.util.concurrent.TimeUnit; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; - -public class NoArgumentsCacheHandler implements InvocationHandler { - - private Cache cache; - private Object delegate; - - public NoArgumentsCacheHandler(Object delegate, Duration cacheDuration) { - this.delegate = delegate; - this.cache = CacheBuilder.newBuilder().expireAfterWrite(cacheDuration.toMillis(), TimeUnit.MILLISECONDS).build(); - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if (args==null || args.length==0) { - return invokeCached(proxy, method); - } - return method.invoke(proxy, args); - } - - private Object invokeCached(Object proxy, Method method) throws Exception { - return cache.get(method.getName(), () -> { - return method.invoke(delegate, new Object[] {}); - }); - } - -} diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/RemoteSpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/RemoteSpringBootApp.java index cb7243711..15ed6135a 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/RemoteSpringBootApp.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/RemoteSpringBootApp.java @@ -12,7 +12,6 @@ package org.springframework.ide.vscode.commons.boot.app.cli; import java.io.IOException; import java.lang.management.RuntimeMXBean; -import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.time.Duration; import java.util.Map.Entry; @@ -20,11 +19,13 @@ import java.util.Properties; import javax.management.remote.JMXServiceURL; +import org.springframework.ide.vscode.commons.util.MemoizingProxy; + public class RemoteSpringBootApp extends AbstractSpringBootApp { private String jmxUrl; - private RemoteSpringBootApp(String jmxUrl) { + protected RemoteSpringBootApp(String jmxUrl) { this.jmxUrl = jmxUrl; } @@ -75,14 +76,13 @@ public class RemoteSpringBootApp extends AbstractSpringBootApp { } } } catch (IOException e) { - logger.equals(e); + logger.error("", e); } return "Unknown"; } public static SpringBootApp create(String jmxUrl) { - RemoteSpringBootApp delegate = new RemoteSpringBootApp(jmxUrl); - return (SpringBootApp) Proxy.newProxyInstance(RemoteSpringBootApp.class.getClassLoader(), new Class[] {SpringBootApp.class}, new NoArgumentsCacheHandler(delegate, Duration.ofMillis(4900))); + return MemoizingProxy.create(RemoteSpringBootApp.class, Duration.ofMillis(4900), jmxUrl); } } diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java index e8f8709da..821491506 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java @@ -10,15 +10,11 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.boot.app.cli; -import java.io.IOException; -import java.net.MalformedURLException; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Properties; -import javax.management.remote.JMXConnector; - import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping; @@ -38,7 +34,6 @@ public interface SpringBootApp { List getActiveProfiles(); Optional> getLiveConditionals() throws Exception; Properties getSystemProperties() throws Exception; - JMXConnector getJmxConnector() throws MalformedURLException, IOException; default String getSystemProperty(String string) throws Exception { Object r = getSystemProperties().get(string); diff --git a/headless-services/commons/commons-util/pom.xml b/headless-services/commons/commons-util/pom.xml index d8bd061c2..641d4f65a 100644 --- a/headless-services/commons/commons-util/pom.xml +++ b/headless-services/commons/commons-util/pom.xml @@ -49,6 +49,13 @@ reactor-core ${reactor-version} + + + cglib + cglib + ${cglib-version} + + com.kotcrab.remark 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 new file mode 100644 index 000000000..a9cde5f9c --- /dev/null +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/MemoizingProxy.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2018 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.util; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.time.Duration; +import java.util.concurrent.Callable; +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; + +/** + * Utility to instrument a given class, memoizing all it's zero-argument method invocations. + * Note that the memoization behaves a little different from guava's Suppliers.memoize in that + * it caches exception results as well as regularly returned values. + */ +public class MemoizingProxy { + + static class Result { + + Throwable e; + Object v; + + Result(Callable computer) { + try { + v = computer.call(); + } catch (Throwable e) { + this.e = e; + } + } + + public Object get() throws Exception { + if (e!=null) { + throw ExceptionUtil.exception(e); + } + return v; + } + } + + + /** + * Memoizes all zero-argument public methods for a given duration. + */ + @SuppressWarnings("unchecked") + public static T create(Class klass, Duration duration, Object... args) { + 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); + } + } + }); + Class[] argumentTypes = new Class[args.length]; + for (int i = 0; i < argumentTypes.length; i++) { + argumentTypes[i] = args[i].getClass(); + } + return (T) enhancer.create(argumentTypes, args); + } + +} diff --git a/headless-services/commons/pom.xml b/headless-services/commons/pom.xml index 0db38a318..ce010e3cd 100644 --- a/headless-services/commons/pom.xml +++ b/headless-services/commons/pom.xml @@ -80,6 +80,7 @@ 2.5.0 2.10 0.4.0-SNAPSHOT + 3.2.7 3.8.0.RELEASE 3.1.5.RELEASE