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 8510253fd..2c4694748 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Pivotal, Inc. + * Copyright (c) 2018, 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 @@ -17,6 +17,7 @@ import java.lang.management.PlatformManagedObject; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -30,6 +31,8 @@ import java.util.concurrent.TimeUnit; import javax.management.InstanceNotFoundException; import javax.management.MBeanServerConnection; import javax.management.ObjectName; +import javax.management.Query; +import javax.management.QueryExp; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; @@ -74,6 +77,8 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { private String jmxMbeanActuatorDomain; + private Set nonBootLiveMBeanNames; + private LiveBeansModel cachedBeansModel; private String cachedBeansModelMD5; @@ -103,6 +108,9 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { @Override public abstract boolean isSpringBootApp(); + @Override + public abstract boolean isSpringApp(); + private final MemoizingDisposableSupplier jmxConnector = new MemoizingDisposableSupplier( //creating jmx connector: @@ -219,6 +227,28 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { } } + @Override + public boolean providesNonBootLiveBeans() { + return getNonBootSpringLiveMBeans().size() > 0; + } + + protected Set getNonBootSpringLiveMBeans() { + if (this.nonBootLiveMBeanNames == null) { + try { + this.nonBootLiveMBeanNames = withJmxConnector(jmxConnector -> { + MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); + QueryExp queryExp = Query.isInstanceOf(Query.value("org.springframework.context.support.LiveBeansView")); + return connection.queryNames(null, queryExp); + }); + } catch (Exception e) { + e.printStackTrace(); + this.nonBootLiveMBeanNames = Collections.emptySet(); + } + } + + return this.nonBootLiveMBeanNames; + } + protected ObjectName getObjectName(String keyProperties) throws Exception { String domain = getDomainForActuator(); return getObjectName(domain, keyProperties); @@ -240,6 +270,10 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { try { String domain = getDomainForActuator(); json = getBeansFromActuator(domain); + + if (json == null && this.providesNonBootLiveBeans()) { + json = getBeansFromNonBootMBean(); + } } catch (IOException e) { // PT 160096886 - Don't throw exception, as actuator info will not be available when app stopping, and // this is not an error condition. Return empty model instead. @@ -253,7 +287,14 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { synchronized(AbstractSpringBootApp.this) { if (cachedBeansModel == null || !md5.equals(cachedBeansModelMD5)) { - cachedBeansModel = LiveBeansModel.parse(gson.toJson(json)); + + if (json instanceof String) { + cachedBeansModel = LiveBeansModel.parse((String)json); + } + else { + cachedBeansModel = LiveBeansModel.parse(gson.toJson(json)); + } + cachedBeansModelMD5 = md5; logger.debug("Got {} beans for {}", cachedBeansModel.getBeanNames().size(), this); } else { @@ -276,6 +317,16 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { } } + protected Object getBeansFromNonBootMBean() throws Exception { + Set nonBootSpringLiveMBeans = getNonBootSpringLiveMBeans(); + if (nonBootSpringLiveMBeans.size() > 0) { + return getActuatorDataFromAttribute(nonBootSpringLiveMBeans.iterator().next(), "SnapshotAsJson"); + } + else { + return null; + } + } + private Object getBeansFromActuator(String domain) throws Exception { Object result = getActuatorDataFromOperation(getObjectName(domain, "type=Endpoint,name=Beans"), "beans"); if (result != null) return result; @@ -342,7 +393,7 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { return withJmxConnector(jmxConnector -> { try { MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); - return connection.getAttribute(objectName, "Data"); + return connection.getAttribute(objectName, attribute); } catch (InstanceNotFoundException|IOException e) { return null; } diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootApp.java index 845dfb358..baf2943e0 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootApp.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LocalSpringBootApp.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2018 Pivotal, Inc. + * Copyright (c) 2017, 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 @@ -11,11 +11,9 @@ package org.springframework.ide.vscode.commons.boot.app.cli; import java.io.IOException; -import java.time.Duration; import java.util.Collection; import java.util.Map.Entry; import java.util.Properties; -import java.util.concurrent.Callable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +27,7 @@ import com.sun.tools.attach.VirtualMachineDescriptor; /** * @author Martin Lippert */ +@SuppressWarnings("restriction") public class LocalSpringBootApp extends AbstractSpringBootApp { private static final Logger logger = LoggerFactory.getLogger(LocalSpringBootApp.class); @@ -39,6 +38,7 @@ public class LocalSpringBootApp extends AbstractSpringBootApp { private static final String LOCAL_CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress"; private Boolean isSpringBootApp; + private Boolean isSpringApp; private static LocalSpringBootAppCache cache = new LocalSpringBootAppCache(); @@ -47,7 +47,7 @@ public class LocalSpringBootApp extends AbstractSpringBootApp { } public static Collection getAllRunningSpringApps() throws Exception { - return getAllRunningJavaApps().stream().filter(SpringBootApp::isSpringBootApp).collect(CollectorUtil.toImmutableList()); + return getAllRunningJavaApps().stream().filter(SpringBootApp::isSpringApp).collect(CollectorUtil.toImmutableList()); } public LocalSpringBootApp(VirtualMachineDescriptor vmd) throws AttachNotSupportedException, IOException { @@ -92,9 +92,32 @@ public class LocalSpringBootApp extends AbstractSpringBootApp { return firstSpace < 0 ? rawName : rawName.substring(0, firstSpace); } + @Override + public boolean isSpringApp() { + if (isSpringApp == null) { + try { + isSpringApp = !containsSystemProperty("sts4.languageserver.name") + && ( + isSpringAppClasspath() || + providesNonBootLiveBeans() + ); + } catch (Exception e) { + //Couldn't determine if the VM is a spring boot app. Could be it already died. Or could be its not accessible (yet). + // We will ignore the exception, pretend its not a boot app (most likely isn't) but DO NOT CACHE this result + // so it will be retried again on the next polling loop. + return false; + } + } + return isSpringApp; + } + + private boolean isSpringAppClasspath() throws Exception { + return contains(getClasspath(), "spring-core"); + } + @Override public boolean isSpringBootApp() { - if (isSpringBootApp==null) { + if (isSpringBootApp == null) { try { isSpringBootApp = !containsSystemProperty("sts4.languageserver.name") && ( 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 de4e39d6b..90339638b 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Pivotal, Inc. + * Copyright (c) 2018, 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 @@ -64,11 +64,21 @@ public class RemoteSpringBootApp extends AbstractSpringBootApp { @Override public boolean isSpringBootApp() { + return isSpringApp(); + } + + @Override + public boolean isSpringApp() { //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; } + @Override + public boolean providesNonBootLiveBeans() { + return false; + } + @Override public String getProcessID() { try { 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 ec2681892..cb6cca2f4 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Pivotal, Inc. + * Copyright (c) 2018, 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 @@ -29,11 +29,16 @@ public interface SpringBootApp extends Disposable { String getHost() throws Exception; String getPort() throws Exception; String getContextPath() throws Exception; + boolean isSpringBootApp(); + boolean isSpringApp(); String getEnvironment() throws Exception; Collection getRequestMappings() throws Exception; + LiveBeansModel getBeans(); + boolean providesNonBootLiveBeans(); + List getActiveProfiles(); Optional> getLiveConditionals() throws Exception; Properties getSystemProperties() throws Exception; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerParams.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerParams.java index ea06f494d..8e476c714 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerParams.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerParams.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2018 Pivotal, Inc. + * Copyright (c) 2017, 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 @@ -98,7 +98,7 @@ public class BootLanguageServerParams { indexProvider.setProgressService(server.getProgressService()); return new BootLanguageServerParams( - jdtProjectCache.filter(SpringProjectUtil::isBootProject), + jdtProjectCache.filter(project -> SpringProjectUtil.isBootProject(project) || SpringProjectUtil.isSpringProject(project)), jdtProjectCache, indexProvider, (IDocument doc) -> new TypeUtil(jdtProjectCache.find(new TextDocumentIdentifier(doc.getUri()))), 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 8acf4c699..c722a19f7 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2018 Pivotal, Inc. + * Copyright (c) 2017, 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 @@ -101,7 +101,21 @@ public class BootJavaHoverProvider implements HoverHandler { Optional project = getProject(document); if (!project.isPresent()) return new CodeLens[0]; - if (!hasActuatorDependency(project.get())) return new CodeLens[0]; + + if (!hasActuatorDependency(project.get())) { + // double check the running apps in case there is a non-boot app running with live beans enabled + boolean nonBootLiveBeansAround = false; + for (SpringBootApp bootApp : runningBootApps) { + if (bootApp.providesNonBootLiveBeans()) { + nonBootLiveBeansAround = true; + break; + } + } + + if (!nonBootLiveBeansAround) { + return new CodeLens[0]; + } + } return server.getCompilationUnitCache().withCompilationUnit(project.get(), URI.create(document.getUri()), cu -> { Collection result = new LinkedHashSet<>();