Java SE基礎鞏固(十三):JMX

1 概述

不少Java相關的基礎書籍(包括Java核心技術I、II)都沒有涉及到JMX的內容,以致於大多數Java學習者對JMX都不甚瞭解,但這個東西確實是Java SE的一部分,不少性能監控軟件都是基於JMX規範開發的,例如jconsole。html

JMX即Java Management Extensions(Java管理擴展),是Java SE的一部分,在Java2的時候加入到Java SE平臺中,但Java5才正式發佈。JMX提供了一個很是簡單的途徑去管理應用程序的資源,這裏的所說的資源包括內存資源,磁盤資源等,並且由於JMX相關技術是動態的,因此能夠在應用程序運行時監控和管理資源。java

JMX主要由三大部分構成:shell

  • Managed Beans (MBean)表明了某種資源,MBean能夠暴露一些接口,用於對MBean所表明的資源進行查看、修改等操做。安全

  • JMX agents (JMX代理)管理着一個或多個MBean,並將MBean暴露給客戶端。JMX代理的核心組件是MBean Server,用於接受客戶端的鏈接,並對客戶端的請求作出響應。服務器

  • Remoete Connectors 能夠簡單理解成客戶端,最貼近用戶,用於接受用戶的請求,並將請求發送給JMX代理,而後將JMX代理的響應返回給用戶。網絡

舉個例子,假設公司如今有一大堆糧食(有不少種類)須要管理,並設計了一套方案用來管理糧食。他們是這樣設計的:用一類特殊的對象來這些糧食而且搭建了一個服務器,該服務器管理着這些糧食,並且還開發了一個對用戶友好的程序供用戶查看,購買各類各樣的糧食。在這個例子中,用來表示糧食的對象就是MBean,而服務器就是JMX Aagent,對用戶友好的程序其實就是客戶端。架構

例子可能不太恰當,但願各位能理解其中的意思。oracle

下面是JMX的架構圖:app

idoI7n.png

接下來我將就圍繞這三大部分介紹JMX技術。dom

2 Managed Beans

Managed Beans簡稱MBean(後面就統一使用MBean了),一個MBean是一個被管理的Java對象,它和Java Bean有些類似,但不是徹底等同,一個標準的MBean包含了如下內容:

  • 一系列可讀的或者可寫或者既可讀又可寫的屬性。
  • 一系列可被調用的操做(方法)。
  • 描述本身的信息。

JMX規範定義了五種類型的MBean,分別是:

  • Standard MBeans
  • Dynamic MBeans
  • Open MBeans
  • Model MBeans
  • MXBeans

本文主要介紹的是Standard MBeans,至於其餘類型的MBean,讀者若是感興趣,能夠自行查找資料學習。

2.1 標準的MBean

下面是一個「標準」的MBean:

public interface HelloMBean {

    void sayHello();

    int add(int x, int y);

    String getName();

    void setCacheSize(int cacheSize);

    int getCacheSize();
}
public class Hello implements HelloMBean {

    private final String name = "Yeonon";

    private static final int DEFAULT_CACHE_SIZE = 200;
    private int cacheSize = DEFAULT_CACHE_SIZE;

    @Override
    public void sayHello() {
        System.out.println("hello, world!");
    }

