Partially working remote app live hover

Uses a single-hard-coded jmx url to connect to remote
and fetch actuator data.
This commit is contained in:
Kris De Volder
2018-07-11 16:49:52 -07:00
parent e1c0953f4f
commit a81a54f391
12 changed files with 741 additions and 465 deletions

View File

@@ -0,0 +1,493 @@
/*******************************************************************************
* 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.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.PlatformManagedObject;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.TimeUnit;
import javax.management.InstanceNotFoundException;
import javax.management.JMX;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.Boot1xRequestMapping;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMappingsParser20;
import org.springframework.ide.vscode.commons.util.FuctionWithException;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.StringUtil;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* A abstract base class which attempts to capture commonalities between
* Local and Remote connections to SpringBootApp using JMX.
*/
public abstract class AbstractSpringBootApp implements SpringBootApp {
private static final String SPRINGFRAMEWORK_BOOT_DOMAIN = "org.springframework.boot";
protected static Logger logger = LoggerFactory.getLogger(SpringBootApp.class);
private String jmxMbeanActuatorDomain;
// NOTE: Gson-based serialisation replaces the old Jackson ObjectMapper. Not sure if this makes a difference in the long run, but to retain the same output that Jackson Object Mapper
// was generating during serialisatino, some configuration in Gson is required, as the default behaviour of Gson is different than Object Mapper.
// Namely: Object Mapper does not escape Html, whereas Gson does by default (for example
// '=' in Gson appears as '\u003d')
protected final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.create();
protected abstract JMXServiceURL getJmxUrl() throws MalformedURLException;
@Override
public abstract Properties getSystemProperties() throws Exception;
@Override
public abstract String getProcessID();
@Override
public abstract String getProcessName() throws Exception;
@Override
public abstract boolean isSpringBootApp();
@Override
public JMXConnector getJmxConnector() throws MalformedURLException, IOException {
JMXServiceURL serviceUrl = getJmxUrl();
JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, null);
return jmxConnector;
}
@Override
public final List<String> getActiveProfiles() {
try {
String _env = getEnvironment();
if (_env != null) {
JSONObject env = new JSONObject(_env);
Object _profiles = env.opt("activeProfiles"); //Boot 2.0
if (_profiles==null) {
_profiles = env.opt("profiles"); //Boot 1.5
}
if (_profiles instanceof JSONArray) {
JSONArray profiles = (JSONArray) _profiles;
ImmutableList.Builder<String> list = ImmutableList.builder();
for (Object object : profiles) {
if (object instanceof String) {
list.add((String) object);
}
}
return list.build();
}
}
} catch (Exception e) {
logger.error("error resolving profiles from env", e);
}
return null;
}
@Override
public Collection<RequestMapping> getRequestMappings() throws Exception {
//Boot 1.x
Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=requestMappingEndpoint"), "Data");
if (result != null) {
String mappings = gson.toJson(result);
return parseRequestMappingsJson(mappings, "1.x");
}
//Boot 2.x
result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Mappings"), "mappings");
if (result != null) {
String mappings = gson.toJson(result);
return parseRequestMappingsJson(mappings, "2.x");
}
return null;
}
public static Collection<RequestMapping> parseRequestMappingsJson(String json, String bootVersion) {
JSONObject obj = new JSONObject(json);
if (bootVersion.equals("2.x")) {
return RequestMappingsParser20.parse(obj);
} else { //1.x
List<RequestMapping> result = new ArrayList<>();
Iterator<String> keys = obj.keys();
while (keys.hasNext()) {
String rawKey = keys.next();
JSONObject value = obj.getJSONObject(rawKey);
result.add(new Boot1xRequestMapping(rawKey, value));
}
return result;
}
}
protected ObjectName getObjectName(String keyProperties) throws Exception {
String domain = getDomainForActuator();
return getObjectName(domain, keyProperties);
}
protected ObjectName getObjectName(String domain, String keyProperties) throws Exception {
if (StringUtil.hasText(domain) && StringUtil.hasText(keyProperties)) {
String fullName = domain + ":" + keyProperties;
return ObjectName.getInstance(fullName);
}
return null;
}
@Override
public final LiveBeansModel getBeans() {
try {
String domain = getDomainForActuator();
String json = getBeansFromActuator(domain);
LiveBeansModel beans = LiveBeansModel.parse(json);
logger.info("Got {} beans for {}", beans.getBeanNames().size(), this);
return beans;
} catch (Exception e) {
logger.error("Error parsing beans", e);
return LiveBeansModel.builder().build();
}
}
private String getBeansFromActuator(String domain) throws Exception {
Object result = getActuatorDataFromAttribute(getObjectName(domain, "type=Endpoint,name=beansEndpoint"), "Data");
if (result != null) {
String beans = gson.toJson(result);
return beans;
}
result = getActuatorDataFromOperation(getObjectName(domain, "type=Endpoint,name=Beans"), "beans");
if (result != null) {
String beans = gson.toJson(result);
return beans;
}
return null;
}
/**
* PT 156072399: Actuator information can be defined using a different JMX MBean domain.
* By default, Spring Boot exposes management endpoints as JMX MBeans under the 'org.springframework.boot' domain.
* Users can however define another domain in the app's application.properties, for example using this property:
* management.endpoints.jmx.domain=com.example.myapp
* <b/>
* Therefore we need to support other domains than just: 'org.springframework.boot'
* @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 {
if (this.jmxMbeanActuatorDomain == null) {
JMXConnector jmxConnector = null;
try {
jmxConnector = getJmxConnector();
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
// To be more efficient in finding the domain containing actuator information,
// and avoid many JMX connections
// first check the default springframework boot domain:
String beansJson = getBeansFromActuator(SPRINGFRAMEWORK_BOOT_DOMAIN);
if (StringUtil.hasText(beansJson)) {
this.jmxMbeanActuatorDomain = SPRINGFRAMEWORK_BOOT_DOMAIN;
}
if (this.jmxMbeanActuatorDomain == null) {
String[] domains = connection.getDomains();
if (domains != null) {
for (String domain : domains) {
// we already checked default boot domain, no need to check it again
// Note that default spring boot domain may still appear even if another
// domain contains actuator Beans (for example, "Admin" will be under default spring framework domain)
if (!SPRINGFRAMEWORK_BOOT_DOMAIN.equals(domain)) {
beansJson = getBeansFromActuator(domain);
if (StringUtil.hasText(beansJson)) {
this.jmxMbeanActuatorDomain = domain;
break;
}
}
}
}
}
} finally {
if (jmxConnector != null) {
jmxConnector.close();
}
}
}
return this.jmxMbeanActuatorDomain;
}
protected <T extends PlatformManagedObject,R> R withPlatformMxBean(Class<T> mbeanType, FuctionWithException<T,R> doit) throws Exception {
JMXConnector jmxConnector = null;
try {
jmxConnector = getJmxConnector();
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
T proxy = ManagementFactory.getPlatformMXBean(connection, mbeanType);
return doit.apply(proxy);
} finally {
if (jmxConnector != null) jmxConnector.close();
}
}
protected final Object getActuatorDataFromAttribute(ObjectName objectName, String attribute) throws Exception {
JMXConnector jmxConnector = null;
try {
if (objectName != null) {
jmxConnector = getJmxConnector();
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
try {
Object result = connection.getAttribute(objectName, "Data");
return result;
}
catch (InstanceNotFoundException e) {
}
}
return null;
}
finally {
if (jmxConnector != null) jmxConnector.close();
}
}
protected final Object getActuatorDataFromOperation(ObjectName objectName, String operation) throws Exception {
JMXConnector jmxConnector = null;
try {
if (objectName != null) {
jmxConnector = getJmxConnector();
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
try {
Object result = connection.invoke(objectName, operation, null, null);
return result;
}
catch (InstanceNotFoundException e) {
}
}
return null;
}
finally {
if (jmxConnector != null) jmxConnector.close();
}
}
@Override
public String getEnvironment() throws Exception {
Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=environmentEndpoint"), "Data");
if (result != null) {
String environment = gson.toJson(result);
return environment;
}
result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Env"), "environment");
if (result != null) {
String environment = gson.toJson(result);
return environment;
}
return null;
}
@Override
public String[] getClasspath() throws Exception {
Properties props = getSystemProperties();
String classpath = (String) props.get("java.class.path");
String[] cpElements = splitClasspath(classpath);
return cpElements;
}
private String[] splitClasspath(String classpath) {
List<String> classpathElements = new ArrayList<>();
if (classpath != null) {
StringTokenizer tokenizer = new StringTokenizer(classpath, File.pathSeparator);
while (tokenizer.hasMoreTokens()) {
String classpathElement = tokenizer.nextToken();
classpathElements.add(classpathElement);
}
}
return classpathElements.toArray(new String[classpathElements.size()]);
}
@Override
public String getJavaCommand() throws Exception {
Properties props = getSystemProperties();
System.err.println(">>>>> sysprops");
for (Entry<Object, Object> e : props.entrySet()) {
System.err.println(e.getKey() +"="+e.getValue());
}
System.err.println("<<<< sysprops");
return (String) props.get("sun.java.command");
}
@Override
public String getHost() throws Exception {
//TODO: different implementation for cf apps with locally tunnelled
// jmx connection?
JMXServiceURL serviceUrl = getJmxUrl();
return serviceUrl.getHost();
}
@Override
public Optional<List<LiveConditional>> getLiveConditionals() throws Exception {
return getLiveConditionals(getAutoConfigReport(), getProcessID(), getProcessName());
}
/**
* Publicly visible so that it can be tested via a mock app
*
* @param autoConfigReport
* @param processId
* @param processName
* @return
* @throws Exception
*/
public static Optional<List<LiveConditional>> getLiveConditionals(String autoConfigReport, String processId,
String processName) {
return LiveConditionalParser.parse(autoConfigReport, processId, processName);
}
private String getAutoConfigReport() throws Exception {
//Boot 1.x
Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=autoConfigurationReportEndpoint"), "Data");
if (result != null) {
String report = gson.toJson(result);
return report;
}
//Boot 2.x
result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Conditions"), "applicationConditionEvaluation");
if (result != null) {
String report = gson.toJson(result);
return report;
}
return null;
}
@Override
public String getPort() throws Exception {
JMXConnector jmxConnector = null;
try {
jmxConnector = getJmxConnector();
return getPort(jmxConnector);
}
finally {
if (jmxConnector != null) jmxConnector.close();
}
}
protected String getPort(JMXConnector jmxConnector) throws Exception {
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
String port = getPortViaAdmin(connection);
if (port != null) {
return port;
}
port = getPortViaActuator(connection);
if (port != null) {
return port;
}
port = getPortViaTomcatBean(connection);
return port;
}
protected String getPortViaAdmin(MBeanServerConnection connection) throws Exception {
try {
String DEFAULT_OBJECT_NAME = "org.springframework.boot:type=Admin,name=SpringApplication";
ObjectName objectName = new ObjectName(DEFAULT_OBJECT_NAME);
Object o = connection.invoke(objectName,"getProperty", new String[] {"local.server.port"}, new String[] {String.class.getName()});
return o.toString();
}
catch (InstanceNotFoundException e) {
return null;
}
}
protected String getPortViaActuator(MBeanServerConnection connection) throws Exception {
String environment = getEnvironment();
if (environment != null) {
JSONObject env = new JSONObject(environment);
if (env != null) {
JSONObject portsObject = env.optJSONObject("server.ports");
if (portsObject != null) {
String portValue = portsObject.optString("local.server.port");
if (portValue!=null) {
return portValue;
}
}
//Not found as direct property value... in Boot 2.0 we must look inside the 'propertySources'.
//Similar... but structure is more complex.
JSONArray propertySources = env.optJSONArray("propertySources");
if (propertySources!=null) {
for (Object _source : propertySources) {
if (_source instanceof JSONObject) {
JSONObject source = (JSONObject) _source;
String sourceName = source.optString("name");
if ("server.ports".equals(sourceName)) {
JSONObject props = source.optJSONObject("properties");
JSONObject valueObject = props.optJSONObject("local.server.port");
if (valueObject!=null) {
String portValue = valueObject.optString("value");
if (portValue!=null) {
return portValue;
}
}
}
}
}
}
}
}
return null;
}
protected String getPortViaTomcatBean(MBeanServerConnection connection) throws Exception {
try {
Set<ObjectName> queryNames = connection.queryNames(null, null);
for (ObjectName objectName : queryNames) {
if (objectName.toString().startsWith("Tomcat") && objectName.toString().contains("type=Connector")) {
Object result = connection.getAttribute(objectName, "localPort");
if (result != null) {
return result.toString();
}
}
}
}
catch (InstanceNotFoundException e) {
}
return null;
}
}

