1. 概述

调试对于开发者来说可能耗费大量时间。为了提高调试效率,可以使用JMX(Java Management Extensions),这是一种针对Java应用的监控和管理技术。

本文将指导您如何使用JMXTerm对Java应用程序进行外部调试。

2. JMXTerm

JMX为Java应用提供了多种工具,如JConsoleVisualVM和JMXTerm。JConsole是一个性能监控图形界面,VisualVM提供高级调试和性能分析功能,但需要插件支持MBeans。尽管这些工具都很实用,但JMXTerm是一个轻量级、灵活且命令行的选项,可用于自动化操作。

2.1. 安装与配置

要使用JMXTerm,首先需要下载并安装。最新版本的JMXTerm可以从官方网站获取,它打包在一个简单的.jar文件中:

$ java -jar jmxterm.jar

请注意,对于后续的Java版本,可能会遇到问题,因为模块化JDK对反射访问有限制。解决此问题的一种方法是使用--add-exports

$ java --add-exports jdk.jconsole/sun.tools.jconsole=ALL-UNNAMED -jar jmxterm.jar

2.2. 连接

启动JMXTerm后,可以通过主机名和端口连接到Java应用。注意,这可能需要额外步骤来配置或查找所需的端口:

$ open [host]:[port]

也可以在启动时直接传递地址:

$ java -jar jmxterm.jar -l [host]:[port]

另一种选择是使用PID来访问并建立连接。JMXTerm允许我们直接通过jvms命令查找:

$ jvms
83049    (m) - com.baeldung.jmxterm.GameRunner
$ open 83049
#Connection to 83049 is opened

2.3. MBeans

MBeans是通过JMX管理的Java对象,它们提供了一个接口,可通过JMX代理访问和控制,从而简化问题诊断和排错。

创建MBean时,应遵循约定,首先创建一个以MBean结尾的接口。例如,在我们的案例中,将是GuessGameMBean。这意味着类名应该是GuessGame,而不是其他任何名称。另外,有时也可以使用MXBeans。接口中包含我们希望暴露给JMX的操作:

public interface GuessGameMBean {
    void finishGame();
    void pauseGame();
    void unpauseGame();
}

游戏本身是一个简单的猜数字游戏:

public class GuessGame extends GuessGameMBean {
    //...
    public void start() {
        int randomNumber = randomNumbergenerator.getLastNumber();
        while (!isFinished) {
            waitASecond();
            while (!isPaused && !isFinished) {
                log.info("Current random number is " + randomNumber);
                waitASecond();
                for (Player player : players) {
                    int guess = player.guessNumber();
                    if (guess == randomNumber) {
                        log.info("Players " + player.getName() + " " + guess + " is correct");
                        player.incrementScore();
                        notifyAboutWinner(player);
                        randomNumber = randomNumbergenerator.generateRandomNumber();
                        break;
                    }
                    log.info("Player " + player.getName() + " guessed incorrectly with " + guess);
                }
                log.info("\n");
            }
            if (isPaused) {
                log.info("Game is paused");
            }
            if (isFinished) {
                log.info("Game is finished");
            }
        }
    }
    //...
}

我们还会通过JMX跟踪玩家:

public interface PlayerMBean {
    int getGuess();
    int getScore();
    String getName();
}

3. 通过JMXTerm进行调试

一旦通过JMXTerm连接到Java应用,我们可以查询可用的域:

$ domains
#following domains are available
JMImplementation
com.baeldung.jmxterm
com.sun.management
java.lang
java.nio
java.util.logging
jdk.management.jfr

3.1. 日志级别调整

让我们尝试改变正在运行应用的日志级别。我们将使用java.util.logging.Logger进行演示,但请注意JUL存在一些显著的缺点JUL默认提供MBeans:

现在我们可以查看域中的MBeans:

$ beans
#domain = java.util.logging:
java.util.logging:type=Logging

接下来,我们需要检查日志器提供的信息:

