1. 概述
调试对于开发者来说可能耗费大量时间。为了提高调试效率,可以使用JMX(Java Management Extensions),这是一种针对Java应用的监控和管理技术。
本文将指导您如何使用JMXTerm对Java应用程序进行外部调试。
2. JMXTerm
JMX为Java应用提供了多种工具,如JConsole、VisualVM和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上找到。