Repository: skiptests/astraea
Pull request #519 (2022-07-28 14:11:30)

Refactor MBeanClient
1. 將`jmx` package移除
2. 將`MBeanClientImpl`放進`MBeanClient`
3. rename `MBeanClientImpl` to `AbstractMBeanClient`


Patch:
diff --git a/app/src/main/java/org/astraea/app/metrics/jmx/BeanObject.java b/app/src/main/java/org/astraea/app/metrics/BeanObject.java
similarity index 98%
rename from app/src/main/java/org/astraea/app/metrics/jmx/BeanObject.java
rename to app/src/main/java/org/astraea/app/metrics/BeanObject.java
index 226536fb..96c23f63 100644
--- a/app/src/main/java/org/astraea/app/metrics/jmx/BeanObject.java
+++ b/app/src/main/java/org/astraea/app/metrics/BeanObject.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.astraea.app.metrics.jmx;
+package org.astraea.app.metrics;
 
 import static java.util.Map.Entry;
 
diff --git a/app/src/main/java/org/astraea/app/metrics/jmx/BeanQuery.java b/app/src/main/java/org/astraea/app/metrics/BeanQuery.java
similarity index 99%
rename from app/src/main/java/org/astraea/app/metrics/jmx/BeanQuery.java
rename to app/src/main/java/org/astraea/app/metrics/BeanQuery.java
index ffe17fef..a37cc790 100644
--- a/app/src/main/java/org/astraea/app/metrics/jmx/BeanQuery.java
+++ b/app/src/main/java/org/astraea/app/metrics/BeanQuery.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.astraea.app.metrics.jmx;
+package org.astraea.app.metrics;
 
 import java.util.HashMap;
 import java.util.Hashtable;
diff --git a/app/src/main/java/org/astraea/app/metrics/HasBeanObject.java b/app/src/main/java/org/astraea/app/metrics/HasBeanObject.java
index 3dd3fb99..e9ad5dad 100644
--- a/app/src/main/java/org/astraea/app/metrics/HasBeanObject.java
+++ b/app/src/main/java/org/astraea/app/metrics/HasBeanObject.java
@@ -16,8 +16,6 @@
  */
 package org.astraea.app.metrics;
 
