Spring 1.2+ 提供優秀的 JMX 整合,但預設配置缺乏細粒度控制。本文展示如何擴展 Spring JMX:透過外部 XML 定義 MBean 元資料 (類似 Hibernate mapping),並為屬性變更、方法執行前後自動發送通知事件,實現零侵入式管理監控。
Spring JMX Extension Architecture
├── MBeanDescriptorEnabledExporter (擴展導出器)
│ └── 創建 ModelMBeanExtension 替代預設 ModelMBean
├── ModelMBeanExtension (擴展 ModelMBean)
│ ├── 重寫 invoke() → 發送 before/after 通知
│ ├── 重寫 setAttribute() → 發送屬性變更通知
│ └── 內建 NotificationInfoMap 快速查找
├── MBeanDescriptorBasedAssembler (自訂裝配器)
│ └── 從 classpath 載入 {ClassName}.mbean.xml 產生 ModelMBeanInfo
└── XML Metadata Model (JDOM 解析)
├── MBeanDescriptor (根模型)
├── AttributeDescriptor, OperationDescriptor, NotificationDescriptor
└── MBeanDescriptorUtil (載入工具)
<?xml version="1.0" encoding="UTF-8"?>
<mbean name="ExampleService" description="Example Service"
type="com.claudeduguay.jmx.demo.server.ExampleService">
<attribute name="propertyValue"
description="Property Value Access"
type="java.lang.String"
readable="true" writable="true" />
<operation name="stopService"
description="Stop Example Service" />
<operation name="startService"
description="Start Example Service" />
<notification name="PropertyValueSet"
types="example.service.set.propertyValue"
description="PropertyValue was set" />
<notification name="BeforeStartService"
types="example.service.before.startService"
description="Example Service is Starting" />
<notification name="AfterStartService"
types="example.service.after.startService"
description="Example Service is Started" />
<notification name="BeforeStopService"
types="example.service.before.stopService"
description="Example Service is Stopping" />
<notification name="AfterStopService"
types="example.service.after.stopService"
description="Example Service is Stopped" />
</mbean>
set.{attributeName}:屬性設定通知before.{methodName}:方法執行前通知after.{methodName}:方法執行後通知{domain}.{type}.{name} (如 example.service.set.propertyValue)public class ModelMBeanExtension extends RequiredModelMBean {
private NotificationInfoMap notificationInfoMap;
private ModelMBeanInfo modelMBeanInfo;
private Object managedBean;
public ModelMBeanExtension() throws MBeanException {}
public ModelMBeanExtension(ModelMBeanInfo modelMBeanInfo) throws MBeanException {
super(modelMBeanInfo);
this.modelMBeanInfo = modelMBeanInfo;
this.notificationInfoMap = new NotificationInfoMap(modelMBeanInfo);
}
@Override
public void setModelMBeanInfo(ModelMBeanInfo modelMBeanInfo) throws MBeanException {
this.modelMBeanInfo = modelMBeanInfo;
this.notificationInfoMap = new NotificationInfoMap(modelMBeanInfo);
super.setModelMBeanInfo(modelMBeanInfo);
}
@Override
public MBeanNotificationInfo[] getNotificationInfo() {
return modelMBeanInfo.getNotifications(); // 僅回傳 XML 定義的通知
}
@Override
public void setManagedResource(Object managedBean, String type)
throws MBeanException, RuntimeOperationsException,
InstanceNotFoundException, InvalidTargetObjectTypeException {
super.setManagedResource(managedBean, type);
this.managedBean = managedBean;
}
// 方法調用攔截:before/after 通知
@Override
public Object invoke(String name, Object[] args, String[] signature)
throws MBeanException, ReflectionException {
maybeSendMethodNotification("before", name);
Object returnValue = super.invoke(name, args, signature);
maybeSendMethodNotification("after", name);
return returnValue;
}
// 屬性設定攔截:屬性變更通知
@Override
public void setAttribute(Attribute attribute)
throws MBeanException, AttributeNotFoundException,
InvalidAttributeValueException, ReflectionException {
try {
Method setter = ModelMBeanUtil.findSetMethod(modelMBeanInfo, managedBean, attribute.getName());
setter.invoke(managedBean, attribute.getValue());
maybeSendAttributeNotification(attribute);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new MBeanException(e);
}
}
@Override
public Object getAttribute(String name) throws MBeanException,
AttributeNotFoundException, ReflectionException {
try {
Method getter = ModelMBeanUtil.findGetMethod(modelMBeanInfo, managedBean, name);
return getter.invoke(managedBean, new Object[0]);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new MBeanException(e);
}
}
private void maybeSendMethodNotification(String type, String name) throws MBeanException {
MBeanNotificationInfo info = notificationInfoMap.findNotificationInfo(type, name);
if (info != null) {
long timeStamp = System.currentTimeMillis();
String notificationType = ModelMBeanUtil.matchType(info, "." + type + "." + name);
sendNotification(new Notification(notificationType, this, timeStamp, info.getDescription()));
}
}
private void maybeSendAttributeNotification(Attribute attribute) throws MBeanException {
String name = attribute.getName();
MBeanNotificationInfo info = notificationInfoMap.findNotificationInfo("set", name);
if (info != null) {
Object oldValue = getAttribute(name);
Object newValue = attribute.getValue();
long timeStamp = System.currentTimeMillis();
String notificationType = ModelMBeanUtil.matchType(info, ".set." + name);
sendNotification(new AttributeChangeNotification(
this, timeStamp, timeStamp, info.getDescription(),
info.getName(), notificationType, oldValue, newValue));
}
}
}
public class NotificationInfoMap {
private Map map = new HashMap<>();
public NotificationInfoMap(ModelMBeanInfo info) {
for (MBeanNotificationInfo n : info.getNotifications()) {
String[] types = n.getNotifTypes();
for (String type : types) {
map.put(type, n); // key: "example.service.set.propertyValue"
}
}
}
public MBeanNotificationInfo findNotificationInfo(String prefix, String name) {
return map.get(prefix + "." + name); // 如 "set.propertyValue"
}
}
public class MBeanDescriptorBasedAssembler implements MBeanInfoAssembler {
@Override
public ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) {
String className = managedBean.getClass().getName();
String resourcePath = className.replace('.', '/') + ".mbean.xml";
ClassPathResource resource = new ClassPathResource(resourcePath);
try (InputStream input = resource.getInputStream()) {
MBeanDescriptor descriptor = MBeanDescriptorUtil.read(input);
return descriptor.createMBeanInfo();
} catch (IOException e) {
throw new IllegalStateException("無法載入 MBean 描述檔: " + resourcePath, e);
}
}
}
public class MBeanDescriptorEnabledExporter extends MBeanExporter {
private MBeanInfoAssembler mBeanInfoAssembler;
public MBeanDescriptorEnabledExporter() {
setAssembler(new MBeanDescriptorBasedAssembler());
}
@Override
public ModelMBean createModelMBean() throws MBeanException {
// 僅當使用自訂裝配器時才建立擴展 ModelMBean
if (mBeanInfoAssembler instanceof MBeanDescriptorBasedAssembler) {
return new ModelMBeanExtension();
}
return super.createModelMBean();
}
@Override
public void setAssembler(MBeanInfoAssembler mBeanInfoAssembler) {
this.mBeanInfoAssembler = mBeanInfoAssembler;
super.setAssembler(mBeanInfoAssembler);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="exporter"
class="com.claudeduguay.mbeans.spring.MBeanDescriptorEnabledExporter">
<property name="beans">
<map>
<entry key="Services:name=ExampleService" value-ref="ExampleService" />
<entry key="MX4J:name=HttpAdaptor" value-ref="HttpAdaptor" />
<entry key="MX4J:name=XSLTProcessor" value-ref="XSLTProcessor" />
</map>
</property>
</bean>
<bean id="XSLTProcessor" class="mx4j.tools.adaptor.http.XSLTProcessor" />
<bean id="HttpAdaptor" class="mx4j.tools.adaptor.http.HttpAdaptor">
<property name="processor" ref="XSLTProcessor"/>
<property name="port" value="8080"/>
</bean>
<bean id="ExampleService"
class="com.claudeduguay.jmx.demo.server.ExampleService" />
</beans>
public class SpringJmxServer {
public static void main(String[] args) throws Exception {
// JVM 參數: -Dcom.sun.management.jmxremote.port=8999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false
ApplicationContext context = new ClassPathXmlApplicationContext(
"com/claudeduguay/jmx/demo/server/SpringJmxServer.xml");
HttpAdaptor httpAdaptor = (HttpAdaptor) context.getBean("HttpAdaptor");
httpAdaptor.start();
}
}
public class ExampleService {
private String propertyValue = "default value";
public String getPropertyValue() {
System.out.println("ExampleService: Get Property Value");
return propertyValue;
}
public void setPropertyValue(String propertyValue) {
System.out.println("ExampleService: Set Property Value");
this.propertyValue = propertyValue;
}
public void startService() {
System.out.println("ExampleService: Start Service Called");
}
public void stopService() {
System.out.println("ExampleService: Stop Service Called");
}
}
public class SpringJmxClient implements NotificationListener {
@Override
public void handleNotification(Notification notification, Object handback) {
System.out.println("Notification: " + notification.getMessage());
}
public static void main(String[] args) throws Exception {
SpringJmxClient listener = new SpringJmxClient();
String url = "service:jmx:rmi:///jndi/rmi://localhost:8999/jmxrmi";
JMXServiceURL serviceURL = new JMXServiceURL(url);
JMXConnector connector = JMXConnectorFactory.connect(serviceURL, null);
MBeanServerConnection mbsc = connector.getMBeanServerConnection();
ObjectName serviceName = ObjectName.getInstance("Services:name=ExampleService");
mbsc.addNotificationListener(serviceName, listener, null, null);
// 觸發各種操作
mbsc.invoke(serviceName, "startService", null, null); // before/after 通知
mbsc.setAttribute(serviceName, new Attribute("propertyValue", "new value")); // set 通知
System.out.println("Property: " + mbsc.getAttribute(serviceName, "propertyValue"));
mbsc.invoke(serviceName, "stopService", null, null); // before/after 通知
}
}
Notification: Example Service is Starting (before.startService)
Notification: Example Service is Started (after.startService)
Notification: PropertyValue was set (set.propertyValue, old=default value, new=new value)
Notification: Example Service is Stopping (before.stopService)
Notification: Example Service is Stopped (after.stopService)
瀏覽器開啟 https://localhost:8080 (MX4J HttpAdaptor),可視覺化操作屬性與方法,客戶端同樣接收通知。
| 特性 | 預設 Spring JMX | 本擴展方案 |
|---|---|---|
| 元資料定義 | Java 註解 / 介面 / 內省 | 外部 XML 檔案 (類路徑) |
| 通知事件 | 需手動實作 NotificationBroadcaster | XML 宣告自動發送 (set/before/after) |
| 訊息客製化 | 困難 | XML 中自訂 description |
| 程式碼侵入性 | 低 (需註解或介面) | 零 (POJO 完全不知情) |
| 版本管理 | 隨代碼編譯 | XML 可獨立部署更新 |