使用JMXTerm进行外部调试

2023/07/04

1. 概述

对于开发人员来说,调试可能非常耗时,使调试过程更高效的方法之一是使用JMX(Java Management Extension),这是一种用于Java应用程序的监视和管理技术。

在本教程中,我们将介绍如何使用JMXTerm对Java应用程序执行外部调试。

2. JMXTerm

JMX有几个可用于Java应用程序的工具,例如JConsoleVisualVM和JMXTerm。JConsole是一个用于监控性能的图形工具。VisualVM提供高级调试和分析功能,需要一个插件才能与MBean配合使用。虽然这些都是有用的工具,但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) - cn.tuyucheng.taketoday.jmxterm.GameRunner
$ open 83049
#Connection to 83049 is opened

2.3 MBean

MBean是我们可以通过JMX管理和监控的Java对象,它们公开了一个可以通过JMX代理访问和控制的接口,使诊断和故障排除变得更加容易。

要创建MBean,我们应该遵循约定,首先创建一个名称以MBean结尾的接口。在我们的例子中,它将是GuessGameMBean。这意味着该类的名称应该是GuessGame,而不是其他名称。或者,在某些情况下,可以使用MXBeans。该接口包含我们要公开给JMX的操作:

public interface GuessGameMBean {
    void finishGame();

    void pauseGame();

    void unpauseGame();
}

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

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
cn.tuyucheng.taketoday.jmxterm
com.sun.management
java.lang
java.nio
java.util.logging
jdk.management.jfr

3.1 Logger级别

让我们尝试更改正在运行的应用程序的日志记录级别,我们将使用java.util.logging.Logger进行演示,但请注意JUL有很大的缺点。JUL提供开箱即用的MBean

$ domain java.util.logging
#domain is set to java.util.logging

现在我们可以检查域中可用的MBean:

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

下一步,我们需要检查记录器bean提供的信息:

$ 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 = [ ..., cn.tuyucheng.taketoday.jmxterm.GuessGame, ...];

最后,检查日志记录级别:

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

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

$ run setLoggerLevel cn.tuyucheng.taketoday.jmxterm.GuessGame INFO

之后,我们可以观察应用程序的日志:

...
Apr 14, 2023 12:04:30 PM cn.tuyucheng.taketoday.jmxterm.GuessGame start
INFO: Current random number is 7
Apr 14, 2023 12:04:31 PM cn.tuyucheng.taketoday.jmxterm.GuessGame start
INFO: Player Bob guessed incorrectly with 10
Apr 14, 2023 12:04:31 PM cn.tuyucheng.taketoday.jmxterm.GuessGame start
INFO: Player Alice guessed incorrectly with 5
Apr 14, 2023 12:04:31 PM cn.tuyucheng.taketoday.jmxterm.GuessGame start
INFO: Player John guessed incorrectly with 4
...

3.2 使用域Bean

让我们尝试从我们的应用程序外部停止我们的游戏,这些步骤与记录器示例中的步骤相同:

$ domain cn.tuyucheng.taketoday.jmxterm
#domain is set to cn.tuyucheng.taketoday.jmxterm
$ beans
#domain = cn.tuyucheng.taketoday.jmxterm:
cn.tuyucheng.taketoday.jmxterm:id=singlegame,type=game
$ bean cn.tuyucheng.taketoday.jmxterm:id=singlegame,type=game
#bean is set to cn.tuyucheng.taketoday.jmxterm:id=singlegame,type=game
$ info
#mbean = cn.tuyucheng.taketoday.jmxterm:id=singlegame,type=game
#class name = cn.tuyucheng.taketoday.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 cn.tuyucheng.taketoday.jmxterm:id=singlegame,type=game with params []

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

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

另外,我们可以完成游戏:

$ run finishGame

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

...
Apr 14, 2023 12:17:47 PM cn.tuyucheng.taketoday.jmxterm.GuessGame start
INFO: Game is finished

3.3 watch

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

$ info
# attributes
#mbean = cn.tuyucheng.taketoday.jmxterm:id=Bobd661ee89-b972-433c-adff-93e7495c7e0a,type=player
#class name = cn.tuyucheng.taketoday.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 通知

另一个重要的调试功能是MBean通知,但是,这需要对我们的代码进行最少的更改。首先,我们必须实现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 cn.tuyucheng.taketoday.jmxterm:id=singlegame,type=game
#bean is set to cn.tuyucheng.taketoday.jmxterm:id=singlegame,type=game
$ info
#mbean = cn.tuyucheng.taketoday.jmxterm:id=singlegame,type=game
#class name = cn.tuyucheng.taketoday.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 cn.tuyucheng.taketoday.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应用程序。通过为JMX操作提供命令行接口,JMXTerm允许开发人员和管理员快速轻松地执行监控属性值、调用操作和更改配置设置等任务。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章