diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootAppCache.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootAppCache.java index 884ca6d8a..05cbfac33 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootAppCache.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootAppCache.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Pivotal, Inc. + * Copyright (c) 2017, 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 @@ -15,17 +15,22 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import org.springframework.ide.vscode.commons.util.MemoizingProxy; + import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; + public class LocalSpringBootAppCache { private static final Duration EXPIRE_AFTER = Duration.ofMillis(500); //Limits rate at which we refresh list of apps private long nextRefreshAfter = Long.MIN_VALUE; - private ImmutableMap apps = ImmutableMap.of(); + private ImmutableMap apps = ImmutableMap.of(); public synchronized Collection getAllRunningJavaApps() { if (System.currentTimeMillis()>=nextRefreshAfter) { @@ -36,14 +41,17 @@ public class LocalSpringBootAppCache { private void refresh() { List currentVms = VirtualMachine.list(); - ImmutableMap.Builder newAppsBuilder = ImmutableMap.builder(); + ImmutableMap.Builder newAppsBuilder = ImmutableMap.builder(); for (VirtualMachineDescriptor vm : currentVms) { - LocalSpringBootApp existingApp = apps.get(vm); + SpringBootApp existingApp = apps.get(vm); if (existingApp!=null) { newAppsBuilder.put(vm, existingApp); } else { try { - newAppsBuilder.put(vm, new LocalSpringBootApp(vm)); + LocalSpringBootApp localApp = new LocalSpringBootApp(vm); + MethodInterceptor handler = new MemoizingProxy.MemoizingProxyHandler(localApp, Duration.ofMillis(4500)); + SpringBootApp proxiedLocalApp = (SpringBootApp) Enhancer.create(SpringBootApp.class, handler); + newAppsBuilder.put(vm, proxiedLocalApp); } catch (Exception e) { //Ignore problems attaching to a VM. We will try again on next polling loop, if vm still exists. //The most likely cause is that the VM already died since we obtained a reference to it. @@ -51,7 +59,7 @@ public class LocalSpringBootAppCache { } } HashSet oldVms = new HashSet<>(apps.keySet()); - ImmutableMap newApps = newAppsBuilder.build(); + ImmutableMap newApps = newAppsBuilder.build(); oldVms.removeAll(newApps.keySet()); for (VirtualMachineDescriptor oldVm : oldVms) { apps.get(oldVm).dispose(); 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 393402a8d..4259e0df4 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 @@ -40,12 +40,12 @@ public class RemoteSpringBootApp extends AbstractSpringBootApp { @Override public String getPort() throws Exception { - return port!=null ? port : super.getPort(); + return port != null ? port : super.getPort(); } @Override public String getHost() throws Exception { - if (host!=null) { + if (host != null) { return host; } return super.getHost(); @@ -66,7 +66,7 @@ public class RemoteSpringBootApp extends AbstractSpringBootApp { public boolean isSpringBootApp() { //For now, let's assume that, if its not a boot app, then we won't create a RemoteSpringBootApp instance for it. //The check that is here really only determines whether there's a process reachable at the remote jmx url. - return getProcessID()!=null; + return getProcessID() != null; } @Override @@ -82,9 +82,9 @@ public class RemoteSpringBootApp extends AbstractSpringBootApp { public String getProcessName() throws Exception { try { String command = getJavaCommand(); - if (command!=null) { + if (command != null) { int space = command.indexOf(' '); - if (space>=0) { + if (space >= 0) { command = command.substring(0, space); } command = command.trim(); 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 a9cde5f9c..2c962249a 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 @@ -89,4 +89,43 @@ public class MemoizingProxy { return (T) enhancer.create(argumentTypes, 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/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java index c4fdba858..54899dae0 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.boot.java.handlers; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.Optional; @@ -58,14 +59,15 @@ public class BootJavaHoverProvider implements HoverHandler { private JavaProjectFinder projectFinder; private BootJavaLanguageServerComponents server; private AnnotationHierarchyAwareLookup hoverProviders; - private RunningAppProvider runningAppProvider; + + private Collection runningSpringBootApps; public BootJavaHoverProvider(BootJavaLanguageServerComponents server, JavaProjectFinder projectFinder, AnnotationHierarchyAwareLookup specificProviders, RunningAppProvider runningAppProvider) { this.server = server; this.projectFinder = projectFinder; this.hoverProviders = specificProviders; - this.runningAppProvider = runningAppProvider; + this.runningSpringBootApps = Collections.emptyList(); } @Override @@ -262,10 +264,11 @@ public class BootJavaHoverProvider implements HoverHandler { private Hover provideHoverForMethodDeclaration(MethodDeclaration methodDeclaration, int offset, TextDocument doc, IJavaProject project) { SpringBootApp[] runningApps = getRunningSpringApps(project); + if (runningApps.length > 0) { for (HoverProvider provider : this.hoverProviders.getAll()) { Hover hover = provider.provideHover(methodDeclaration, offset, doc, project, runningApps); - if (hover!=null) { + if (hover != null) { //TODO: compose multiple hovers somehow instead of just returning the first one? return hover; } @@ -278,8 +281,10 @@ public class BootJavaHoverProvider implements HoverHandler { ITypeBinding type = annotation.resolveTypeBinding(); if (type != null) { logger.debug("Hover requested for "+type.getName()); + SpringBootApp[] runningApps = getRunningSpringApps(project); if (runningApps.length > 0) { + for (HoverProvider provider : this.hoverProviders.get(type)) { Hover hover = provider.provideHover(exactNode, annotation, type, offset, doc, project, runningApps); if (hover!=null) { @@ -347,11 +352,18 @@ public class BootJavaHoverProvider implements HoverHandler { private SpringBootApp[] getRunningSpringApps(IJavaProject project) { try { - return RunningAppMatcher.getAllMatchingApps(runningAppProvider.getAllRunningSpringApps(), project).toArray(new SpringBootApp[0]); + Collection allApps = this.runningSpringBootApps; + Collection allMatchingApps = RunningAppMatcher.getAllMatchingApps(allApps, project); + + return allMatchingApps.toArray(new SpringBootApp[0]); } catch (Exception e) { logger.error("error getting all matching projects for project'" + project.getElementName() + "'", e); return new SpringBootApp[0]; } } + public void setRunningSpringApps(Collection allRunningSpringBootApps) { + this.runningSpringBootApps = allRunningSpringBootApps; + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringLiveHoverWatchdog.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringLiveHoverWatchdog.java index 6caa1c095..7911b1137 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringLiveHoverWatchdog.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringLiveHoverWatchdog.java @@ -17,6 +17,7 @@ import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentSkipListSet; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.lsp4j.CodeLens; @@ -33,8 +34,12 @@ import org.springframework.ide.vscode.commons.languageserver.HighlightParams; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.util.MemoizingProxy; import org.springframework.ide.vscode.commons.util.text.TextDocument; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; + /** * @author Martin Lippert */ @@ -155,6 +160,26 @@ public class SpringLiveHoverWatchdog { } } + protected void update() { + if (this.watchedDocs.size() > 0) { + try { + Collection runningBootApps = runningAppProvider.getAllRunningSpringApps(); + Collection cachedApps = createAppCaches(runningBootApps); + + for (String docURI : watchedDocs) { + IJavaProject project = identifyProject(docURI); + SpringBootApp[] matchingApps = RunningAppMatcher.getAllMatchingApps(cachedApps, project).toArray(new SpringBootApp[0]); + update(docURI, matchingApps); + } + + this.hoverProvider.setRunningSpringApps(cachedApps); + + } catch (Exception e) { + logger.error("", e); + } + } + } + protected void update(String docURI, SpringBootApp[] runningBootApps) { if (highlightsEnabled) { try { @@ -175,20 +200,20 @@ public class SpringLiveHoverWatchdog { } } - protected void update() { - if (this.watchedDocs.size() > 0) { - try { - Collection runningBootApps = runningAppProvider.getAllRunningSpringApps(); + private Collection createAppCaches(Collection runningBootApps) { + return runningBootApps.stream().map(app -> { + MethodInterceptor handler = new MemoizingProxy.MemoizingProxyHandler(app, Duration.ofMillis(20000)); + SpringBootApp proxied = (SpringBootApp) Enhancer.create(SpringBootApp.class, handler); - for (String docURI : watchedDocs) { - IJavaProject project = identifyProject(docURI); - SpringBootApp[] matchingApps = RunningAppMatcher.getAllMatchingApps(runningBootApps, project).toArray(new SpringBootApp[0]); - update(docURI, matchingApps); - } - } catch (Exception e) { - logger.error("", e); - } - } + try { + proxied.getProcessName(); + proxied.getProcessID(); + } + catch (Exception e) { + } + + return proxied; + }).filter(app -> app != null).collect(Collectors.toList()); } private IJavaProject identifyProject(String docURI) {