    @Override
    public int add(int x, int y) {
        return x + y;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void setCacheSize(int cacheSize) {
        int oldValue = this.cacheSize;
        this.cacheSize = cacheSize;
        System.out.println("Cache size now is : " + this.cacheSize);
    }

    @Override
    public int getCacheSize() {
        return this.cacheSize;
    }
}
複製代碼

標準的MBean一般都包含了一個以MBean爲後綴的接口,以及其實現類,例如這裏的HelloMBean以及其實現類Hello,在接口裏定義了一系列的抽象方法,這些方法都是後面被暴露的,getter和setter方法的最主要做用是來表示某個字段的可讀性和可寫性,若是某個字段只包含了getter方法而沒有包含setter方法,那麼該字段就只是可讀的,即沒法經過JMX技術來修改該屬性。在示例代碼中,只定義了個操做,即sayHello和add(getter和setter雖然也是方法,但不把他們認爲是操做),客戶端能夠在遠程執行這些操做(調用這些方法),後面會看到一個具體的表現,在這裏先不說。

2.2 通知

JMX還支持事件通知。當MBean的屬性被修改、刪除或者出現問題的時候,能夠生成一個通知,並將其發送出去給,若是客戶端包含了通知監聽器,客戶端就能夠接受該通知並將其呈現給用戶。MBean要支持通知機制,其實現類必須實現NotificationEmitter接口或者繼承NotificationBroadcasterSupport類(推薦繼承NotificationBroadcasterSupport類,由於該類已經實現了不少有用的方法),對上面的Hello類進行修改,修改以後的代碼以下所示:

public class Hello extends NotificationBroadcasterSupport implements HelloMBean {

    private final String name = "Yeonon";

    private final AtomicLong sequenceNumber = new AtomicLong(0);

    private static final int DEFAULT_CACHE_SIZE = 200;
    private int cacheSize = DEFAULT_CACHE_SIZE;

    @Override
    public void sayHello() {
        System.out.println("hello, world!");
    }

    @Override
    public int add(int x, int y) {
        return x + y;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void setCacheSize(int cacheSize) {
        int oldValue = this.cacheSize;
        this.cacheSize = cacheSize;
        System.out.println("Cache size now is : " + this.cacheSize);

        Notification notification = new AttributeChangeNotification(
                this, sequenceNumber.getAndIncrement(), System.currentTimeMillis(),
                "Cache Size changed", "CacheSize", "int",
                oldValue, this.cacheSize);
        sendNotification(notification);
    }

    @Override
    public int getCacheSize() {
        return this.cacheSize;
    }

    @Override
    public MBeanNotificationInfo[] getNotificationInfo() {
        String[] types = new String[] {
                AttributeChangeNotification.ATTRIBUTE_CHANGE
        };
        String name = AttributeChangeNotification.class.getName();
        String description = "An attribute of this MBean has changed";
        MBeanNotificationInfo info = new MBeanNotificationInfo(types, name, description);
        return new MBeanNotificationInfo[]{info};
    }
}

複製代碼

最主要的修改是在setCacheSize方法裏生成了一個Notification對象,該Notification對象就表明一個通知,Notification有不少已經編寫好的實現類,例如這裏的AttributeChangeNotification,由於這裏要發送通知的緣由就是屬性被修改,因此就使用了這個實現類,該實現類的構造器簽名以下所示:

public AttributeChangeNotification(Object source, long sequenceNumber, long timeStamp, String msg, String attributeName, String attributeType, Object oldValue, Object newValue) {

        super(AttributeChangeNotification.ATTRIBUTE_CHANGE, source, sequenceNumber, timeStamp, msg);
        this.attributeName = attributeName;
        this.attributeType = attributeType;
        this.oldValue = oldValue;
        this.newValue = newValue;
    }
複製代碼

一共7個參數,這裏我就說一個參數(其餘參數根據參數名就能夠知道是什麼東西了),即sequenceNumber,從參數名上能夠知道這是一個序列號,每個通知都有一個序列化,主要是爲了防止通知的順序錯誤,和網絡協議中的那個序列化有殊途同歸之妙。例如,若是通知在JMX Agent和遠程客戶端之間的鏈路中傳輸時,發生了順序錯亂,有了序列號,客戶端就能夠知道應該先處理哪一個通知,後處理哪一個通知,避免亂序處理。

通知生成完畢以後,就調用sendNotification()方法將通知發送出去。你可能會有疑問?這個通知發送到哪去呢?答案是發送到客戶端那裏(默認是經過RMI機制發送),若是客戶端有實現通知監聽器,客戶端就能夠接受並處理通知了。

下面來看看JMX代理。

3 JMX代理

JMX代理的核心組件就是一個MBean Server,能夠將MBean註冊到MBean Server中,這就表示MBean由該MBean Server管理了。下面的代碼演示了獲取MBean Server而且將MBean註冊到MBean Server的過程:

public class Main {

