diff --git a/headless-services/commons/commons-boot-app-cli/pom.xml b/headless-services/commons/commons-boot-app-cli/pom.xml
index a3dbddf3e..19690bda7 100644
--- a/headless-services/commons/commons-boot-app-cli/pom.xml
+++ b/headless-services/commons/commons-boot-app-cli/pom.xml
@@ -18,12 +18,6 @@
json
20160810
-
-
- com.fasterxml.jackson.core
- jackson-databind
- 2.8.8.1
-
commons-io
commons-io
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 33e8b8638..d71d82fa1 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) 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
@@ -12,6 +12,7 @@ package org.springframework.ide.vscode.commons.boot.app.cli;
import java.io.File;
import java.io.IOException;
+import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
@@ -31,17 +32,20 @@ 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.RequestMapping;
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.Log;
+import org.springframework.ide.vscode.commons.util.StringUtil;
-import com.fasterxml.jackson.databind.ObjectMapper;
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;
@@ -51,12 +55,29 @@ import com.sun.tools.attach.VirtualMachineDescriptor;
*/
public class SpringBootApp {
+ private static final String SPRINGFRAMEWORK_BOOT_DOMAIN = "org.springframework.boot";
+
+ private Logger logger = LoggerFactory.getLogger(SpringBootApp.class);
+
private VirtualMachine vm;
private VirtualMachineDescriptor vmd;
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:
+ // 1. Object Mapper serialises null, but not Gson by default
+ // 2. Object Mapper does not escape Html, whereas Gson does by default (for example
+ // '=' in Gson appears as '\u003d')
+ private Gson gson = new GsonBuilder()
+ .serializeNulls()
+ .disableHtmlEscaping()
+ .create();
+
private final Supplier jmxConnect = Suppliers.memoize(() -> {
String address = null;
@@ -69,7 +90,7 @@ public class SpringBootApp {
try {
address = vm.startLocalManagementAgent();
} catch (IOException e) {
- Log.log(e);
+ logger.error("Error starting local management agent", e);
}
}
return address;
@@ -90,7 +111,7 @@ public class SpringBootApp {
public SpringBootApp(VirtualMachineDescriptor vmd) throws AttachNotSupportedException, IOException {
this.vmd = vmd;
this.vm = VirtualMachine.attach(vmd);
- Log.info("SpringBootApp created: "+this);
+ logger.info("SpringBootApp created: "+this);
}
public String getProcessID() {
@@ -145,8 +166,7 @@ public class SpringBootApp {
public String getPort() throws Exception {
JMXConnector jmxConnector = null;
try {
- JMXServiceURL serviceUrl = new JMXServiceURL(jmxConnect.get());
- jmxConnector = JMXConnectorFactory.connect(serviceUrl, null);
+ jmxConnector = getJmxConnector();
return getPort(jmxConnector);
}
finally {
@@ -155,31 +175,31 @@ public class SpringBootApp {
}
public String getEnvironment() throws Exception {
- Object result = getActuatorDataFromAttribute("org.springframework.boot:type=Endpoint,name=environmentEndpoint", "Data");
+ Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=environmentEndpoint"), "Data");
if (result != null) {
- String environment = new ObjectMapper().writeValueAsString(result);
+ String environment = gson.toJson(result);
return environment;
}
- result = getActuatorDataFromOperation("org.springframework.boot:type=Endpoint,name=Env", "environment");
+ result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Env"), "environment");
if (result != null) {
- String environment = new ObjectMapper().writeValueAsString(result);
+ String environment = gson.toJson(result);
return environment;
}
return null;
}
- private String getBeansJson() throws Exception {
- Object result = getActuatorDataFromAttribute("org.springframework.boot:type=Endpoint,name=beansEndpoint", "Data");
+ private String getBeansFromActuator(String domain) throws Exception {
+ Object result = getActuatorDataFromAttribute(getObjectName(domain, "type=Endpoint,name=beansEndpoint"), "Data");
if (result != null) {
- String beans = new ObjectMapper().writeValueAsString(result);
+ String beans = gson.toJson(result);
return beans;
}
- result = getActuatorDataFromOperation("org.springframework.boot:type=Endpoint,name=Beans", "beans");
+ result = getActuatorDataFromOperation(getObjectName(domain, "type=Endpoint,name=Beans"), "beans");
if (result != null) {
- String beans = new ObjectMapper().writeValueAsString(result);
+ String beans = gson.toJson(result);
return beans;
}
@@ -188,10 +208,11 @@ public class SpringBootApp {
public LiveBeansModel getBeans() {
try {
- String json = getBeansJson();
+ String domain = getDomainForActuator();
+ String json = getBeansFromActuator(domain);
return LiveBeansModel.parse(json);
} catch (Exception e) {
- Log.log(e);
+ logger.error("Error parsing beans", e);
return LiveBeansModel.builder().build();
}
}
@@ -214,16 +235,16 @@ public class SpringBootApp {
public Collection getRequestMappings() throws Exception {
//Boot 1.x
- Object result = getActuatorDataFromAttribute("org.springframework.boot:type=Endpoint,name=requestMappingEndpoint", "Data");
+ Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=requestMappingEndpoint"), "Data");
if (result != null) {
- String mappings = new ObjectMapper().writeValueAsString(result);
+ String mappings = gson.toJson(result);
return parseRequestMappingsJson(mappings, "1.x");
}
//Boot 2.x
- result = getActuatorDataFromOperation("org.springframework.boot:type=Endpoint,name=Mappings", "mappings");
+ result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Mappings"), "mappings");
if (result != null) {
- String mappings = new ObjectMapper().writeValueAsString(result);
+ String mappings = gson.toJson(result);
return parseRequestMappingsJson(mappings, "2.x");
}
@@ -250,35 +271,35 @@ public class SpringBootApp {
private String getAutoConfigReport() throws Exception {
//Boot 1.x
- Object result = getActuatorDataFromAttribute("org.springframework.boot:type=Endpoint,name=autoConfigurationReportEndpoint", "Data");
+ Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=autoConfigurationReportEndpoint"), "Data");
if (result != null) {
- String report = new ObjectMapper().writeValueAsString(result);
+ String report = gson.toJson(result);
return report;
}
//Boot 2.x
- result = getActuatorDataFromOperation("org.springframework.boot:type=Endpoint,name=Conditions", "applicationConditionEvaluation");
+ result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Conditions"), "applicationConditionEvaluation");
if (result != null) {
- String report = new ObjectMapper().writeValueAsString(result);
+ String report = gson.toJson(result);
return report;
}
return null;
}
- protected Object getActuatorDataFromAttribute(String actuatorID, String attribute) throws Exception {
+ protected Object getActuatorDataFromAttribute(ObjectName objectName, String attribute) throws Exception {
JMXConnector jmxConnector = null;
try {
- JMXServiceURL serviceUrl = new JMXServiceURL(jmxConnect.get());
- jmxConnector = JMXConnectorFactory.connect(serviceUrl, null);
- MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+ if (objectName != null) {
+ jmxConnector = getJmxConnector();
+ MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
- try {
- ObjectName objectName = new ObjectName(actuatorID);
- Object result = connection.getAttribute(objectName, "Data");
- return result;
- }
- catch (InstanceNotFoundException e) {
+ try {
+ Object result = connection.getAttribute(objectName, "Data");
+ return result;
+ }
+ catch (InstanceNotFoundException e) {
+ }
}
return null;
}
@@ -287,19 +308,19 @@ public class SpringBootApp {
}
}
- protected Object getActuatorDataFromOperation(String actuatorID, String operation) throws Exception {
+ protected Object getActuatorDataFromOperation(ObjectName objectName, String operation) throws Exception {
JMXConnector jmxConnector = null;
try {
- JMXServiceURL serviceUrl = new JMXServiceURL(jmxConnect.get());
- jmxConnector = JMXConnectorFactory.connect(serviceUrl, null);
- MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
+ if (objectName != null) {
+ jmxConnector = getJmxConnector();
+ MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
- try {
- ObjectName objectName = new ObjectName(actuatorID);
- Object result = connection.invoke(objectName, operation, null, null);
- return result;
- }
- catch (InstanceNotFoundException e) {
+ try {
+ Object result = connection.invoke(objectName, operation, null, null);
+ return result;
+ }
+ catch (InstanceNotFoundException e) {
+ }
}
return null;
}
@@ -307,6 +328,11 @@ public class SpringBootApp {
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 boolean contains(String[] cpElements, String element) {
for (String cpElement : cpElements) {
@@ -415,6 +441,69 @@ public class SpringBootApp {
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
+ *
+ * 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()+"`]";
@@ -462,14 +551,14 @@ public class SpringBootApp {
}
}
} catch (Exception e) {
- Log.log(e);
+ logger.error("error resolving profiles from env", e);
}
return null;
}
public void dispose() {
if (vm!=null) {
- Log.info("SpringBootApp disposed: "+this);
+ logger.info("SpringBootApp disposed: "+this);
try {
vm.detach();
} catch (Exception e) {