$ bean java.util.logging:type=Logging
#bean is set to java.util.logging:type=Logging
$ info
#mbean = java.util.logging:type=Logging
#class name = sun.management.ManagementFactoryHelper$PlatformLoggingImpl
# attributes
  %0   - LoggerNames ([Ljava.lang.String;, r)
  %1   - ObjectName (javax.management.ObjectName, r)
# operations
  %0   - java.lang.String getLoggerLevel(java.lang.String p0)
  %1   - java.lang.String getParentLoggerName(java.lang.String p0)
  %2   - void setLoggerLevel(java.lang.String p0,java.lang.String p1)
#there's no notifications

为了访问GuessGame对象中的日志器,我们需要找到日志器的名称:

$ get LoggerNames
#mbean = java.util.logging:type=Logging:
LoggerNames = [ ..., com.baeldung.jmxterm.GuessGame, ...];

最后,检查日志级别:

$ run getLoggerLevel com.baeldung.jmxterm.GuessGame
#calling operation getLoggerLevel of mbean java.util.logging:type=Logging with params [com.baeldung.jmxterm.GuessGame]
#operation returns:
WARNING

要更改它,只需调用带有参数的setter方法:

$ run setLoggerLevel com.baeldung.jmxterm.GuessGame INFO

3.2. 调整域豆

让我们尝试在应用外部停止游戏。步骤与日志级别调整的例子类似:

$ domain com.baeldung.jmxterm
#domain is set to com.baeldung.jmxterm
$ beans
#domain = com.baeldung.jmxterm:
com.baeldung.jmxterm:id=singlegame,type=game
$ bean com.baeldung.jmxterm:id=singlegame,type=game
#bean is set to com.baeldung.jmxterm:id=singlegame,type=game
$ info
#mbean = com.baeldung.jmxterm:id=singlegame,type=game
#class name = com.baeldung.jmxterm.GuessGame
#there is no attribute
# operations
  %0   - void finishGame()
  %1   - void pauseGame()
  %2   - void unpauseGame()
#there's no notifications
$ run pauseGame
#calling operation pauseGame of mbean com.baeldung.jmxterm:id=singlegame,type=game with params []

我们将在输出中看到游戏已暂停:

...
Apr 14, 2023 12:17:01 PM com.baeldung.jmxterm.GuessGame start
INFO: Game is paused
Apr 14, 2023 12:17:02 PM com.baeldung.jmxterm.GuessGame start
INFO: Game is paused
Apr 14, 2023 12:17:03 PM com.baeldung.jmxterm.GuessGame start
INFO: Game is paused
Apr 14, 2023 12:17:04 PM com.baeldung.jmxterm.GuessGame start
INFO: Game is paused
...

此外,我们还可以结束游戏:

$ run finishGame

输出应包含游戏结束的信息:

...
Apr 14, 2023 12:17:47 PM com.baeldung.jmxterm.GuessGame start
INFO: Game is finished

3.3. watch 命令

此外,我们还可以使用watch命令追踪属性值:

$ info
# attributes
#mbean = com.baeldung.jmxterm:id=Bobd661ee89-b972-433c-adff-93e7495c7e0a,type=player
#class name = com.baeldung.jmxterm.Player
#there's no operations
#there's no notifications
  %0   - Guess (int, r)
  %1   - Name (java.lang.String, r)
  %2   - Score (int, r)
$ watch Score
#press any key to stop. DO NOT press Ctrl+C !!!
683683683683683683683

原始的watch输出很难阅读,但我们可提供格式化选项:

$ watch --format Score\\ {0}\\  Score
#press any key to stop. DO NOT press Ctrl+C !!!
Score 707 Score 707 Score 707 Score 707 Score 707 

然而,我们还可以通过--report--stopafter选项进一步优化:

$ watch --report --stopafter 10 --format The\\ score\\ is\\ {0} Score
The score is 727
The score is 727
The score is 727
The score is 728
The score is 728

3.4. 通知

另一个强大的调试特性是MBeans通知。但这需要我们在代码中做少许改动。首先,我们需要实现javax.management.NotificationBroadcaster接口:

public interface NotificationBroadcaster {
    void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback)
      throws java.lang.IllegalArgumentException;
    void removeNotificationListener(NotificationListener listener)
      throws ListenerNotFoundException;
    MBeanNotificationInfo[] getNotificationInfo();
}

为了发送关于赢家的通知,我们将使用javax.management.NotificationBroadcasterSupport

public abstract class BroadcastingGuessGame implements NotificationBroadcaster, GuessGameMBean {
    private NotificationBroadcasterSupport broadcaster =
      new NotificationBroadcasterSupport();

    private long notificationSequence = 0;

    private MBeanNotificationInfo[] notificationInfo;

    public BroadcastingGuessGame() {
        this.notificationInfo = new MBeanNotificationInfo[]{ 
            new MBeanNotificationInfo(new String[]{"game"}, Notification.class.getName(),"Game notification") 
        };
    }

    protected void notifyAboutWinner(Player winner) {
        String message = "Winner is " + winner.getName() + " with score " + winner.getScore();
        Notification notification = new Notification("game.winner", this, notificationSequence++, message);
        broadcaster.sendNotification(notification);
    }

    public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) {
        broadcaster.addNotificationListener(listener, filter, handback);
    }

    public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException {
        broadcaster.removeNotificationListener(listener);
    }

    public MBeanNotificationInfo[] getNotificationInfo() {
        return notificationInfo;
    }
}

然后,我们可以在bean上看到通知:

$ bean com.baeldung.jmxterm:id=singlegame,type=game
#bean is set to com.baeldung.jmxterm:id=singlegame,type=game
$ info
#mbean = com.baeldung.jmxterm:id=singlegame,type=game
#class name = com.baeldung.jmxterm.GuessGame
#there is no attribute
# operations
  %0   - void finishGame()
  %1   - void pauseGame()
  %2   - void unpauseGame()
# notifications
  %0   - javax.management.Notification(game.winner)

接着,我们可以订阅通知:

$ subscribe
#Subscribed to com.baeldung.jmxterm:id=singlegame,type=game
notification received: ...,message=Winner is John with score 10
notification received: ...,message=Winner is Alice with score 9
notification received: ...,message=Winner is Bob with score 13
notification received: ...,message=Winner is Bob with score 14
notification received: ...,message=Winner is John with score 11

通过提供--domain--bean选项,我们可以订阅多个bean。

4. 结论

JMXTerm是一个强大的工具,用于通过JMX管理和监控Java应用。它提供了一个命令行界面,让开发者和管理员能够快速轻松地执行任务,如监控属性值、调用操作和更改配置设置。

本文示例的源代码可在GitHub上找到。