JMX (Java Management Extensions, JSR 160) 提供統一的管理介面,讓應用程式、系統物件、裝置能被本地或遠程監控與控制。本文實作一個基於 Struts 的 Web 應用,結合 JMX 遠程連接器 (RMI + JNDI)、認證授權、MC4J/jManage 圖形化客戶端,實現「登入統計的實時遠程監控與安全管理」。
ServletContext 應用作用域)LoginStatsMBean 介面 + LoginStats 實作service:jmx:rmi://host:port/jndi/rmi://host:registryPort/serviceName)jmx.remote.x.password.file)ServletContextListener 啟動/停止 JMX Agent 與 RMI Registrypublic class LoginAction extends Action {
private static final String SUCCESS_KEY = "successLogins";
private static final String FAILED_KEY = "failedLogins";
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) {
LoginForm lf = (LoginForm) form;
HttpSession session = request.getSession();
ServletContext ctx = session.getServletContext();
if ("hello".equals(lf.getUsername()) && "world".equals(lf.getPassword())) {
incrementCounter(ctx, SUCCESS_KEY);
return mapping.findForward("success");
} else {
incrementCounter(ctx, FAILED_KEY);
return mapping.findForward("failure");
}
}
private void incrementCounter(ServletContext ctx, String key) {
Integer val = (Integer) ctx.getAttribute(key);
int newVal = (val == null) ? 1 : val + 1;
ctx.setAttribute(key, newVal);
}
}
// 介面命名慣例: 實作類名 + MBean
public interface LoginStatsMBean {
int getFailedLogins();
int getSuccessLogins();
}
public class LoginStats implements LoginStatsMBean {
private final ServletContext ctx;
private static final String SUCCESS_KEY = "successLogins";
private static final String FAILED_KEY = "failedLogins";
public LoginStats(ServletContext ctx) { this.ctx = ctx; }
@Override
public int getFailedLogins() {
Integer val = (Integer) ctx.getAttribute(FAILED_KEY);
return (val == null) ? 0 : val;
}
@Override
public int getSuccessLogins() {
Integer val = (Integer) ctx.getAttribute(SUCCESS_KEY);
return (val == null) ? 0 : val;
}
}
public class JMXAgent {
private static final String DOMAIN = "com.example.jmxapp";
private static final int MBEAN_SERVER_PORT = 9589;
private static final int RMI_REGISTRY_PORT = 1100;
private static final String JMX_USERS_FILE = "jmx_users.properties"; // 格式: user=password
private MBeanServer mbs;
private JMXConnectorServer connectorServer;
private Registry registry;
public JMXAgent(ServletContext ctx) {
// 1. 建立 MBeanServer
mbs = MBeanServerFactory.createMBeanServer(DOMAIN);
try {
mbs.registerMBean(new LoginStats(ctx), new ObjectName(DOMAIN + ":name=LoginStats"));
} catch (Exception e) {
throw new RuntimeException("MBean registration failed", e);
}
// 2. 建立 JMXServiceURL (RMI + JNDI)
JMXServiceURL url = new JMXServiceURL("rmi", null, MBEAN_SERVER_PORT,
"/jndi/rmi://localhost:" + RMI_REGISTRY_PORT + "/jmxapp");
// 3. 安全配置: 密碼檔路徑
String userFile = ctx.getRealPath("/WEB-INF/classes/") + JMX_USERS_FILE;
Map env = new HashMap<>();
env.put("jmx.remote.x.password.file", userFile);
// 4. 建立 ConnectorServer
try {
connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
} catch (IOException e) {
throw new RuntimeException("ConnectorServer creation failed", e);
}
}
public void start() {
try {
// 必須先啟動 RMI Registry
registry = LocateRegistry.createRegistry(RMI_REGISTRY_PORT);
connectorServer.start();
System.out.println("JMX Agent started: " + connectorServer.getAddress());
} catch (IOException e) {
throw new RuntimeException("JMX Agent start failed", e);
}
}
public void stop() {
try {
if (connectorServer != null) connectorServer.stop();
if (registry != null) UnicastRemoteObject.unexportObject(registry, true);
} catch (Exception e) {
System.err.println("JMX Agent stop error: " + e.getMessage());
}
}
}
public class StartupListener implements ServletContextListener {
private JMXAgent agent;
@Override
public void contextInitialized(ServletContextEvent sce) {
agent = new JMXAgent(sce.getServletContext());
agent.start();
// 可選: 將 agent 存入 ServletContext 供其他組件使用
sce.getServletContext().setAttribute("jmxAgent", agent);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
if (agent != null) agent.stop();
}
}
在 web.xml 註冊:
<listener>
<listener-class>com.example.StartupListener</listener-class>
</listener>
jmx_users.properties (放於 WEB-INF/classes)admin=secret123
monitor=viewonly
mc4j.bat/sh。service:jmx:rmi://host:9589/jndi/rmi://localhost:1100/jmxappadmin,Credentials: secret123。com.example.jmxapp → name=LoginStats → Attributes,即可看到 FailedLogins、SuccessLogins 實時數值。jmanage.war 至同一 Tomcat,或獨立運行。public class JMXClient {
public static void main(String[] args) throws Exception {
JMXServiceURL url = new JMXServiceURL(
"service:jmx:rmi://host:9589/jndi/rmi://localhost:1100/jmxapp");
Map env = new HashMap<>();
env.put(JMXConnector.CREDENTIALS, new String[]{"admin", "secret123"});
try (JMXConnector jmxc = JMXConnectorFactory.connect(url, env)) {
MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
ObjectName name = new ObjectName("com.example.jmxapp:name=LoginStats");
while (true) {
int success = (Integer) mbsc.getAttribute(name, "SuccessLogins");
int failed = (Integer) mbsc.getAttribute(name, "FailedLogins");
System.out.printf("Success: %d, Failed: %d%n", success, failed);
Thread.sleep(5000);
}
}
}
}
service:jmx:rmi://: /jndi/rmi:// : / │ │ │ │ │ │ │ └─ RMI Registry 中綁定的服務名 │ │ └─ JNDI 查找協議 (rmi://) 指向 Registry │ └─ JMX RMI 連接器監聽埠 (MBeanServer 實際通訊埠) └─ 固定前綴
常見誤區:以為只需一個埠。實際需兩個埠:mbeanPort (JRMP 通訊) 與 registryPort (RMI Registry 命名服務)。防火牆需同時開放。
| 方式 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
手動 rmiregistry & | 簡單、獨立進程不隨應用重啟 | 需人工操作、埠衝突風險、部署腳本複雜 | 開發測試、單機部署 |
程式化 LocateRegistry.createRegistry(port) | 應用自包含、隨應用生命週期、CI/CD 友好 | 應用崩潰導致 Registry 掛掉、需處理導出/未導出 | 生產環境、容器化部署 |
rmi.ssl 協議,配置 KeyStore/TrustStore,防止憑證與數據明文傳輸。MBeanServerForwarder 攔截 invoke/setAttribute 檢查角色。PasswordAuthenticator 支援密碼加密儲存、更豐富的安全 SPI。jmx_users.properties 放入 WEB-INF/classes,檔案權限 600 (僅擁有者可讀寫)。mbeanPort (9589) + registryPort (1100)。java.rmi.server.hostname 為公網 IP,或使用 SSH 隧道/VPN。JMX 將管理能力內建於 Java 平台,配合 J2SE 5.0 完善的遠程 API,讓 Web 應用輕鬆獲得企業級監控能力。本文示範的「統計計數器 → MBean → RMI/JNDI 連接器 → 密碼認證 → 圖形化客戶端」完整鏈路,可直接套用於生產環境的 JVM 參數監控、資料庫連線池狀態、快取命中率、業務指標儀表板等場景。掌握 JMX,是邁向「可觀測性」架構的關鍵一步。