diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategy.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategy.java
new file mode 100644
index 00000000000..b5d3f31228a
--- /dev/null
+++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategy.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2012-2015 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.metrics.jmx;
+
+import java.util.Hashtable;
+
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+
+import org.springframework.jmx.export.naming.KeyNamingStrategy;
+import org.springframework.jmx.export.naming.ObjectNamingStrategy;
+import org.springframework.util.StringUtils;
+
+/**
+ * MBean naming strategy for metric keys. A metric name of
+ * counter.foo.bar.spam translates to an object name with
+ * type=counter, name=foo and value=bar.spam. This
+ * results in a more or less pleasing view with no tweaks in jconsole or jvisualvm. The
+ * domain is copied from the input key and the type in the input key is discarded.
+ *
+ * @author Dave Syer
+ */
+public class DefaultMetricNamingStrategy implements ObjectNamingStrategy {
+
+ private ObjectNamingStrategy namingStrategy = new KeyNamingStrategy();
+
+ @Override
+ public ObjectName getObjectName(Object managedBean, String beanKey)
+ throws MalformedObjectNameException {
+ ObjectName objectName = this.namingStrategy.getObjectName(managedBean, beanKey);
+ String domain = objectName.getDomain();
+ Hashtable table = new Hashtable(
+ objectName.getKeyPropertyList());
+ String name = objectName.getKeyProperty("name");
+ if (name != null) {
+ table.remove("name");
+ String[] parts = StringUtils.delimitedListToStringArray(name, ".");
+ table.put("type", parts[0]);
+ if (parts.length > 1) {
+ table.put(parts.length > 2 ? "name" : "value", parts[1]);
+ }
+ if (parts.length > 2) {
+ table.put("value",
+ name.substring(parts[0].length() + parts[1].length() + 2));
+ }
+ }
+ return new ObjectName(domain, table);
+ }
+
+}
diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/JmxMetricWriter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/JmxMetricWriter.java
new file mode 100644
index 00000000000..db345b7833d
--- /dev/null
+++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/JmxMetricWriter.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2012-2015 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.metrics.jmx;
+
+import java.util.Date;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.management.MalformedObjectNameException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.boot.actuate.metrics.Metric;
+import org.springframework.boot.actuate.metrics.writer.Delta;
+import org.springframework.boot.actuate.metrics.writer.MetricWriter;
+import org.springframework.jmx.export.MBeanExporter;
+import org.springframework.jmx.export.annotation.ManagedAttribute;
+import org.springframework.jmx.export.annotation.ManagedOperation;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.springframework.jmx.export.naming.ObjectNamingStrategy;
+
+/**
+ * A {@link MetricWriter} for MBeans. Each metric is registered as an individual MBean, so
+ * (for instance) it can be graphed and monitored. The object names are provided by an
+ * {@link ObjectNamingStrategy}, where the default is a
+ * {@link DefaultMetricNamingStrategy} which provides type, name
+ * and value keys by splitting up the metric name on periods.
+ *
+ * @author Dave Syer
+ */
+@ManagedResource(description = "MetricWriter for pushing metrics to JMX MBeans.")
+public class JmxMetricWriter implements MetricWriter {
+
+ private static Log logger = LogFactory.getLog(JmxMetricWriter.class);
+
+ private final ConcurrentMap values = new ConcurrentHashMap();
+
+ private final MBeanExporter exporter;
+
+ private ObjectNamingStrategy namingStrategy = new DefaultMetricNamingStrategy();
+
+ private String domain = "org.springframework.metrics";
+
+ public JmxMetricWriter(MBeanExporter exporter) {
+ this.exporter = exporter;
+ }
+
+ public void setNamingStrategy(ObjectNamingStrategy namingStrategy) {
+ this.namingStrategy = namingStrategy;
+ }
+
+ public void setDomain(String domain) {
+ this.domain = domain;
+ }
+
+ @ManagedOperation
+ public void increment(String name, long value) {
+ increment(new Delta(name, value));
+ }
+
+ @Override
+ public void increment(Delta> delta) {
+ MetricValue counter = getValue(delta.getName());
+ counter.increment(delta.getValue().longValue());
+ }
+
+ @ManagedOperation
+ public void set(String name, double value) {
+ set(new Metric(name, value));
+ }
+
+ @Override
+ public void set(Metric> value) {
+ MetricValue metric = getValue(value.getName());
+ metric.setValue(value.getValue().doubleValue());
+ }
+
+ @Override
+ @ManagedOperation
+ public void reset(String name) {
+ MetricValue value = this.values.remove(name);
+ if (value != null) {
+ try {
+ // We can unregister the MBean, but if this writer is on the end of an
+ // Exporter the chances are it will be re-registered almost immediately.
+ this.exporter.unregisterManagedResource(this.namingStrategy
+ .getObjectName(value, getKey(name)));
+ }
+ catch (MalformedObjectNameException e) {
+ logger.warn("Could not unregister MBean for " + name);
+ }
+ }
+ }
+
+ private MetricValue getValue(String name) {
+ if (!this.values.containsKey(name)) {
+ this.values.putIfAbsent(name, new MetricValue());
+ MetricValue value = this.values.get(name);
+ try {
+ this.exporter.registerManagedResource(value,
+ this.namingStrategy.getObjectName(value, getKey(name)));
+ }
+ catch (Exception e) {
+ // Could not register mbean, maybe just a race condition
+ }
+ }
+ return this.values.get(name);
+ }
+
+ private String getKey(String name) {
+ return String.format(this.domain + ":type=MetricValue,name=%s", name);
+ }
+
+ @ManagedResource
+ public static class MetricValue {
+
+ private double value;
+
+ private long lastUpdated = 0;
+
+ public void setValue(double value) {
+ if (this.value != value) {
+ this.lastUpdated = System.currentTimeMillis();
+ }
+ this.value = value;
+ }
+
+ public void increment(long value) {
+ this.lastUpdated = System.currentTimeMillis();
+ this.value += value;
+ }
+
+ @ManagedAttribute
+ public double getValue() {
+ return this.value;
+ }
+
+ @ManagedAttribute
+ public Date getLastUpdated() {
+ return new Date(this.lastUpdated);
+ }
+
+ }
+
+}
diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/package-info.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/package-info.java
new file mode 100644
index 00000000000..9550f9a4c8d
--- /dev/null
+++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2012-2015 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.
+ */
+
+/**
+ * Metrics integration with JMX.
+ */
+package org.springframework.boot.actuate.metrics.jmx;
+
diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategyTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategyTests.java
new file mode 100644
index 00000000000..3da8bcfebf1
--- /dev/null
+++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategyTests.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2015 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.metrics.jmx;
+
+import javax.management.ObjectName;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Dave Syer
+ */
+public class DefaultMetricNamingStrategyTests {
+
+ private DefaultMetricNamingStrategy strategy = new DefaultMetricNamingStrategy();
+
+ @Test
+ public void simpleName() throws Exception {
+ ObjectName name = this.strategy.getObjectName(null,
+ "domain:type=MetricValue,name=foo");
+ assertEquals("domain", name.getDomain());
+ assertEquals("foo", name.getKeyProperty("type"));
+ }
+
+ @Test
+ public void onePeriod() throws Exception {
+ ObjectName name = this.strategy.getObjectName(null,
+ "domain:type=MetricValue,name=foo.bar");
+ assertEquals("domain", name.getDomain());
+ assertEquals("foo", name.getKeyProperty("type"));
+ assertEquals("Wrong name: " + name, "bar", name.getKeyProperty("value"));
+ }
+
+ @Test
+ public void twoPeriods() throws Exception {
+ ObjectName name = this.strategy.getObjectName(null,
+ "domain:type=MetricValue,name=foo.bar.spam");
+ assertEquals("domain", name.getDomain());
+ assertEquals("foo", name.getKeyProperty("type"));
+ assertEquals("Wrong name: " + name, "bar", name.getKeyProperty("name"));
+ assertEquals("Wrong name: " + name, "spam", name.getKeyProperty("value"));
+ }
+
+ @Test
+ public void threePeriods() throws Exception {
+ ObjectName name = this.strategy.getObjectName(null,
+ "domain:type=MetricValue,name=foo.bar.spam.bucket");
+ assertEquals("domain", name.getDomain());
+ assertEquals("foo", name.getKeyProperty("type"));
+ assertEquals("Wrong name: " + name, "bar", name.getKeyProperty("name"));
+ assertEquals("Wrong name: " + name, "spam.bucket", name.getKeyProperty("value"));
+ }
+
+}