/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.jmx;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
import org.springframework.boot.actuate.endpoint.LoggersEndpoint;
import org.springframework.boot.logging.logback.LogbackLoggingSystem;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.jmx.support.ObjectNameManager;
import org.springframework.util.ObjectUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage;
/**
* Tests for {@link EndpointMBeanExporter}
*
* @author Christian Dupuis
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
public class EndpointMBeanExporterTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
GenericApplicationContext context = null;
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void testRegistrationOfOneEndpoint() throws Exception {
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class));
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(TestEndpoint.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
MBeanInfo mbeanInfo = mbeanExporter.getServer()
.getMBeanInfo(getObjectName("endpoint1", this.context));
assertThat(mbeanInfo).isNotNull();
assertThat(mbeanInfo.getOperations().length).isEqualTo(3);
assertThat(mbeanInfo.getAttributes().length).isEqualTo(3);
}
@Test
public void testSkipRegistrationOfDisabledEndpoint() throws Exception {
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class));
MutablePropertyValues mpv = new MutablePropertyValues();
mpv.add("enabled", Boolean.FALSE);
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(TestEndpoint.class, null, mpv));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertThat(mbeanExporter.getServer()
.isRegistered(getObjectName("endpoint1", this.context))).isFalse();
}
@Test
public void testRegistrationOfEnabledEndpoint() throws Exception {
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class));
MutablePropertyValues mpv = new MutablePropertyValues();
mpv.add("enabled", Boolean.TRUE);
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(TestEndpoint.class, null, mpv));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertThat(mbeanExporter.getServer()
.isRegistered(getObjectName("endpoint1", this.context))).isTrue();
}
@Test
public void testRegistrationTwoEndpoints() throws Exception {
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class));
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(TestEndpoint.class));
this.context.registerBeanDefinition("endpoint2",
new RootBeanDefinition(TestEndpoint2.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertThat(mbeanExporter.getServer()
.getMBeanInfo(getObjectName("endpoint1", this.context))).isNotNull();
assertThat(mbeanExporter.getServer()
.getMBeanInfo(getObjectName("endpoint2", this.context))).isNotNull();
}
@Test
public void testRegistrationWithDifferentDomain() throws Exception {
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class, null,
new MutablePropertyValues(
Collections.singletonMap("domain", "test-domain"))));
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(TestEndpoint.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertThat(mbeanExporter.getServer().getMBeanInfo(
getObjectName("test-domain", "endpoint1", false, this.context)))
.isNotNull();
}
@Test
public void testRegistrationWithDifferentDomainAndIdentity() throws Exception {
Map<String, Object> properties = new HashMap<>();
properties.put("domain", "test-domain");
properties.put("ensureUniqueRuntimeObjectNames", true);
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class, null,
new MutablePropertyValues(properties)));
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(TestEndpoint.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertThat(mbeanExporter.getServer().getMBeanInfo(
getObjectName("test-domain", "endpoint1", true, this.context)))
.isNotNull();
}
@Test
public void testRegistrationWithDifferentDomainAndIdentityAndStaticNames()
throws Exception {
Map<String, Object> properties = new HashMap<>();
properties.put("domain", "test-domain");
properties.put("ensureUniqueRuntimeObjectNames", true);
Properties staticNames = new Properties();
staticNames.put("key1", "value1");
staticNames.put("key2", "value2");
properties.put("objectNameStaticProperties", staticNames);
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class, null,
new MutablePropertyValues(properties)));
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(TestEndpoint.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertThat(mbeanExporter.getServer().getMBeanInfo(ObjectNameManager.getInstance(
getObjectName("test-domain", "endpoint1", true, this.context).toString()
+ ",key1=value1,key2=value2"))).isNotNull();
}
@Test
public void testRegistrationWithParentContext() throws Exception {
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class));
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(TestEndpoint.class));
GenericApplicationContext parent = new GenericApplicationContext();
this.context.setParent(parent);
parent.refresh();
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
assertThat(mbeanExporter.getServer()
.getMBeanInfo(getObjectName("endpoint1", this.context))).isNotNull();
parent.close();
}
@Test
public void jsonMapConversionWithDefaultObjectMapper() throws Exception {
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class));
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(JsonMapConversionEndpoint.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
Object response = mbeanExporter.getServer().invoke(
getObjectName("endpoint1", this.context), "getData", new Object[0],
new String[0]);
assertThat(response).isInstanceOf(Map.class);
assertThat(((Map<?, ?>) response).get("date")).isInstanceOf(Long.class);
}
@Test
public void jsonMapConversionWithCustomObjectMapper() throws Exception {
this.context = new GenericApplicationContext();
ConstructorArgumentValues constructorArgs = new ConstructorArgumentValues();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
constructorArgs.addIndexedArgumentValue(0, objectMapper);
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class, constructorArgs,
null));
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(JsonMapConversionEndpoint.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
Object response = mbeanExporter.getServer().invoke(
getObjectName("endpoint1", this.context), "getData", new Object[0],
new String[0]);
assertThat(response).isInstanceOf(Map.class);
assertThat(((Map<?, ?>) response).get("date")).isInstanceOf(String.class);
}
@Test
public void jsonListConversion() throws Exception {
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class));
this.context.registerBeanDefinition("endpoint1",
new RootBeanDefinition(JsonListConversionEndpoint.class));
this.context.refresh();
MBeanExporter mbeanExporter = this.context.getBean(EndpointMBeanExporter.class);
Object response = mbeanExporter.getServer().invoke(
getObjectName("endpoint1", this.context), "getData", new Object[0],
new String[0]);
assertThat(response).isInstanceOf(List.class);
assertThat(((List<?>) response).get(0)).isInstanceOf(Long.class);
}
@Test
public void loggerEndpointLowerCaseLogLevel() throws Exception {
MBeanExporter mbeanExporter = registerLoggersEndpoint();
Object response = mbeanExporter.getServer().invoke(
getObjectName("loggersEndpoint", this.context), "setLogLevel",
new Object[] { "com.example", "trace" },
new String[] { String.class.getName(), String.class.getName() });
assertThat(response).isNull();
}
@Test
public void loggerEndpointUnknownLogLevel() throws Exception {
MBeanExporter mbeanExporter = registerLoggersEndpoint();
this.thrown.expect(MBeanException.class);
this.thrown.expectCause(hasMessage(containsString("No enum constant")));
this.thrown.expectCause(hasMessage(containsString("LogLevel.INVALID")));
mbeanExporter.getServer().invoke(getObjectName("loggersEndpoint", this.context),
"setLogLevel", new Object[] { "com.example", "invalid" },
new String[] { String.class.getName(), String.class.getName() });
}
private MBeanExporter registerLoggersEndpoint() {
this.context = new GenericApplicationContext();
this.context.registerBeanDefinition("endpointMbeanExporter",
new RootBeanDefinition(EndpointMBeanExporter.class));
RootBeanDefinition bd = new RootBeanDefinition(LoggersEndpoint.class);
ConstructorArgumentValues values = new ConstructorArgumentValues();
values.addIndexedArgumentValue(0,
new LogbackLoggingSystem(getClass().getClassLoader()));
bd.setConstructorArgumentValues(values);
this.context.registerBeanDefinition("loggersEndpoint", bd);
this.context.refresh();
return this.context.getBean(EndpointMBeanExporter.class);
}
private ObjectName getObjectName(String beanKey, GenericApplicationContext context)
throws MalformedObjectNameException {
return getObjectName("org.springframework.boot", beanKey, false, context);
}
private ObjectName getObjectName(String domain, String beanKey,
boolean includeIdentity, ApplicationContext applicationContext)
throws MalformedObjectNameException {
if (includeIdentity) {
return ObjectNameManager.getInstance(String.format(
"%s:type=Endpoint,name=%s,identity=%s", domain, beanKey, ObjectUtils
.getIdentityHexString(applicationContext.getBean(beanKey))));
}
return ObjectNameManager
.getInstance(String.format("%s:type=Endpoint,name=%s", domain, beanKey));
}
public static class TestEndpoint extends AbstractEndpoint<String> {
public TestEndpoint() {
super("test");
}
@Override
public String invoke() {
return "hello world";
}
}
public static class TestEndpoint2 extends TestEndpoint {
}
public static class JsonMapConversionEndpoint
extends AbstractEndpoint<Map<String, Object>> {
public JsonMapConversionEndpoint() {
super("json_map_conversion");
}
@Override
public Map<String, Object> invoke() {
Map<String, Object> result = new LinkedHashMap<>();
result.put("date", new Date());
return result;
}
}
public static class JsonListConversionEndpoint
extends AbstractEndpoint<List<Object>> {
public JsonListConversionEndpoint() {
super("json_list_conversion");
}
@Override
public List<Object> invoke() {
return Arrays.<Object>asList(new Date());
}
}
}