    public static void main(String[] args) throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
        MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
        ObjectName name = new ObjectName("top.yeonon.jmx:type=Hello");
        Hello mbean = new Hello();
        mBeanServer.registerMBean(mbean, name);

        System.out.println("waiting forever....");
        while (true) {

        }
    }
}

複製代碼

能夠經過ManagementFactory.getPlatformMBeanServer()來獲取一個MBean Server實例對象,而後調用mBeanServer.registerMBean方法來把一個MBean註冊到MBean Server中,該方法有兩個參數,一個參數是MBean實例對象,第二個參數是對應MBean的名字,是ObjectName類型的對象,這裏的名字其實並不必定就是top.yeonon.jmx:type=Hello,其格式是XXX:type=YYY,XXX和YYY均可以是任意的字符串,並不必定就是包名和類名,不過最好仍是建議使用包名和類名,這樣能夠避免命名衝突。

啓動程序,發現輸出waiting forever....,程序沒有關閉,其實就是進入無限循環了。這裏先不着急,只要程序不包錯就好了,下面我會帶你來點刺激的!

4 JMX客戶端

JMX客戶端是最接近用戶的,用戶也許不知道後面的JMX Agent,MBean,但必定會知道客戶端,JMX客戶端不會直接和MBean打交道,而是經過JMX Agent來操做其管理的MBean。咱們熟知的Jconsole就是一個JMX客戶端,下面先介紹一下如何使用Jconsole來操做MBean,隨後再介紹如何自定義一個JMX客戶端。

4.1 Jconsole

保持JMX Agent處於運行狀態,而後打開jconsole軟件,以下所示:

id7byF.png

選擇對應的本地進程(後面會介紹如何使用遠程進程來訪問),點擊以後看到以下界面:

id7OeJ.png

選擇MBean選項卡,而後找到top.yeonon.jmx選項,這裏會看到有一個名字叫作Hello的MBean(注意到了嗎,其實就是咱們在JMX Agent裏定義的那個ObjectName),如今選擇cacheSize那一欄,在值那裏修改能夠看到當前cacheSize的值是200,修改爲150,並點擊刷新試試,以下圖所示:

id7joR.png

這時候回到控制檯,發現多了一行輸出:

Cache size now is : 150
複製代碼

說明如今cacheSize屬性已經被修改爲150了!咱們成功的在程序運行時修改了屬性。其餘的操做,例如sayHello(),add等,各位能夠自行嘗試,在此就很少說了。

除了屬性和操做,還能夠看到有一個通知,選擇通知那一欄,而後在右下邊能夠看到有一個「訂閱」按鈕,點擊一下,表示想要接受通知,而後再次嘗試修改CacheSize,此時會發現多了一行記錄,以下所示:

idH7tI.png

消息的內容就是咱們以前定義的,Jconsole只是將其展現出來了,並有作過多的處理。

4.2 自定義客戶端

光是用別人開發的客戶端沒啥意思是否是,沒事!JMX也提供了很方便的方式讓咱們編寫自定義的客戶端,下面是一個自定義客戶端的代碼:

package top.yeonon.jmx;

import javax.management.*;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Set;

import static java.lang.Thread.sleep;

/** * @Author yeonon * @date 2018/10/16 0016 20:47 **/
public class Client {

