HADOOP-15339. Support additional key/value propereties in JMX bean registration. Contributed by Elek Marton.
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/util/MBeans.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/util/MBeans.java
index ded49d6..916367f 100644
--- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/util/MBeans.java
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/util/MBeans.java
@@ -18,27 +18,33 @@
 package org.apache.hadoop.metrics2.util;
 
 import java.lang.management.ManagementFactory;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import javax.management.InstanceAlreadyExistsException;
 import javax.management.MBeanServer;
 import javax.management.ObjectName;
 
+import com.google.common.annotations.VisibleForTesting;
 import org.apache.hadoop.classification.InterfaceAudience;
 import org.apache.hadoop.classification.InterfaceStability;
 import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+
+import com.google.common.base.Preconditions;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  * This util class provides a method to register an MBean using
  * our standard naming convention as described in the doc
- *  for {link {@link #register(String, String, Object)}
+ *  for {link {@link #register(String, String, Object)}.
  */
 @InterfaceAudience.Public
 @InterfaceStability.Stable
-public class MBeans {
+public final class MBeans {
   private static final Logger LOG = LoggerFactory.getLogger(MBeans.class);
   private static final String DOMAIN_PREFIX = "Hadoop:";
   private static final String SERVICE_PREFIX = "service=";
@@ -48,10 +54,13 @@
       "^" + DOMAIN_PREFIX + SERVICE_PREFIX + "([^,]+)," +
       NAME_PREFIX + "(.+)$");
 
+  private MBeans() {
+  }
+
   /**
    * Register the MBean using our standard MBeanName format
    * "hadoop:service=<serviceName>,name=<nameName>"
-   * Where the <serviceName> and <nameName> are the supplied parameters
+   * Where the <serviceName> and <nameName> are the supplied parameters.
    *
    * @param serviceName
    * @param nameName
@@ -60,8 +69,30 @@
    */
   static public ObjectName register(String serviceName, String nameName,
                                     Object theMbean) {
+    return register(serviceName, nameName, new HashMap<String, String>(),
+        theMbean);
+  }
+
+  /**
+   * Register the MBean using our standard MBeanName format
+   * "hadoop:service=<serviceName>,name=<nameName>"
+   * Where the <serviceName> and <nameName> are the supplied parameters.
+   *
+   * @param serviceName
+   * @param nameName
+   * @param properties - Key value pairs to define additional JMX ObjectName
+   *                     properties.
+   * @param theMbean    - the MBean to register
+   * @return the named used to register the MBean
+   */
+  static public ObjectName register(String serviceName, String nameName,
+                                    Map<String, String> properties,
+                                    Object theMbean) {
     final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
-    ObjectName name = getMBeanName(serviceName, nameName);
+    Preconditions.checkNotNull(properties,
+        "JMX bean properties should not be null for "
+            + "bean registration.");
+    ObjectName name = getMBeanName(serviceName, nameName, properties);
     if (name != null) {
       try {
         mbs.registerMBean(theMbean, name);
@@ -116,9 +147,18 @@
     DefaultMetricsSystem.removeMBeanName(mbeanName);
   }
 
-  static private ObjectName getMBeanName(String serviceName, String nameName) {
+  @VisibleForTesting
+  static ObjectName getMBeanName(String serviceName, String nameName,
+      Map<String, String> additionalParameters) {
+
+    String additionalKeys = additionalParameters.entrySet()
+        .stream()
+        .map(entry -> entry.getKey() + "=" + entry.getValue())
+        .collect(Collectors.joining(","));
+
     String nameStr = DOMAIN_PREFIX + SERVICE_PREFIX + serviceName + "," +
-        NAME_PREFIX + nameName;
+        NAME_PREFIX + nameName +
+        (additionalKeys.isEmpty() ? "" : "," + additionalKeys);
     try {
       return DefaultMetricsSystem.newMBeanName(nameStr);
     } catch (Exception e) {
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/metrics2/util/DummyMXBean.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/metrics2/util/DummyMXBean.java
new file mode 100644
index 0000000..e1ff35c
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/metrics2/util/DummyMXBean.java
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you 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.apache.hadoop.metrics2.util;
+
+/**
+ * Sample JMX Bean interface to test JMX registration.
+ */
+public interface DummyMXBean {
+
+  int getCounter();
+}
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/metrics2/util/TestMBeans.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/metrics2/util/TestMBeans.java
new file mode 100644
index 0000000..3c93dbe
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/metrics2/util/TestMBeans.java
@@ -0,0 +1,107 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.apache.hadoop.metrics2.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+import java.lang.management.ManagementFactory;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Test MXBean addition of key/value pairs to registered MBeans.
+ */
+public class TestMBeans implements DummyMXBean {
+
+  private int counter = 1;
+
+  @Test
+  public void testRegister() throws Exception {
+    ObjectName objectName = null;
+    try {
+      counter = 23;
+      objectName = MBeans.register("UnitTest",
+          "RegisterTest", this);
+
+      MBeanServer platformMBeanServer =
+          ManagementFactory.getPlatformMBeanServer();
+
+      int jmxCounter = (int) platformMBeanServer
+          .getAttribute(objectName, "Counter");
+      Assert.assertEquals(counter, jmxCounter);
+    } finally {
+      if (objectName != null) {
+        MBeans.unregister(objectName);
+      }
+    }
+  }
+
+
+  @Test
+  public void testRegisterWithAdditionalProperties() throws Exception {
+    ObjectName objectName = null;
+    try {
+      counter = 42;
+
+      Map<String, String> properties = new HashMap<String, String>();
+      properties.put("flavour", "server");
+      objectName = MBeans.register("UnitTest", "RegisterTest",
+          properties, this);
+
+      MBeanServer platformMBeanServer =
+          ManagementFactory.getPlatformMBeanServer();
+      int jmxCounter =
+          (int) platformMBeanServer.getAttribute(objectName, "Counter");
+      Assert.assertEquals(counter, jmxCounter);
+    } finally {
+      if (objectName != null) {
+        MBeans.unregister(objectName);
+      }
+    }
+  }
+
+  @Test
+  public void testGetMbeanNameName() {
+    HashMap<String, String> properties = new HashMap<>();
+
+    ObjectName mBeanName = MBeans.getMBeanName("Service",
+        "Name", properties);
+
+    Assert.assertEquals("Service",
+        MBeans.getMbeanNameService(mBeanName));
+
+    properties.put("key", "value");
+    mBeanName = MBeans.getMBeanName(
+        "Service",
+        "Name",
+        properties);
+
+    Assert.assertEquals("Service",
+        MBeans.getMbeanNameService(mBeanName));
+
+  }
+
+  @Override
+  public int getCounter() {
+    return counter;
+  }
+
+}
\ No newline at end of file