View File

@@ -15,7 +15,6 @@ import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
@@ -35,17 +34,10 @@ import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.Boot1xRequestMapping;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMappingsParser20;
import org.springframework.ide.vscode.commons.util.CollectorUtil;
import org.springframework.ide.vscode.commons.util.StringUtil;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
@@ -53,11 +45,9 @@ import com.sun.tools.attach.VirtualMachineDescriptor;
/**
* @author Martin Lippert
*/
public class LocalSpringBootApp extends SpringBootApp {
public class LocalSpringBootApp extends AbstractSpringBootApp {
private static final String SPRINGFRAMEWORK_BOOT_DOMAIN = "org.springframework.boot";
private Logger logger = LoggerFactory.getLogger(LocalSpringBootApp.class);
private static final Logger logger = LoggerFactory.getLogger(LocalSpringBootApp.class);
private VirtualMachine vm;
private VirtualMachineDescriptor vmd;
@@ -65,16 +55,6 @@ public class LocalSpringBootApp extends SpringBootApp {
private static final String LOCAL_CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";
private Boolean isSpringBootApp;
private String jmxMbeanActuatorDomain;
// NOTE: Gson-based serialisation replaces the old Jackson ObjectMapper. Not sure if this makes a difference in the long run, but to retain the same output that Jackson Object Mapper
// was generating during serialisatino, some configuration in Gson is required, as the default behaviour of Gson is different than Object Mapper.
// Namely: Object Mapper does not escape Html, whereas Gson does by default (for example
// '=' in Gson appears as '\u003d')
private Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.create();
private final Supplier<String> jmxConnect = Suppliers.memoize(() -> {
String address = null;
@@ -122,12 +102,6 @@ public class LocalSpringBootApp extends SpringBootApp {
return vmd.displayName();
}
@Override
public String getHost() throws Exception {
JMXServiceURL serviceUrl = new JMXServiceURL(jmxConnect.get());
return serviceUrl.getHost();
}
@Override
public boolean isSpringBootApp() {
if (isSpringBootApp==null) {
@@ -148,207 +122,27 @@ public class LocalSpringBootApp extends SpringBootApp {
}
private boolean isSpringBootAppSysprops() throws IOException {
Properties sysprops = this.vm.getSystemProperties();
Properties sysprops = getSystemProperties();
return "org.springframework.boot.loader".equals(sysprops.getProperty("java.protocol.handler.pkgs"));
}
private boolean isSpringBootAppClasspath() throws IOException {
private boolean isSpringBootAppClasspath() throws Exception {
return contains(getClasspath(), "spring-boot");
}
@Override
public String[] getClasspath() throws IOException {
Properties props = this.vm.getSystemProperties();
String classpath = (String) props.get("java.class.path");
String[] cpElements = splitClasspath(classpath);
return cpElements;
}
@Override
public String getJavaCommand() throws IOException {
Properties props = this.vm.getSystemProperties();
return (String) props.get("sun.java.command");
public Properties getSystemProperties() throws IOException {
return this.vm.getSystemProperties();
}
public boolean containsSystemProperty(Object key) throws IOException {
Properties props = this.vm.getSystemProperties();
Properties props = getSystemProperties();
return props.containsKey(key);
}
@Override
public String getPort() throws Exception {
JMXConnector jmxConnector = null;
try {
jmxConnector = getJmxConnector();
return getPort(jmxConnector);
}
finally {
if (jmxConnector != null) jmxConnector.close();
}
}
@Override
public String getEnvironment() throws Exception {
Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=environmentEndpoint"), "Data");
if (result != null) {
String environment = gson.toJson(result);
return environment;
}
result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Env"), "environment");
if (result != null) {
String environment = gson.toJson(result);
return environment;
}
return null;
}
private String getBeansFromActuator(String domain) throws Exception {
Object result = getActuatorDataFromAttribute(getObjectName(domain, "type=Endpoint,name=beansEndpoint"), "Data");
if (result != null) {
String beans = gson.toJson(result);
return beans;
}
result = getActuatorDataFromOperation(getObjectName(domain, "type=Endpoint,name=Beans"), "beans");
if (result != null) {
String beans = gson.toJson(result);
return beans;
}
return null;
}
@Override
public LiveBeansModel getBeans() {
try {
String domain = getDomainForActuator();
String json = getBeansFromActuator(domain);
return LiveBeansModel.parse(json);
} catch (Exception e) {
logger.error("Error parsing beans", e);
return LiveBeansModel.builder().build();
}
}
public static Collection<RequestMapping> parseRequestMappingsJson(String json, String bootVersion) {
JSONObject obj = new JSONObject(json);
if (bootVersion.equals("2.x")) {
return RequestMappingsParser20.parse(obj);
} else { //1.x
List<RequestMapping> result = new ArrayList<>();
Iterator<String> keys = obj.keys();
while (keys.hasNext()) {
String rawKey = keys.next();
JSONObject value = obj.getJSONObject(rawKey);
result.add(new Boot1xRequestMapping(rawKey, value));
}
return result;
}
}
@Override
public Collection<RequestMapping> getRequestMappings() throws Exception {
//Boot 1.x
Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=requestMappingEndpoint"), "Data");
if (result != null) {
String mappings = gson.toJson(result);
return parseRequestMappingsJson(mappings, "1.x");
}
//Boot 2.x
result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Mappings"), "mappings");
if (result != null) {
String mappings = gson.toJson(result);
return parseRequestMappingsJson(mappings, "2.x");
}
return null;
}
@Override
public Optional<List<LiveConditional>> getLiveConditionals() throws Exception {
return getLiveConditionals(getAutoConfigReport(), getProcessID(), getProcessName());
}
/**
* Publicly visible so that it can be tested via a mock app
*
* @param autoConfigReport
* @param processId
* @param processName
* @return
* @throws Exception
*/
public static Optional<List<LiveConditional>> getLiveConditionals(String autoConfigReport, String processId,
String processName) {
return LiveConditionalParser.parse(autoConfigReport, processId, processName);
}
private String getAutoConfigReport() throws Exception {
//Boot 1.x
Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=autoConfigurationReportEndpoint"), "Data");
if (result != null) {
String report = gson.toJson(result);
return report;
}
//Boot 2.x
result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Conditions"), "applicationConditionEvaluation");
if (result != null) {
String report = gson.toJson(result);
return report;
}
return null;
}
protected Object getActuatorDataFromAttribute(ObjectName objectName, String attribute) throws Exception {
JMXConnector jmxConnector = null;
try {
if (objectName != null) {
jmxConnector = getJmxConnector();
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
try {
Object result = connection.getAttribute(objectName, "Data");
return result;
}
catch (InstanceNotFoundException e) {
}
}
return null;
}
finally {
if (jmxConnector != null) jmxConnector.close();
}
}
protected Object getActuatorDataFromOperation(ObjectName objectName, String operation) throws Exception {
JMXConnector jmxConnector = null;
try {
if (objectName != null) {
jmxConnector = getJmxConnector();
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
try {
Object result = connection.invoke(objectName, operation, null, null);
return result;
}
catch (InstanceNotFoundException e) {
}
}
return null;
}
finally {
if (jmxConnector != null) jmxConnector.close();
}
}
protected JMXConnector getJmxConnector() throws MalformedURLException, IOException {
JMXServiceURL serviceUrl = new JMXServiceURL(jmxConnect.get());
JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, null);
return jmxConnector;
protected JMXServiceURL getJmxUrl() throws MalformedURLException {
return new JMXServiceURL(jmxConnect.get());
}
protected boolean contains(String[] cpElements, String element) {
@@ -360,170 +154,9 @@ public class LocalSpringBootApp extends SpringBootApp {
return false;
}
protected String[] splitClasspath(String classpath) {
List<String> classpathElements = new ArrayList<>();
if (classpath != null) {
StringTokenizer tokenizer = new StringTokenizer(classpath, File.pathSeparator);
while (tokenizer.hasMoreTokens()) {
String classpathElement = tokenizer.nextToken();
classpathElements.add(classpathElement);
}
}
return classpathElements.toArray(new String[classpathElements.size()]);
}
protected String getPort(JMXConnector jmxConnector) throws Exception {
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
String port = getPortViaAdmin(connection);
if (port != null) {
return port;
}
port = getPortViaActuator(connection);
if (port != null) {
return port;
}
port = getPortViaTomcatBean(connection);
return port;
}
protected String getPortViaAdmin(MBeanServerConnection connection) throws Exception {
try {
String DEFAULT_OBJECT_NAME = "org.springframework.boot:type=Admin,name=SpringApplication";
ObjectName objectName = new ObjectName(DEFAULT_OBJECT_NAME);
Object o = connection.invoke(objectName,"getProperty", new String[] {"local.server.port"}, new String[] {String.class.getName()});
return o.toString();
}
catch (InstanceNotFoundException e) {
return null;
}
}
protected String getPortViaActuator(MBeanServerConnection connection) throws Exception {
String environment = getEnvironment();
if (environment != null) {
JSONObject env = new JSONObject(environment);
if (env != null) {
JSONObject portsObject = env.optJSONObject("server.ports");
if (portsObject != null) {
String portValue = portsObject.optString("local.server.port");
if (portValue!=null) {
return portValue;
}
}
//Not found as direct property value... in Boot 2.0 we must look inside the 'propertySources'.
//Similar... but structure is more complex.
JSONArray propertySources = env.optJSONArray("propertySources");
if (propertySources!=null) {
for (Object _source : propertySources) {
if (_source instanceof JSONObject) {
JSONObject source = (JSONObject) _source;
String sourceName = source.optString("name");
if ("server.ports".equals(sourceName)) {
JSONObject props = source.optJSONObject("properties");
JSONObject valueObject = props.optJSONObject("local.server.port");
if (valueObject!=null) {
String portValue = valueObject.optString("value");
if (portValue!=null) {
return portValue;
}
}
}
}
}
}
}
}
return null;
}
protected String getPortViaTomcatBean(MBeanServerConnection connection) throws Exception {
try {
Set<ObjectName> queryNames = connection.queryNames(null, null);
for (ObjectName objectName : queryNames) {
if (objectName.toString().startsWith("Tomcat") && objectName.toString().contains("type=Connector")) {
Object result = connection.getAttribute(objectName, "localPort");
if (result != null) {
return result.toString();
}
}
}
}
catch (InstanceNotFoundException e) {
}
return null;
}
protected ObjectName getObjectName(String keyProperties) throws Exception {
String domain = getDomainForActuator();
return getObjectName(domain, keyProperties);
}
protected ObjectName getObjectName(String domain, String keyProperties) throws Exception {
if (StringUtil.hasText(domain) && StringUtil.hasText(keyProperties)) {
String fullName = domain + ":" + keyProperties;
return ObjectName.getInstance(fullName);
}
return null;
}
/**
* PT 156072399: Actuator information can be defined using a different JMX MBean domain.
* By default, Spring Boot exposes management endpoints as JMX MBeans under the 'org.springframework.boot' domain.
* Users can however define another domain in the app's application.properties, for example using this property:
* management.endpoints.jmx.domain=com.example.myapp
* <b/>
* Therefore we need to support other domains than just: 'org.springframework.boot'
* @return JMX MBean domain containing actuator information, or null if not resolved.
* @throws Exception when resolving domain from JMX
*/
protected String getDomainForActuator() throws Exception {
if (this.jmxMbeanActuatorDomain == null) {
JMXConnector jmxConnector = null;
try {
jmxConnector = getJmxConnector();
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
// To be more efficient in finding the domain containing actuator information,
// and avoid many JMX connections
// first check the default springframework boot domain:
String beansJson = getBeansFromActuator(SPRINGFRAMEWORK_BOOT_DOMAIN);
if (StringUtil.hasText(beansJson)) {
this.jmxMbeanActuatorDomain = SPRINGFRAMEWORK_BOOT_DOMAIN;
}
if (this.jmxMbeanActuatorDomain == null) {
String[] domains = connection.getDomains();
if (domains != null) {
for (String domain : domains) {
// we already checked default boot domain, no need to check it again
// Note that default spring boot domain may still appear even if another
// domain contains actuator Beans (for example, "Admin" will be under default spring framework domain)
if (!SPRINGFRAMEWORK_BOOT_DOMAIN.equals(domain)) {
beansJson = getBeansFromActuator(domain);
if (StringUtil.hasText(beansJson)) {
this.jmxMbeanActuatorDomain = domain;
break;
}
}
}
}
}
} finally {
if (jmxConnector != null) {
jmxConnector.close();
}
}
}
return this.jmxMbeanActuatorDomain;
}
@Override
public String toString() {
return "Process [id=" +getProcessID() + ", name=`"+getProcessName()+"`]";
return "LocalSpringBootApp [id=" +getProcessID() + ", name=`"+getProcessName()+"`]";
}
/**
@@ -547,33 +180,6 @@ public class LocalSpringBootApp extends SpringBootApp {
System.out.println("}");
}
@Override
public List<String> getActiveProfiles() {
try {
String _env = getEnvironment();
if (_env != null) {
JSONObject env = new JSONObject(_env);
Object _profiles = env.opt("activeProfiles"); //Boot 2.0
if (_profiles==null) {
_profiles = env.opt("profiles"); //Boot 1.5
}
if (_profiles instanceof JSONArray) {
JSONArray profiles = (JSONArray) _profiles;
ImmutableList.Builder<String> list = ImmutableList.builder();
for (Object object : profiles) {
if (object instanceof String) {
list.add((String) object);
}
}
return list.build();
}
}
} catch (Exception e) {
logger.error("error resolving profiles from env", e);
}
return null;
}
public void dispose() {
if (vm!=null) {
logger.info("SpringBootApp disposed: "+this);

View File

@@ -0,0 +1,45 @@
/*******************************************************************************
* 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<String, Object> 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[] {});
});
}
}

View File

@@ -0,0 +1,88 @@
/*******************************************************************************
* 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.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;
import java.util.Properties;
import javax.management.remote.JMXServiceURL;
public class RemoteSpringBootApp extends AbstractSpringBootApp {
private String jmxUrl;
private RemoteSpringBootApp(String jmxUrl) {
this.jmxUrl = jmxUrl;
}
@Override
protected JMXServiceURL getJmxUrl() throws MalformedURLException {
return new JMXServiceURL(jmxUrl);
}
@Override
public Properties getSystemProperties() throws Exception {
return withPlatformMxBean(RuntimeMXBean.class, runtime -> {
Properties props = new Properties();
for (Entry<String, String> e : runtime.getSystemProperties().entrySet()) {
props.put(e.getKey(), e.getValue());
}
return props;
});
}
@Override
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.
return true;
}
@Override
public String getProcessID() {
try {
return withPlatformMxBean(RuntimeMXBean.class, runtime -> runtime.getName());
} catch (Exception e) {
return "Unknown-PID";
}
}
@Override
public String getProcessName() throws Exception {
try {
String command = getJavaCommand();
if (command!=null) {
int space = command.indexOf(' ');
if (space>=0) {
command = command.substring(0, space);
}
command = command.trim();
if (!"".equals(command)) {
return command;
}
}
} catch (IOException e) {
logger.equals(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)));
}
}

View File

@@ -1,42 +1,33 @@
/*******************************************************************************
* 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.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;
/**
* A abstract base class which attempts to capture commonalities between
* Local and Remote connections to SpringBootApp using JMX.
*/
public abstract class SpringBootApp {
public interface SpringBootApp {
public abstract String getProcessID();
public abstract String getProcessName();
public abstract boolean isSpringBootApp();
public abstract String[] getClasspath() throws IOException;
public abstract String getJavaCommand() throws IOException;
public abstract String getHost() throws Exception;
public abstract String getPort() throws Exception;
public abstract Optional<List<LiveConditional>> getLiveConditionals() throws Exception;
public abstract Collection<RequestMapping> getRequestMappings() throws Exception;
public abstract LiveBeansModel getBeans();
public abstract List<String> getActiveProfiles();
public abstract String getEnvironment() throws Exception;
String[] getClasspath() throws Exception;
String getJavaCommand() throws Exception;
String getProcessName() throws Exception;
String getProcessID();
String getHost() throws Exception;
String getPort() throws Exception;
boolean isSpringBootApp();
String getEnvironment() throws Exception;
Collection<RequestMapping> getRequestMappings() throws Exception;
LiveBeansModel getBeans();
List<String> getActiveProfiles();
Optional<List<LiveConditional>> getLiveConditionals() throws Exception;
Properties getSystemProperties() throws Exception;
JMXConnector getJmxConnector() throws MalformedURLException, IOException;
}

View File

@@ -0,0 +1,16 @@
/*******************************************************************************
* 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;
@FunctionalInterface
public interface FuctionWithException<I, O> {
O apply(I in) throws Exception;
}

View File

@@ -29,6 +29,8 @@ import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareLookup;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
@@ -51,6 +53,8 @@ import com.google.common.collect.ImmutableList;
*/
public class BootJavaHoverProvider implements HoverHandler {
private static Logger logger = LoggerFactory.getLogger(BootJavaHoverProvider.class);
private JavaProjectFinder projectFinder;
private BootJavaLanguageServerComponents server;
private AnnotationHierarchyAwareLookup<HoverProvider> hoverProviders;
@@ -220,14 +224,17 @@ public class BootJavaHoverProvider implements HoverHandler {
private Hover provideHoverForAnnotation(ASTNode exactNode, Annotation annotation, int offset, TextDocument doc, IJavaProject project) {
ITypeBinding type = annotation.resolveTypeBinding();
if (type != null) {
logger.info("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) {
logger.info("Hover found: "+hover);
//TODO: compose multiple hovers somehow instead of just returning the first one?
return hover;
}
logger.info("NO Hover!");
}
//Only reaching here if we didn't get a hover.
if (!hasActuatorDependency(project)) {

View File

@@ -14,14 +14,40 @@ import java.util.Collection;
import org.springframework.ide.vscode.commons.boot.app.cli.LocalSpringBootApp;
import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp;
import org.springframework.ide.vscode.commons.boot.app.cli.RemoteSpringBootApp;
import com.google.common.collect.ImmutableList;
public interface RunningAppProvider {
public static final RunningAppProvider LOCAL_APPS = LocalSpringBootApp::getAllRunningSpringApps;
public static final SpringBootApp HARDCODED_REMOTE_APP = //TODO: proper configuration of remote apps instead of this hard-coded stuff which is just for testing.
RemoteSpringBootApp.create("service:jmx:rmi://localhost:9111/jndi/rmi://localhost:9111/jmxrmi");
static RunningAppProvider composite(RunningAppProvider... children) {
if (children.length==1) {
return children[0];
}
return () -> {
ImmutableList.Builder<SpringBootApp> allApps = ImmutableList.builder();
for (RunningAppProvider c : children) {
Collection<SpringBootApp> moreApps = c.getAllRunningSpringApps();
if (moreApps!=null) {
allApps.addAll(moreApps);
}
}
return allApps.build();
};
}
public static final RunningAppProvider LOCAL_APPS = LocalSpringBootApp::getAllRunningSpringApps;
public static final RunningAppProvider REMOTE_APPS = () -> ImmutableList.of(
HARDCODED_REMOTE_APP
);
public static final RunningAppProvider DEFAULT = composite(
LOCAL_APPS
// REMOTE_APPS
);
public static final RunningAppProvider DEFAULT = LocalSpringBootApp::getAllRunningSpringApps;
public static final RunningAppProvider NULL = () -> ImmutableList.of();
Collection<SpringBootApp> getAllRunningSpringApps() throws Exception;

View File

@@ -87,7 +87,12 @@ public class LiveHoverUtils {
}
public static String niceAppName(SpringBootApp app) {
return niceAppName(app.getProcessID(), app.getProcessName());
try {
return niceAppName(app.getProcessID(), app.getProcessName());
} catch (Exception e) {
e.printStackTrace();
return app.toString();
}
}
public static String niceAppName(String processId, String processName) {

View File

@@ -23,25 +23,25 @@ import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp;
* @author Martin Lippert
*/
public class ChangeDetectionHistory {
private Map<String, ChangeHistory> changeHistory;
public ChangeDetectionHistory() {
this.changeHistory = new HashMap<>();
}
public Change[] checkForChanges(SpringBootApp[] runningApps) {
List<Change> result = null;
for (SpringBootApp runningApp : runningApps) {
String virtualID = getVirtualAppID(runningApp);
if (changeHistory.containsKey(virtualID)) {
// the standard case
ChangeHistory appHistory = changeHistory.get(virtualID);
appHistory.updateProcess(runningApp);
Change change = appHistory.checkForUpdates();
Change change = appHistory.checkForUpdates();
if (change != null) {
if (result == null) {
result = new ArrayList<>();
@@ -55,8 +55,8 @@ public class ChangeDetectionHistory {
ChangeHistory oldHistory = changeHistory.remove(oldAppID);
oldHistory.updateProcess(runningApp);
changeHistory.put(virtualID, oldHistory);
Change change = oldHistory.checkForUpdates();
Change change = oldHistory.checkForUpdates();
if (change != null) {
if (result == null) {
result = new ArrayList<>();
@@ -68,12 +68,12 @@ public class ChangeDetectionHistory {
ChangeHistory newHistory = new ChangeHistory();
newHistory.updateProcess(runningApp);
changeHistory.put(virtualID, newHistory);
newHistory.checkForUpdates();
}
}
}
if (result != null) {
return (Change[]) result.toArray(new Change[result.size()]);
}
@@ -85,10 +85,10 @@ public class ChangeDetectionHistory {
private String getVirtualAppID(SpringBootApp app) {
return app.getProcessID();
}
private String getOldApp(SpringBootApp app, SpringBootApp[] allApps) {
Set<String> histories = this.changeHistory.keySet();
try {
String commandLine = app.getJavaCommand();
String[] classpath = app.getClasspath();
@@ -100,10 +100,10 @@ public class ChangeDetectionHistory {
}
}
}
catch (IOException e) {
catch (Exception e) {
e.printStackTrace();
}
return null;
}
@@ -119,7 +119,7 @@ public class ChangeDetectionHistory {
catch (Exception e) {
e.printStackTrace();
}
return true;
}

View File

@@ -10,7 +10,6 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -37,21 +36,21 @@ public class ChangeHistory {
public ChangeHistory() {
}
public void updateProcess(SpringBootApp app) {
if (this.associatedProcess != app) {
this.associatedProcess = app;
try {
this.associatedProcessCommand = app.getJavaCommand();
this.associatedProcessClasspath = app.getClasspath();
}
catch (IOException e) {
catch (Exception e) {
e.printStackTrace();
}
}
}
public boolean matchesProcess(String commandLine, String[] classpath) {
return this.associatedProcessCommand != null && this.associatedProcessCommand.equals(commandLine)
&& this.associatedProcessClasspath != null && Arrays.deepEquals(this.associatedProcessClasspath, classpath);
@@ -71,7 +70,7 @@ public class ChangeHistory {
lastBeans = currentBeans;
}
}
return result;
}
@@ -79,20 +78,20 @@ public class ChangeHistory {
if (previous == current) {
return result;
}
Set<String> currentNames = current.getBeanNames();
Set<String> previousNames = previous.getBeanNames();
Set<String> allNames = new HashSet<>(currentNames);
allNames.addAll(previousNames);
for (String name : allNames) {
List<LiveBean> currentBeans = current.getBeansOfName(name);
List<LiveBean> previousBeans = previous.getBeansOfName(name);
result = calculateBeansDiff(previousBeans, currentBeans, result);
}
return result;
}
@@ -110,10 +109,10 @@ public class ChangeHistory {
result.addDeletedBean(bean);
}
}
for (LiveBean bean : currentBeans) {
if (!contains(previousBeans, bean)) {
if (result == null) {
result = new Change(associatedProcess);
}
@@ -121,7 +120,7 @@ public class ChangeHistory {
result.addNewBean(bean);
}
}
return result;
}
@@ -133,7 +132,7 @@ public class ChangeHistory {
return true;
}
}
return false;
}

View File

@@ -85,7 +85,7 @@ public class MockRunningAppProvider {
return this;
}
public MockAppBuilder processName(String name) {
public MockAppBuilder processName(String name) throws Exception {
this.processName = name;
when(app.getProcessName()).thenReturn(name);
return this;