    //構建一個監聽器類,該類須要實現NotificationListener接口並實現handleNotification方法
    public static class ClientListener implements NotificationListener {
        @Override
        public void handleNotification(Notification notification, Object handback) {
            echo("\nReceived notification:");
            echo("\tClassName: " + notification.getClass().getName());
            echo("\tSource: " + notification.getSource());
            echo("\tType: " + notification.getType());
            echo("\tMessage: " + notification.getMessage());
            //若是通知類型是AttributeChangeNotification,那麼就獲取一些和屬性有關的信息
            if (notification instanceof AttributeChangeNotification) {
                AttributeChangeNotification acn = (AttributeChangeNotification) notification;
                echo("\tAttributeName: " + acn.getAttributeName());
                echo("\tAttributeType: " + acn.getAttributeType());
                echo("\tNewValue: " + acn.getNewValue());
                echo("\tOldValue: " + acn.getOldValue());
            }
        }

        public static void main(String[] args) throws IOException, MalformedObjectNameException, InstanceNotFoundException, InterruptedException {
            echo("\nCreate an RMI connector client and " +
                    "connect it to the RMI connector server");
            //構造並獲取RMI鏈接
            JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://:9999/jmxrmi");
            JMXConnector jmxc = JMXConnectorFactory.connect(url, null);

            //獲取MBeanServer的鏈接
            MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
            waitForEnterPressed();

            echo("\nMBeanServer default domain = " + mbsc.getDefaultDomain());
            echo("\nMBean count = " + mbsc.getMBeanCount());
            echo("\nQuery MBeanServer MBeans:");

            Set<ObjectName> objectNames = mbsc.queryNames(null, null);
            for (ObjectName objectName : objectNames) {
                echo("\tObjectName = " + objectName);
            }
            waitForEnterPressed();

            //建立監聽器
            NotificationListener listener = new ClientListener();

            //管理 Hello MBean
            ObjectName mbeanName = new ObjectName("top.yeonon.jmx:type=Hello");
            HelloMBean mbeanProxy = JMX.newMBeanProxy(mbsc, mbeanName, HelloMBean.class, true);
            echo("\nAdd notification listener...");
            mbsc.addNotificationListener(mbeanName, listener, null, null);

            echo("\nCacheSize = " + mbeanProxy.getCacheSize());
            mbeanProxy.setCacheSize(150);
            echo("\nWaiting for notification...");
            sleep(2000);
            echo("\nCacheSize = " + mbeanProxy.getCacheSize());
            echo("\nInvoke sayHello() in Hello MBean...");
            mbeanProxy.sayHello();
            echo("\nInvoke add(2, 3) in Hello MBean...");
            mbeanProxy.add(2,3);

            waitForEnterPressed();

            //關閉客戶端
            echo("\nClose the connection to the server");
            jmxc.close();
            echo("\nBye! Bye!");
        }
    }

    private static void echo(String msg) {
        System.out.println(msg);
    }