-import org.astraea.app.metrics.jmx.BeanObject;
-
 public interface HasBeanObject {
   BeanObject beanObject();
 
diff --git a/app/src/main/java/org/astraea/app/metrics/KafkaMetrics.java b/app/src/main/java/org/astraea/app/metrics/KafkaMetrics.java
index ff4587b4..18ef641b 100644
--- a/app/src/main/java/org/astraea/app/metrics/KafkaMetrics.java
+++ b/app/src/main/java/org/astraea/app/metrics/KafkaMetrics.java
@@ -24,8 +24,6 @@ import java.util.stream.Collectors;
 import org.astraea.app.metrics.broker.BrokerTopicMetricsResult;
 import org.astraea.app.metrics.broker.HasValue;
 import org.astraea.app.metrics.broker.TotalTimeMs;
-import org.astraea.app.metrics.jmx.BeanQuery;
-import org.astraea.app.metrics.jmx.MBeanClient;
 import org.astraea.app.metrics.platform.JvmMemory;
 import org.astraea.app.metrics.platform.OperatingSystemInfo;
 import org.astraea.app.metrics.producer.HasProducerNodeMetrics;
diff --git a/app/src/main/java/org/astraea/app/metrics/MBeanClient.java b/app/src/main/java/org/astraea/app/metrics/MBeanClient.java
new file mode 100644
index 00000000..de0de104
--- /dev/null
+++ b/app/src/main/java/org/astraea/app/metrics/MBeanClient.java
@@ -0,0 +1,282 @@
+/*
+ * 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.astraea.app.metrics;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.management.ManagementFactory;
+import java.net.MalformedURLException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.management.InstanceNotFoundException;
+import javax.management.IntrospectionException;
+import javax.management.MBeanFeatureInfo;
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectInstance;
+import javax.management.ObjectName;
+import javax.management.ReflectionException;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+import org.astraea.app.common.Utils;
+
+/**
+ * A MBeanClient used to retrieve mbean value from remote Jmx server.
+ *
+ * <pre>{@code
+ * try(MBeanClient client = new MBeanClient(jmxConnectorServer.getAddress())) {
+ *   BeanObject bean = client.queryBean(BeanQuery.builder("java.lang")
+ *            .property("type", "MemoryManager")
+ *            .property("name", "CodeCacheManager")
+ *            .build());
+ *   System.out.println(bean.getAttributes());
+ * }</pre>
+ */
+public interface MBeanClient extends AutoCloseable {
+
+  /**
+   * @param host the address of jmx server
+   * @param port the port of jmx server
+   * @return a mbean client using JNDI to lookup metrics.
+   */
+  static MBeanClient jndi(String host, int port) {
+    try {
+      return of(
+          new JMXServiceURL(
+              String.format(
+                  "service:jmx:rmi://%s:%s/jndi/rmi://%s:%s/jmxrmi", host, port, host, port)));
+    } catch (MalformedURLException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  static MBeanClient of(JMXServiceURL jmxServiceURL) {
+    try {
+      var jmxConnector = JMXConnectorFactory.connect(jmxServiceURL);
+      return new AbstractMBeanClient(jmxConnector.getMBeanServerConnection()) {
+        @Override
+        public String host() {
+          return jmxServiceURL.getHost();
+        }
+
+        @Override
+        public int port() {
+          return jmxServiceURL.getPort();
+        }
+
+        @Override
+        public void close() {
+          Utils.packException(jmxConnector::close);
+        }
+      };
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  static MBeanClient local() {
+    return new AbstractMBeanClient(ManagementFactory.getPlatformMBeanServer()) {
+      @Override
+      public String host() {
+        return Utils.hostname();
+      }
+
+      @Override
+      public int port() {
+        return -1;
+      }
+
+      @Override
+      public void close() {}
+    };
+  }
+
+  /**
+   * Fetch all attributes of target mbean.
+   *
+   * <p>Note that when exception is raised during the attribute fetching process, the exact
+   * exception will be placed into the attribute field.
+   *
+   * @param beanQuery the non-pattern BeanQuery
+   * @return A {@link BeanObject} contain all attributes if target resolved successfully.
+   */
+  BeanObject queryBean(BeanQuery beanQuery);
+
+  /**
+   * Fetch given attributes of target mbean
+   *
+   * <p>Note that when exception is raised during the attribute fetching process, the exact
+   * exception will be placed into the attribute field.
+   *
+   * @param beanQuery the non-pattern BeanQuery
+   * @param attributeNameCollection a list of attribute you want to retrieve
+   * @return A {@link BeanObject} contain given specific attributes if target resolved successfully.
+   */
+  BeanObject queryBean(BeanQuery beanQuery, Collection<String> attributeNameCollection);
+
+  /**
+   * Query mBeans by pattern.
+   *
+   * <p>Query mbeans by {@link ObjectName} pattern, the returned {@link BeanObject}s will contain
+   * all the available attributes
+   *
+   * <p>Note that when exception is raised during the attribute fetching process, the exact
+   * exception will be placed into the attribute field.
+   *
+   * @param beanQuery the pattern to query
+   * @return A {@link Set} of {@link BeanObject}, all BeanObject has its own attributes resolved.
+   */
+  Collection<BeanObject> queryBeans(BeanQuery beanQuery);
+
+  /**
+   * Returns the list of domains in which any MBean is currently registered.
+   *
+   * <p>The order of strings within the returned array is not defined.
+   *
+   * @return a {@link List} of domain name {@link String}
+   */
+  List<String> listDomains();
+
+  /** @return the host address of jmx server */
+  String host();
+
+  /** @return the port listened by jmx server */
+  int port();
+
+  @Override
+  void close();
+
+  abstract class AbstractMBeanClient implements MBeanClient {
+
+    private final MBeanServerConnection connection;
+
+    AbstractMBeanClient(MBeanServerConnection connection) {
+      this.connection = connection;
+    }
+
+    @Override
+    public BeanObject queryBean(BeanQuery beanQuery) {
+      try {
+        // ask for MBeanInfo
+        var mBeanInfo = connection.getMBeanInfo(beanQuery.objectName());
+
+        // create a list builder all available attributes name
+        var attributeName =
+            Arrays.stream(mBeanInfo.getAttributes())
+                .map(MBeanFeatureInfo::getName)
+                .collect(Collectors.toList());
+
+        // query the result
+        return queryBean(beanQuery, attributeName);
+      } catch (ReflectionException | IntrospectionException e) {
+        throw new RuntimeException(e);
+      } catch (IOException e) {
+        throw new UncheckedIOException(e);
+      } catch (InstanceNotFoundException e) {
+        throw new NoSuchElementException(e.getMessage());
+      }
+    }
+
+    @Override
+    public BeanObject queryBean(BeanQuery beanQuery, Collection<String> attributeNameCollection) {
+      try {
+
+        // fetch attribute value from mbean server
+        var attributeNameArray = attributeNameCollection.toArray(new String[0]);
+        var attributeList =
+            connection.getAttributes(beanQuery.objectName(), attributeNameArray).asList();
+
+        // collect attribute name & value into a map
+        var attributes = new HashMap<String, Object>();
+        attributeList.forEach(
+            attribute -> attributes.put(attribute.getName(), attribute.getValue()));
+
+        // according to the javadoc of MBeanServerConnection#getAttributes, the API will
+        // ignore any
+        // error occurring during the fetch process (for example, attribute not exists). Below code
+        // check for such condition and try to figure out what exactly the error is. put it into
+        // attributes return result.
+        var notResolvedAttributes =
+            Arrays.stream(attributeNameArray)
+                .filter(str -> !attributes.containsKey(str))
+                .collect(Collectors.toSet());
+        notResolvedAttributes.forEach(
+            attributeName ->
+                attributes.put(
+                    attributeName, fetchAttributeObjectOrException(beanQuery, attributeName)));
+
+        // collect result, and build a new BeanObject as return result
+        return new BeanObject(beanQuery.domainName(), beanQuery.properties(), attributes);
+
+      } catch (ReflectionException e) {
+        throw new RuntimeException(e);
+      } catch (IOException e) {
+        throw new UncheckedIOException(e);
+      } catch (InstanceNotFoundException e) {
+        throw new NoSuchElementException(e.getMessage());
+      }
+    }
+
+    private Object fetchAttributeObjectOrException(BeanQuery beanQuery, String attributeName) {
+      // It is possible to trigger some unexpected runtime exception during the following call.
+      // For example, on my machine when I try to get attribute "BootClassPath" from
+      // "java.lang:type=Runtime".
+      // I will get a {@link java.lang.UnsupportedOperationException} indicates that "Boot class
+      // path
+      // mechanism is not supported". Those attribute actually exists, but I cannot retrieve those
+      // attribute value. Doing so I get that error.
+      //
+      // Instead of blinding that attribute from the library user, I decided to put the
+      // exception
+      // into their result.
+      try {
+        return connection.getAttribute(beanQuery.objectName(), attributeName);
+      } catch (IOException e) {
+        throw new UncheckedIOException(e);
+      } catch (Exception e) {
+        return e;
+      }
+    }
+
+    @Override
+    public Collection<BeanObject> queryBeans(BeanQuery beanQuery) {
+      try {
+        return connection.queryMBeans(beanQuery.objectName(), null).stream()
+            .map(ObjectInstance::getObjectName)
+            .map(BeanQuery::fromObjectName)
+            .map(this::queryBean)
+            .collect(Collectors.toSet());
+
+      } catch (IOException e) {
+        throw new UncheckedIOException(e);
+      }
+    }
+
+    @Override
+    public List<String> listDomains() {
+      try {
+        return Arrays.asList(connection.getDomains());
+      } catch (IOException e) {
+        throw new UncheckedIOException(e);
+      }
+    }
+  }
+}
diff --git a/app/src/main/java/org/astraea/app/metrics/MetricExplorer.java b/app/src/main/java/org/astraea/app/metrics/MetricExplorer.java
index a511704e..c9bcb84f 100644
--- a/app/src/main/java/org/astraea/app/metrics/MetricExplorer.java
+++ b/app/src/main/java/org/astraea/app/metrics/MetricExplorer.java
@@ -33,9 +33,6 @@ import java.util.stream.Stream;
 import javax.management.openmbean.CompositeDataSupport;
 import javax.management.remote.JMXServiceURL;
 import org.astraea.app.argument.Field;
-import org.astraea.app.metrics.jmx.BeanObject;
-import org.astraea.app.metrics.jmx.BeanQuery;
-import org.astraea.app.metrics.jmx.MBeanClient;
 
 public class MetricExplorer {
 
diff --git a/app/src/main/java/org/astraea/app/metrics/broker/BrokerTopicMetricsResult.java b/app/src/main/java/org/astraea/app/metrics/broker/BrokerTopicMetricsResult.java
index 19ff7dfd..74bfb32e 100644
--- a/app/src/main/java/org/astraea/app/metrics/broker/BrokerTopicMetricsResult.java
+++ b/app/src/main/java/org/astraea/app/metrics/broker/BrokerTopicMetricsResult.java
@@ -18,7 +18,7 @@ package org.astraea.app.metrics.broker;
 
 import java.util.Map;
 import java.util.Objects;
-import org.astraea.app.metrics.jmx.BeanObject;
+import org.astraea.app.metrics.BeanObject;
 
 public class BrokerTopicMetricsResult implements HasCount, HasEventType, HasRate {
 
diff --git a/app/src/main/java/org/astraea/app/metrics/broker/HasValue.java b/app/src/main/java/org/astraea/app/metrics/broker/HasValue.java
index b3dd5909..0fc2492a 100644
--- a/app/src/main/java/org/astraea/app/metrics/broker/HasValue.java
+++ b/app/src/main/java/org/astraea/app/metrics/broker/HasValue.java
@@ -16,8 +16,8 @@
  */
 package org.astraea.app.metrics.broker;
 
+import org.astraea.app.metrics.BeanObject;
 import org.astraea.app.metrics.HasBeanObject;
-import org.astraea.app.metrics.jmx.BeanObject;
 
 public interface HasValue extends HasBeanObject {
   default long value() {
diff --git a/app/src/main/java/org/astraea/app/metrics/broker/LogMetrics.java b/app/src/main/java/org/astraea/app/metrics/broker/LogMetrics.java
index 3f24a5a3..8f124e92 100644
--- a/app/src/main/java/org/astraea/app/metrics/broker/LogMetrics.java
+++ b/app/src/main/java/org/astraea/app/metrics/broker/LogMetrics.java
@@ -20,10 +20,10 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.stream.Collectors;
+import org.astraea.app.metrics.BeanObject;
+import org.astraea.app.metrics.BeanQuery;
 import org.astraea.app.metrics.HasBeanObject;
-import org.astraea.app.metrics.jmx.BeanObject;
-import org.astraea.app.metrics.jmx.BeanQuery;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 
 public final class LogMetrics {
 
diff --git a/app/src/main/java/org/astraea/app/metrics/broker/ServerMetrics.java b/app/src/main/java/org/astraea/app/metrics/broker/ServerMetrics.java
index 8af7a1ab..303b55b9 100644
--- a/app/src/main/java/org/astraea/app/metrics/broker/ServerMetrics.java
+++ b/app/src/main/java/org/astraea/app/metrics/broker/ServerMetrics.java
@@ -19,9 +19,9 @@ package org.astraea.app.metrics.broker;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.stream.Collectors;
-import org.astraea.app.metrics.jmx.BeanObject;
-import org.astraea.app.metrics.jmx.BeanQuery;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.BeanObject;
+import org.astraea.app.metrics.BeanQuery;
+import org.astraea.app.metrics.MBeanClient;
 
 public final class ServerMetrics {
 
diff --git a/app/src/main/java/org/astraea/app/metrics/broker/TotalTimeMs.java b/app/src/main/java/org/astraea/app/metrics/broker/TotalTimeMs.java
index ba0d7ea5..91d3d9f7 100644
--- a/app/src/main/java/org/astraea/app/metrics/broker/TotalTimeMs.java
+++ b/app/src/main/java/org/astraea/app/metrics/broker/TotalTimeMs.java
@@ -17,7 +17,7 @@
 package org.astraea.app.metrics.broker;
 
 import java.util.Map;
-import org.astraea.app.metrics.jmx.BeanObject;
+import org.astraea.app.metrics.BeanObject;
 
 public class TotalTimeMs implements HasPercentiles, HasCount, HasStatistics {
 
diff --git a/app/src/main/java/org/astraea/app/metrics/collector/BeanCollector.java b/app/src/main/java/org/astraea/app/metrics/collector/BeanCollector.java
index 9da61fe3..6ea754f1 100644
--- a/app/src/main/java/org/astraea/app/metrics/collector/BeanCollector.java
+++ b/app/src/main/java/org/astraea/app/metrics/collector/BeanCollector.java
@@ -32,7 +32,7 @@ import java.util.function.BiFunction;
 import org.astraea.app.common.Utils;
 import org.astraea.app.metrics.HasBeanObject;
 import org.astraea.app.metrics.KafkaMetrics;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 
 public class BeanCollector {
 
diff --git a/app/src/main/java/org/astraea/app/metrics/collector/Fetcher.java b/app/src/main/java/org/astraea/app/metrics/collector/Fetcher.java
index 81458d01..96bc5ff2 100644
--- a/app/src/main/java/org/astraea/app/metrics/collector/Fetcher.java
+++ b/app/src/main/java/org/astraea/app/metrics/collector/Fetcher.java
@@ -22,7 +22,7 @@ import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.astraea.app.metrics.HasBeanObject;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 
 @FunctionalInterface
 public interface Fetcher {
diff --git a/app/src/main/java/org/astraea/app/metrics/jmx/MBeanClient.java b/app/src/main/java/org/astraea/app/metrics/jmx/MBeanClient.java
deleted file mode 100644
index 23ad00e1..00000000
--- a/app/src/main/java/org/astraea/app/metrics/jmx/MBeanClient.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.astraea.app.metrics.jmx;
-
-import java.net.MalformedURLException;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-import javax.management.ObjectName;
-import javax.management.remote.JMXServiceURL;
-
-/**
- * A MBeanClient used to retrieve mbean value from remote Jmx server.
- *
- * <pre>{@code
- * try(MBeanClient client = new MBeanClient(jmxConnectorServer.getAddress())) {
- *   BeanObject bean = client.queryBean(BeanQuery.builder("java.lang")
- *            .property("type", "MemoryManager")
- *            .property("name", "CodeCacheManager")
- *            .build());
- *   System.out.println(bean.getAttributes());
- * }</pre>
- */
-public interface MBeanClient extends AutoCloseable {
-
-  /**
-   * @param host the address of jmx server
-   * @param port the port of jmx server
-   * @return a mbean client using JNDI to lookup metrics.
-   */
-  static MBeanClient jndi(String host, int port) {
-    try {
-      return of(
-          new JMXServiceURL(
-              String.format(
-                  "service:jmx:rmi://%s:%s/jndi/rmi://%s:%s/jmxrmi", host, port, host, port)));
-    } catch (MalformedURLException e) {
-      throw new IllegalArgumentException(e);
-    }
-  }
-
-  static MBeanClient of(JMXServiceURL url) {
-    return MBeanClientImpl.remote(url);
-  }
-
-  static MBeanClient local() {
-    return MBeanClientImpl.local();
-  }
-
-  /**
-   * Fetch all attributes of target mbean.
-   *
-   * <p>Note that when exception is raised during the attribute fetching process, the exact
-   * exception will be placed into the attribute field.
-   *
-   * @param beanQuery the non-pattern BeanQuery
-   * @return A {@link BeanObject} contain all attributes if target resolved successfully.
-   */
-  BeanObject queryBean(BeanQuery beanQuery);
-
-  /**
-   * Fetch given attributes of target mbean
-   *
-   * <p>Note that when exception is raised during the attribute fetching process, the exact
-   * exception will be placed into the attribute field.
-   *
-   * @param beanQuery the non-pattern BeanQuery
-   * @param attributeNameCollection a list of attribute you want to retrieve
-   * @return A {@link BeanObject} contain given specific attributes if target resolved successfully.
-   */
-  BeanObject queryBean(BeanQuery beanQuery, Collection<String> attributeNameCollection);
-
-  /**
-   * Query mBeans by pattern.
-   *
-   * <p>Query mbeans by {@link ObjectName} pattern, the returned {@link BeanObject}s will contain
-   * all the available attributes
-   *
-   * <p>Note that when exception is raised during the attribute fetching process, the exact
-   * exception will be placed into the attribute field.
-   *
-   * @param beanQuery the pattern to query
-   * @return A {@link Set} of {@link BeanObject}, all BeanObject has its own attributes resolved.
-   */
-  Collection<BeanObject> queryBeans(BeanQuery beanQuery);
-
-  /**
-   * Returns the list of domains in which any MBean is currently registered.
-   *
-   * <p>The order of strings within the returned array is not defined.
-   *
-   * @return a {@link List} of domain name {@link String}
-   */
-  List<String> listDomains();
-
-  /** @return the host address of jmx server */
-  String host();
-
-  /** @return the port listened by jmx server */
-  int port();
-
-  @Override
-  void close();
-}
diff --git a/app/src/main/java/org/astraea/app/metrics/jmx/MBeanClientImpl.java b/app/src/main/java/org/astraea/app/metrics/jmx/MBeanClientImpl.java
deleted file mode 100644
index 36260b34..00000000
--- a/app/src/main/java/org/astraea/app/metrics/jmx/MBeanClientImpl.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * 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.astraea.app.metrics.jmx;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.lang.management.ManagementFactory;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.stream.Collectors;
-import javax.management.InstanceNotFoundException;
-import javax.management.IntrospectionException;
-import javax.management.MBeanFeatureInfo;
-import javax.management.MBeanServerConnection;
-import javax.management.ObjectInstance;
-import javax.management.ReflectionException;
-import javax.management.remote.JMXConnectorFactory;
-import javax.management.remote.JMXServiceURL;
-import org.astraea.app.common.Utils;
-
-abstract class MBeanClientImpl implements MBeanClient {
-
-  static MBeanClientImpl remote(JMXServiceURL jmxServiceURL) {
-    try {
-      var jmxConnector = JMXConnectorFactory.connect(jmxServiceURL);
-      return new MBeanClientImpl(jmxConnector.getMBeanServerConnection()) {
-        @Override
-        public String host() {
-          return jmxServiceURL.getHost();
-        }
-
-        @Override
-        public int port() {
-          return jmxServiceURL.getPort();
-        }
-
-        @Override
-        public void close() {
-          Utils.packException(jmxConnector::close);
-        }
-      };
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-  }
-
-  static MBeanClientImpl local() {
-    return new MBeanClientImpl(ManagementFactory.getPlatformMBeanServer()) {
-      @Override
-      public String host() {
-        return Utils.hostname();
-      }
-
-      @Override
-      public int port() {
-        return -1;
-      }
-
-      @Override
-      public void close() {}
-    };
-  }
-
-  private final MBeanServerConnection connection;
-
-  MBeanClientImpl(MBeanServerConnection connection) {
-    this.connection = connection;
-  }
-
-  @Override
-  public BeanObject queryBean(BeanQuery beanQuery) {
-    try {
-      // ask for MBeanInfo
-      var mBeanInfo = connection.getMBeanInfo(beanQuery.objectName());
-
-      // create a list builder all available attributes name
-      var attributeName =
-          Arrays.stream(mBeanInfo.getAttributes())
-              .map(MBeanFeatureInfo::getName)
-              .collect(Collectors.toList());
-
-      // query the result
-      return queryBean(beanQuery, attributeName);
-    } catch (ReflectionException | IntrospectionException e) {
-      throw new RuntimeException(e);
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    } catch (InstanceNotFoundException e) {
-      throw new NoSuchElementException(e.getMessage());
-    }
-  }
-
-  @Override
-  public BeanObject queryBean(BeanQuery beanQuery, Collection<String> attributeNameCollection) {
-    try {
-
-      // fetch attribute value from mbean server
-      var attributeNameArray = attributeNameCollection.toArray(new String[0]);
-      var attributeList =
-          connection.getAttributes(beanQuery.objectName(), attributeNameArray).asList();
-
-      // collect attribute name & value into a map
-      var attributes = new HashMap<String, Object>();
-      attributeList.forEach(attribute -> attributes.put(attribute.getName(), attribute.getValue()));
-
-      // according to the javadoc of MBeanServerConnection#getAttributes, the API will
-      // ignore any
-      // error occurring during the fetch process (for example, attribute not exists). Below code
-      // check for such condition and try to figure out what exactly the error is. put it into
-      // attributes return result.
-      var notResolvedAttributes =
-          Arrays.stream(attributeNameArray)
-              .filter(str -> !attributes.containsKey(str))
-              .collect(Collectors.toSet());
-      notResolvedAttributes.forEach(
-          attributeName ->
-              attributes.put(
-                  attributeName, fetchAttributeObjectOrException(beanQuery, attributeName)));
-
-      // collect result, and build a new BeanObject as return result
-      return new BeanObject(beanQuery.domainName(), beanQuery.properties(), attributes);
-
-    } catch (ReflectionException e) {
-      throw new RuntimeException(e);
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    } catch (InstanceNotFoundException e) {
-      throw new NoSuchElementException(e.getMessage());
-    }
-  }
-
-  private Object fetchAttributeObjectOrException(BeanQuery beanQuery, String attributeName) {
-    // It is possible to trigger some unexpected runtime exception during the following call.
-    // For example, on my machine when I try to get attribute "BootClassPath" from
-    // "java.lang:type=Runtime".
-    // I will get a {@link java.lang.UnsupportedOperationException} indicates that "Boot class path
-    // mechanism is not supported". Those attribute actually exists, but I cannot retrieve those
-    // attribute value. Doing so I get that error.
-    //
-    // Instead of blinding that attribute from the library user, I decided to put the
-    // exception
-    // into their result.
-    try {
-      return connection.getAttribute(beanQuery.objectName(), attributeName);
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    } catch (Exception e) {
-      return e;
-    }
-  }
-
-  @Override
-  public Collection<BeanObject> queryBeans(BeanQuery beanQuery) {
-    try {
-      return connection.queryMBeans(beanQuery.objectName(), null).stream()
-          .map(ObjectInstance::getObjectName)
-          .map(BeanQuery::fromObjectName)
-          .map(this::queryBean)
-          .collect(Collectors.toSet());
-
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-  }
-
-  @Override
-  public List<String> listDomains() {
-    try {
-      return Arrays.asList(connection.getDomains());
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-  }
-}
diff --git a/app/src/main/java/org/astraea/app/metrics/platform/JvmMemory.java b/app/src/main/java/org/astraea/app/metrics/platform/JvmMemory.java
index 16d91df9..efc812e2 100644
--- a/app/src/main/java/org/astraea/app/metrics/platform/JvmMemory.java
+++ b/app/src/main/java/org/astraea/app/metrics/platform/JvmMemory.java
@@ -17,7 +17,7 @@
 package org.astraea.app.metrics.platform;
 
 import java.lang.management.MemoryUsage;
-import org.astraea.app.metrics.jmx.BeanObject;
+import org.astraea.app.metrics.BeanObject;
 
 public class JvmMemory implements HasJvmMemory {
 
diff --git a/app/src/main/java/org/astraea/app/metrics/platform/OperatingSystemInfo.java b/app/src/main/java/org/astraea/app/metrics/platform/OperatingSystemInfo.java
index bb55490f..4e8848c9 100644
--- a/app/src/main/java/org/astraea/app/metrics/platform/OperatingSystemInfo.java
+++ b/app/src/main/java/org/astraea/app/metrics/platform/OperatingSystemInfo.java
@@ -17,7 +17,7 @@
 package org.astraea.app.metrics.platform;
 
 import java.util.Map;
-import org.astraea.app.metrics.jmx.BeanObject;
+import org.astraea.app.metrics.BeanObject;
 
 public class OperatingSystemInfo implements HasOperatingSystemInfo {
 
diff --git a/app/src/main/java/org/astraea/app/partitioner/smooth/SmoothWeightRoundRobinDispatcher.java b/app/src/main/java/org/astraea/app/partitioner/smooth/SmoothWeightRoundRobinDispatcher.java
index 0447d0e9..93d648db 100644
--- a/app/src/main/java/org/astraea/app/partitioner/smooth/SmoothWeightRoundRobinDispatcher.java
+++ b/app/src/main/java/org/astraea/app/partitioner/smooth/SmoothWeightRoundRobinDispatcher.java
@@ -38,9 +38,9 @@ import org.astraea.app.common.Utils;
 import org.astraea.app.cost.NeutralIntegratedCost;
 import org.astraea.app.cost.Periodic;
 import org.astraea.app.metrics.HasBeanObject;
+import org.astraea.app.metrics.MBeanClient;
 import org.astraea.app.metrics.collector.BeanCollector;
 import org.astraea.app.metrics.collector.Receiver;
-import org.astraea.app.metrics.jmx.MBeanClient;
 import org.astraea.app.partitioner.Configuration;
 import org.astraea.app.partitioner.Dispatcher;
 import org.astraea.app.partitioner.PartitionerUtils;
diff --git a/app/src/main/java/org/astraea/app/performance/ReportFormat.java b/app/src/main/java/org/astraea/app/performance/ReportFormat.java
index 0952d47b..129d4ac2 100644
--- a/app/src/main/java/org/astraea/app/performance/ReportFormat.java
+++ b/app/src/main/java/org/astraea/app/performance/ReportFormat.java
@@ -29,6 +29,7 @@ import java.util.Date;
 import java.util.function.Supplier;
 import java.util.stream.IntStream;
 import org.astraea.app.common.DataSize;
+import org.astraea.app.common.DataUnit;
 import org.astraea.app.common.Utils;
 
 public enum ReportFormat {
@@ -70,6 +71,7 @@ public enum ReportFormat {
                 path.toString(),
                 "Performance"
                     + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
+                    + "."
                     + reportFormat);
     var writer = new BufferedWriter(new FileWriter(filePath.toFile()));
     switch (reportFormat) {
@@ -108,7 +110,7 @@ public enum ReportFormat {
   private static void initCSVFormat(BufferedWriter writer, int producerCounts, int consumerCounts)
       throws IOException {
     writer.write(
-        "Time \\ Name, Consumed/Produced, Output throughput (/sec), Input throughput (/sec), "
+        "Time \\ Name, Consumed/Produced, Output throughput (MiB/sec), Input throughput (MiB/sec), "
             + "Publish max latency (ms), Publish min latency (ms), "
             + "End-to-end max latency (ms), End-to-end min latency (ms)");
     IntStream.range(0, producerCounts)
@@ -118,9 +120,9 @@ public enum ReportFormat {
                 writer.write(
                     ",Producer["
                         + i
-                        + "] current throughput (/sec), Producer["
+                        + "] current throughput (MiB/sec), Producer["
                         + i
-                        + "] current publish latency (ms)");
+                        + "] average publish latency (ms)");
               } catch (IOException ignore) {
               }
             });
@@ -131,9 +133,9 @@ public enum ReportFormat {
                 writer.write(
                     ",Consumer["
                         + i
-                        + "] current throughput (/sec), Consumer["
+                        + "] current throughput (MiB/sec), Consumer["
                         + i
-                        + "] current ene-to-end latency (ms)");
+                        + "] average ene-to-end latency (ms)");
               } catch (IOException ignore) {
               }
             });
@@ -152,23 +154,38 @@ public enum ReportFormat {
     try {
       writer.write(
           result.duration.toHoursPart()
-              + "h"
+              + ":"
               + result.duration.toMinutesPart()
-              + "m"
-              + result.duration.toSecondsPart()
-              + "s");
+              + ":"
+              + result.duration.toSecondsPart());
       writer.write(
           String.format(",%.2f%% / %.2f%%", result.consumerPercentage, result.producerPercentage));
-      writer.write("," + DataSize.Byte.of(result.producerResult.totalCurrentBytes()));
-      writer.write("," + DataSize.Byte.of(result.consumerResult.totalCurrentBytes()));
+      writer.write(
+          ","
+              + DataSize.Byte.of(result.producerResult.totalCurrentBytes())
+                  .measurement(DataUnit.MiB)
+                  .doubleValue());
+      writer.write(
+          ","
+              + DataSize.Byte.of(result.consumerResult.totalCurrentBytes())
+                  .measurement(DataUnit.MiB)
+                  .doubleValue());
       writer.write("," + result.producerResult.maxLatency + "," + result.producerResult.minLatency);
       writer.write("," + result.consumerResult.maxLatency + "," + result.consumerResult.minLatency);
       for (int i = 0; i < result.producerResult.bytes.size(); ++i) {
-        writer.write("," + DataSize.Byte.of(result.producerResult.currentBytes.get(i)));
+        writer.write(
+            ","
+                + DataSize.Byte.of(result.producerResult.currentBytes.get(i))
+                    .measurement(DataUnit.MiB)
+                    .doubleValue());
         writer.write("," + result.producerResult.averageLatencies.get(i));
       }
       for (int i = 0; i < result.consumerResult.bytes.size(); ++i) {
-        writer.write("," + DataSize.Byte.of(result.consumerResult.currentBytes.get(i)));
+        writer.write(
+            ","
+                + DataSize.Byte.of(result.consumerResult.currentBytes.get(i))
+                    .measurement(DataUnit.MiB)
+                    .doubleValue());
         writer.write("," + result.consumerResult.averageLatencies.get(i));
       }
       writer.newLine();
diff --git a/app/src/main/java/org/astraea/app/web/BeanHandler.java b/app/src/main/java/org/astraea/app/web/BeanHandler.java
index 4ea2ad13..74598953 100644
--- a/app/src/main/java/org/astraea/app/web/BeanHandler.java
+++ b/app/src/main/java/org/astraea/app/web/BeanHandler.java
@@ -22,9 +22,9 @@ import java.util.Optional;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.astraea.app.admin.Admin;
-import org.astraea.app.metrics.jmx.BeanObject;
-import org.astraea.app.metrics.jmx.BeanQuery;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.BeanObject;
+import org.astraea.app.metrics.BeanQuery;
+import org.astraea.app.metrics.MBeanClient;
 
 public class BeanHandler implements Handler {
   private final List<MBeanClient> clients;


Test patch:
diff --git a/app/src/test/java/org/astraea/app/admin/ClusterBeanTest.java b/app/src/test/java/org/astraea/app/admin/ClusterBeanTest.java
index 431bb846..0419b7e7 100644
--- a/app/src/test/java/org/astraea/app/admin/ClusterBeanTest.java
+++ b/app/src/test/java/org/astraea/app/admin/ClusterBeanTest.java
@@ -18,10 +18,10 @@ package org.astraea.app.admin;
 
 import java.util.List;
 import java.util.Map;
+import org.astraea.app.metrics.BeanObject;
 import org.astraea.app.metrics.KafkaMetrics;
 import org.astraea.app.metrics.broker.HasValue;
 import org.astraea.app.metrics.broker.LogMetrics;
-import org.astraea.app.metrics.jmx.BeanObject;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
diff --git a/app/src/test/java/org/astraea/app/cost/BrokerInputCostTest.java b/app/src/test/java/org/astraea/app/cost/BrokerInputCostTest.java
index cdddb68d..95c43e3e 100644
--- a/app/src/test/java/org/astraea/app/cost/BrokerInputCostTest.java
+++ b/app/src/test/java/org/astraea/app/cost/BrokerInputCostTest.java
@@ -20,11 +20,11 @@ import java.util.List;
 import java.util.Map;
 import org.astraea.app.admin.ClusterBean;
 import org.astraea.app.admin.ClusterInfo;
+import org.astraea.app.metrics.BeanObject;
 import org.astraea.app.metrics.KafkaMetrics;
 import org.astraea.app.metrics.broker.BrokerTopicMetricsResult;
 import org.astraea.app.metrics.collector.BeanCollector;
 import org.astraea.app.metrics.collector.Receiver;
-import org.astraea.app.metrics.jmx.BeanObject;
 import org.astraea.app.service.RequireBrokerCluster;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
diff --git a/app/src/test/java/org/astraea/app/cost/BrokerOutputCostTest.java b/app/src/test/java/org/astraea/app/cost/BrokerOutputCostTest.java
index 59f8bd55..0e736aa3 100644
--- a/app/src/test/java/org/astraea/app/cost/BrokerOutputCostTest.java
+++ b/app/src/test/java/org/astraea/app/cost/BrokerOutputCostTest.java
@@ -20,11 +20,11 @@ import java.util.List;
 import java.util.Map;
 import org.astraea.app.admin.ClusterBean;
 import org.astraea.app.admin.ClusterInfo;
+import org.astraea.app.metrics.BeanObject;
 import org.astraea.app.metrics.KafkaMetrics;
 import org.astraea.app.metrics.broker.BrokerTopicMetricsResult;
 import org.astraea.app.metrics.collector.BeanCollector;
 import org.astraea.app.metrics.collector.Receiver;
-import org.astraea.app.metrics.jmx.BeanObject;
 import org.astraea.app.service.RequireBrokerCluster;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
diff --git a/app/src/test/java/org/astraea/app/cost/LoadCostTest.java b/app/src/test/java/org/astraea/app/cost/LoadCostTest.java
index 113beea4..508d1654 100644
--- a/app/src/test/java/org/astraea/app/cost/LoadCostTest.java
+++ b/app/src/test/java/org/astraea/app/cost/LoadCostTest.java
@@ -20,10 +20,10 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import org.astraea.app.admin.ClusterBean;
+import org.astraea.app.metrics.BeanObject;
 import org.astraea.app.metrics.HasBeanObject;
 import org.astraea.app.metrics.KafkaMetrics;
 import org.astraea.app.metrics.broker.BrokerTopicMetricsResult;
-import org.astraea.app.metrics.jmx.BeanObject;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
diff --git a/app/src/test/java/org/astraea/app/cost/NodeLatencyCostTest.java b/app/src/test/java/org/astraea/app/cost/NodeLatencyCostTest.java
index cd1de97d..1944d3f7 100644
--- a/app/src/test/java/org/astraea/app/cost/NodeLatencyCostTest.java
+++ b/app/src/test/java/org/astraea/app/cost/NodeLatencyCostTest.java
@@ -24,10 +24,10 @@ import org.astraea.app.admin.Admin;
 import org.astraea.app.admin.ClusterBean;
 import org.astraea.app.admin.ClusterInfo;
 import org.astraea.app.common.Utils;
+import org.astraea.app.metrics.BeanObject;
 import org.astraea.app.metrics.HasBeanObject;
 import org.astraea.app.metrics.KafkaMetrics;
-import org.astraea.app.metrics.jmx.BeanObject;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 import org.astraea.app.producer.Producer;
 import org.astraea.app.service.RequireBrokerCluster;
 import org.junit.jupiter.api.Assertions;
diff --git a/app/src/test/java/org/astraea/app/cost/NodeThroughputCostTest.java b/app/src/test/java/org/astraea/app/cost/NodeThroughputCostTest.java
index 39fb0f73..eb11d7c5 100644
--- a/app/src/test/java/org/astraea/app/cost/NodeThroughputCostTest.java
+++ b/app/src/test/java/org/astraea/app/cost/NodeThroughputCostTest.java
@@ -20,8 +20,8 @@ import java.util.List;
 import java.util.Map;
 import org.astraea.app.admin.ClusterBean;
 import org.astraea.app.admin.ClusterInfo;
-import org.astraea.app.metrics.jmx.BeanObject;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.BeanObject;
+import org.astraea.app.metrics.MBeanClient;
 import org.astraea.app.metrics.producer.HasProducerNodeMetrics;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
diff --git a/app/src/test/java/org/astraea/app/cost/NodeTopicSizeCostTest.java b/app/src/test/java/org/astraea/app/cost/NodeTopicSizeCostTest.java
index f24b81ba..2fe69603 100644
--- a/app/src/test/java/org/astraea/app/cost/NodeTopicSizeCostTest.java
+++ b/app/src/test/java/org/astraea/app/cost/NodeTopicSizeCostTest.java
@@ -20,8 +20,8 @@ import java.util.List;
 import java.util.Map;
 import org.astraea.app.admin.ClusterBean;
 import org.astraea.app.admin.ClusterInfo;
+import org.astraea.app.metrics.BeanObject;
 import org.astraea.app.metrics.broker.LogMetrics;
-import org.astraea.app.metrics.jmx.BeanObject;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
diff --git a/app/src/test/java/org/astraea/app/cost/ReplicaDiskInCostTest.java b/app/src/test/java/org/astraea/app/cost/ReplicaDiskInCostTest.java
index e4ee1f2d..fdf653ff 100644
--- a/app/src/test/java/org/astraea/app/cost/ReplicaDiskInCostTest.java
+++ b/app/src/test/java/org/astraea/app/cost/ReplicaDiskInCostTest.java
@@ -26,10 +26,10 @@ import org.astraea.app.admin.ClusterInfo;
 import org.astraea.app.admin.NodeInfo;
 import org.astraea.app.admin.ReplicaInfo;
 import org.astraea.app.admin.TopicPartition;
+import org.astraea.app.metrics.BeanObject;
 import org.astraea.app.metrics.HasBeanObject;
 import org.astraea.app.metrics.broker.HasValue;
 import org.astraea.app.metrics.broker.LogMetrics;
-import org.astraea.app.metrics.jmx.BeanObject;
 import org.astraea.app.service.RequireBrokerCluster;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
diff --git a/app/src/test/java/org/astraea/app/cost/ReplicaLeaderCostTest.java b/app/src/test/java/org/astraea/app/cost/ReplicaLeaderCostTest.java
index e3e37a49..c8930ec3 100644
--- a/app/src/test/java/org/astraea/app/cost/ReplicaLeaderCostTest.java
+++ b/app/src/test/java/org/astraea/app/cost/ReplicaLeaderCostTest.java
@@ -24,10 +24,10 @@ import org.astraea.app.admin.ClusterBean;
 import org.astraea.app.admin.ClusterInfo;
 import org.astraea.app.admin.NodeInfo;
 import org.astraea.app.admin.ReplicaInfo;
+import org.astraea.app.metrics.BeanObject;
 import org.astraea.app.metrics.HasBeanObject;
 import org.astraea.app.metrics.KafkaMetrics;
 import org.astraea.app.metrics.broker.HasValue;
-import org.astraea.app.metrics.jmx.BeanObject;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
diff --git a/app/src/test/java/org/astraea/app/metrics/jmx/BeanObjectTest.java b/app/src/test/java/org/astraea/app/metrics/BeanObjectTest.java
similarity index 98%
rename from app/src/test/java/org/astraea/app/metrics/jmx/BeanObjectTest.java
rename to app/src/test/java/org/astraea/app/metrics/BeanObjectTest.java
index da017f70..fa00632d 100644
--- a/app/src/test/java/org/astraea/app/metrics/jmx/BeanObjectTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/BeanObjectTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.astraea.app.metrics.jmx;
+package org.astraea.app.metrics;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
diff --git a/app/src/test/java/org/astraea/app/metrics/jmx/BeanQueryTest.java b/app/src/test/java/org/astraea/app/metrics/BeanQueryTest.java
similarity index 99%
rename from app/src/test/java/org/astraea/app/metrics/jmx/BeanQueryTest.java
rename to app/src/test/java/org/astraea/app/metrics/BeanQueryTest.java
index e96070fd..3c7958a8 100644
--- a/app/src/test/java/org/astraea/app/metrics/jmx/BeanQueryTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/BeanQueryTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.astraea.app.metrics.jmx;
+package org.astraea.app.metrics;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
diff --git a/app/src/test/java/org/astraea/app/metrics/KafkaMetricsTest.java b/app/src/test/java/org/astraea/app/metrics/KafkaMetricsTest.java
index 5632e7d2..cc16cf33 100644
--- a/app/src/test/java/org/astraea/app/metrics/KafkaMetricsTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/KafkaMetricsTest.java
@@ -40,8 +40,6 @@ import org.astraea.app.common.Utils;
 import org.astraea.app.metrics.broker.BrokerTopicMetricsResult;
 import org.astraea.app.metrics.broker.LogMetrics;
 import org.astraea.app.metrics.broker.TotalTimeMs;
-import org.astraea.app.metrics.jmx.BeanObject;
-import org.astraea.app.metrics.jmx.MBeanClient;
 import org.astraea.app.metrics.platform.JvmMemory;
 import org.astraea.app.metrics.platform.OperatingSystemInfo;
 import org.astraea.app.service.RequireBrokerCluster;
diff --git a/app/src/test/java/org/astraea/app/metrics/jmx/MBeanClientTest.java b/app/src/test/java/org/astraea/app/metrics/MBeanClientTest.java
similarity index 99%
rename from app/src/test/java/org/astraea/app/metrics/jmx/MBeanClientTest.java
rename to app/src/test/java/org/astraea/app/metrics/MBeanClientTest.java
index cf8e8553..9d8af25d 100644
--- a/app/src/test/java/org/astraea/app/metrics/jmx/MBeanClientTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/MBeanClientTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.astraea.app.metrics.jmx;
+package org.astraea.app.metrics;
 
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toMap;
diff --git a/app/src/test/java/org/astraea/app/metrics/MetricExplorerTest.java b/app/src/test/java/org/astraea/app/metrics/MetricExplorerTest.java
index 9230730d..578d7c75 100644
--- a/app/src/test/java/org/astraea/app/metrics/MetricExplorerTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/MetricExplorerTest.java
@@ -31,8 +31,6 @@ import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.astraea.app.argument.Argument;
-import org.astraea.app.metrics.jmx.BeanObject;
-import org.astraea.app.metrics.jmx.MBeanClient;
 import org.junit.jupiter.api.function.Executable;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
diff --git a/app/src/test/java/org/astraea/app/metrics/jmx/Utility.java b/app/src/test/java/org/astraea/app/metrics/Utility.java
similarity index 99%
rename from app/src/test/java/org/astraea/app/metrics/jmx/Utility.java
rename to app/src/test/java/org/astraea/app/metrics/Utility.java
index 875d2ec8..2588b23e 100644
--- a/app/src/test/java/org/astraea/app/metrics/jmx/Utility.java
+++ b/app/src/test/java/org/astraea/app/metrics/Utility.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.astraea.app.metrics.jmx;
+package org.astraea.app.metrics;
 
 import java.util.Map;
 import javax.management.Attribute;
diff --git a/app/src/test/java/org/astraea/app/metrics/broker/LogMetricsTest.java b/app/src/test/java/org/astraea/app/metrics/broker/LogMetricsTest.java
index 54e381ba..a5288fd6 100644
--- a/app/src/test/java/org/astraea/app/metrics/broker/LogMetricsTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/broker/LogMetricsTest.java
@@ -22,7 +22,7 @@ import java.util.stream.Collectors;
 import org.astraea.app.admin.Admin;
 import org.astraea.app.common.Utils;
 import org.astraea.app.metrics.HasBeanObject;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 import org.astraea.app.service.RequireSingleBrokerCluster;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
diff --git a/app/src/test/java/org/astraea/app/metrics/broker/ServerMetricsTest.java b/app/src/test/java/org/astraea/app/metrics/broker/ServerMetricsTest.java
index b5312648..d1362fb5 100644
--- a/app/src/test/java/org/astraea/app/metrics/broker/ServerMetricsTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/broker/ServerMetricsTest.java
@@ -16,7 +16,7 @@
  */
 package org.astraea.app.metrics.broker;
 
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.EnumSource;
diff --git a/app/src/test/java/org/astraea/app/metrics/collector/BeanCollectorTest.java b/app/src/test/java/org/astraea/app/metrics/collector/BeanCollectorTest.java
index c7ed83a1..e82bfc50 100644
--- a/app/src/test/java/org/astraea/app/metrics/collector/BeanCollectorTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/collector/BeanCollectorTest.java
@@ -25,10 +25,10 @@ import java.util.function.BiFunction;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import org.astraea.app.common.Utils;
+import org.astraea.app.metrics.BeanObject;
 import org.astraea.app.metrics.HasBeanObject;
 import org.astraea.app.metrics.KafkaMetrics;
-import org.astraea.app.metrics.jmx.BeanObject;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
diff --git a/app/src/test/java/org/astraea/app/metrics/collector/FetcherTest.java b/app/src/test/java/org/astraea/app/metrics/collector/FetcherTest.java
index 4702cb0b..f04dbcc8 100644
--- a/app/src/test/java/org/astraea/app/metrics/collector/FetcherTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/collector/FetcherTest.java
@@ -19,7 +19,7 @@ package org.astraea.app.metrics.collector;
 import java.util.List;
 import java.util.Optional;
 import org.astraea.app.metrics.HasBeanObject;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
diff --git a/app/src/test/java/org/astraea/app/metrics/producer/HasProducerNodeMetricsTest.java b/app/src/test/java/org/astraea/app/metrics/producer/HasProducerNodeMetricsTest.java
index 75f2ed21..6d4ca965 100644
--- a/app/src/test/java/org/astraea/app/metrics/producer/HasProducerNodeMetricsTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/producer/HasProducerNodeMetricsTest.java
@@ -24,7 +24,7 @@ import org.astraea.app.admin.Admin;
 import org.astraea.app.admin.TopicPartition;
 import org.astraea.app.common.Utils;
 import org.astraea.app.metrics.KafkaMetrics;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 import org.astraea.app.producer.Producer;
 import org.astraea.app.service.RequireBrokerCluster;
 import org.junit.jupiter.api.Assertions;
diff --git a/app/src/test/java/org/astraea/app/metrics/producer/HasProducerTopicMetricsTest.java b/app/src/test/java/org/astraea/app/metrics/producer/HasProducerTopicMetricsTest.java
index 067aeabb..df49dcd0 100644
--- a/app/src/test/java/org/astraea/app/metrics/producer/HasProducerTopicMetricsTest.java
+++ b/app/src/test/java/org/astraea/app/metrics/producer/HasProducerTopicMetricsTest.java
@@ -19,7 +19,7 @@ package org.astraea.app.metrics.producer;
 import java.util.concurrent.ExecutionException;
 import org.astraea.app.common.Utils;
 import org.astraea.app.metrics.KafkaMetrics;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 import org.astraea.app.producer.Producer;
 import org.astraea.app.service.RequireSingleBrokerCluster;
 import org.junit.jupiter.api.Assertions;
diff --git a/app/src/test/java/org/astraea/app/service/RequireJmxServerTest.java b/app/src/test/java/org/astraea/app/service/RequireJmxServerTest.java
index f2289573..b63afb5c 100644
--- a/app/src/test/java/org/astraea/app/service/RequireJmxServerTest.java
+++ b/app/src/test/java/org/astraea/app/service/RequireJmxServerTest.java
@@ -17,9 +17,9 @@
 package org.astraea.app.service;
 
 import java.util.Set;
+import org.astraea.app.metrics.BeanQuery;
 import org.astraea.app.metrics.KafkaMetrics;
-import org.astraea.app.metrics.jmx.BeanQuery;
-import org.astraea.app.metrics.jmx.MBeanClient;
+import org.astraea.app.metrics.MBeanClient;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;