    private static void waitForEnterPressed() {
        try {
            echo("\nPress <Enter> to continue...");
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

複製代碼

代碼不算少,我將分幾步來說解:

  1. 先建立一個通知監聽器,須要實現NotificationListener並實現handleNotification方法,能夠在該方法裏對消息進行處理,在演示代碼中只是將消息的各項信息列出來而已。

  2. 建立一個RMI鏈接,即以下兩行代碼:

    //構造一個JMXServiceURL,這裏使用的是RMI的鏈接方式,因此要按照RMI的URL格式,端口是9999,
    JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://:9999/jmxrmi");
    //鏈接遠程的JMX Agent
    JMXConnector jmxc = JMXConnectorFactory.connect(url, null);
    複製代碼
  3. 獲取MBeanServer鏈接,使用jmxc.getMBeanServerConnection()方法來獲取,下面的操做大多數都是基於該MBeanServer鏈接。

  4. 若是有須要的話,能夠MBeanServerConnection裏取出各類信息,例如MBean的數量以及具體的MBean的名字(使用MBeanServerConnection.queryNames)。

  5. 獲取想要管理的MBean代理(這涉及到了代理模式)。使用JMX.newMBeanProxy()方法來生成一個MBean的帶來,以後就可使用該MBean代理對具體的MBean進行操做了(例如修改CacheSize等)。

  6. 若是須要接收通知,就使用MBeanServerConnection.addNotificationListener()方法爲特定的MBean綁定一個監聽器。

  7. 關閉客戶端可使用JMXConnector.close()方法。

在運行程序以前,須要先運行JMX Agent程序,因爲這裏是使用RMI進行遠程鏈接,而且鏈接的端口號是9999,若是直接再也不參數啓動的話,客戶端將沒法鏈接到JMX Agent,須要添加的參數有三個,分別是:

  • -Dcom.sun.management.jmxremote.port=9999,即開放的接口是9999。
  • -Dcom.sun.management.jmxremote.authenticate=false,false表示不須要用戶認證。
  • -Dcom.sun.management.jmxremote.ssl=false,false表示不適用SSL安全鏈接。

加入參數啓動以後,就能夠嘗試啓動客戶端了,其輸出大體以下:

Create an RMI connector client and connect it to the RMI connector server

Press <Enter> to continue...

#我按個回車

MBeanServer default domain = DefaultDomain

#遠端的系統中一共有23個MBean
MBean count = 23

#23個MBean的名字
Query MBeanServer MBeans:
	ObjectName = java.lang:type=MemoryPool,name=Metaspace
	ObjectName = java.lang:type=MemoryPool,name=PS Old Gen
	ObjectName = java.lang:type=GarbageCollector,name=PS Scavenge
	ObjectName = java.lang:type=MemoryPool,name=PS Eden Space
	ObjectName = JMImplementation:type=MBeanServerDelegate
	ObjectName = java.lang:type=Runtime
	ObjectName = java.lang:type=Threading
	ObjectName = java.lang:type=OperatingSystem
	ObjectName = java.lang:type=MemoryPool,name=Code Cache
	ObjectName = java.nio:type=BufferPool,name=direct
	ObjectName = java.lang:type=Compilation
	ObjectName = top.yeonon.jmx:type=Hello
	ObjectName = java.lang:type=MemoryManager,name=CodeCacheManager
	ObjectName = java.lang:type=MemoryPool,name=Compressed Class Space
	ObjectName = java.lang:type=Memory
	ObjectName = java.nio:type=BufferPool,name=mapped
	ObjectName = java.util.logging:type=Logging
	ObjectName = java.lang:type=MemoryPool,name=PS Survivor Space
	ObjectName = java.lang:type=ClassLoading
	ObjectName = java.lang:type=MemoryManager,name=Metaspace Manager
	ObjectName = com.sun.management:type=DiagnosticCommand
	ObjectName = java.lang:type=GarbageCollector,name=PS MarkSweep
	ObjectName = com.sun.management:type=HotSpotDiagnostic

Press <Enter> to continue...

#我按個回車

Add notification listener...

CacheSize = 100

Waiting for notification...

#監聽器收到了通知
Received notification:
	ClassName: javax.management.AttributeChangeNotification
	Source: top.yeonon.jmx:type=Hello
	Type: jmx.attribute.change
	Message: Cache Size changed
	AttributeName: CacheSize
	AttributeType: int
	NewValue: 150
	OldValue: 100


CacheSize = 150

#調用了sayHello()方法
Invoke sayHello() in Hello MBean...

#調用了add()方法
Invoke add(2, 3) in Hello MBean...

Press <Enter> to continue...
#回車

Close the connection to the server

Bye! Bye!
複製代碼

好了,打完收工!

5 小結

本文簡單介紹了JMX的三大組成部分,MBean,JMX Agent,Client,但只使用到了JMX的部分功能,其實JMX遠不止如此,還有不少更加高級的功能,例如擴展JMX等,但願本文能對讀者有一些幫助。

若是文章一些地方說的有問題,望指正。由於我也是初學JMX,算是邊學邊寫的。

Ps: 本文的樣例代碼都是來自Oracle JMX Tutorial,下面的參考資料中給出了連接。

6 參考資料

Oracle JMX Tutorial

相關文章
相關標籤/搜索