spring中關於jmx包括幾個概念:
MBeanExporter: 從字面上很容易理解, 用來將一些spring的bean作為MBean暴露給MBEanServer。
MBeanServerFactoryBean: 也可以在spring中作為一個spring bean注入, 它用來將外部或者當前機器上的MBeanServer包裝成一個bean。
MBeanInfoAssembler : 用來控制作為MBean的spring bean的哪些屬性或方法將暴露出去, 以及決定何種形式的bean會被暴露成MBean. 不同的實現有不同的暴露方式。
ObjectNamingStrategy : 用來控制作為MBean暴露出去的spring bean在MBeanServer中將如何命名(ObjectName), 描述, 指定初始值等, ObjectName通常采用"域:鍵=值,鍵=值,...".
ConnectorServerFactoryBean : 用來給外界訪問當前spring中的MBeanServer bean提供一個連接器, 也就是給MBeanServer開一個外加訪問的口子, 比如"service:jmx:jmxmp://localhost:9875" 讓外界通過jmxmp協議, 通過9875端口來訪問MBeanServer, 外界要訪問MBeanServer, 必須提供一個connector. 默認連接器是jmxmp協議service:jmx:jmxmp://localhost:9875 , 也可以通過其他協議的連接器, 比如協議RMI,IIOP, Burlap,Hessian,SOAP
MBeanServerConnectionFactoryBean: 用來創建一個訪問MBeanServer的客戶端連接器, 比如MBeanServer bean暴露了一個服務器端連接器, 那么客戶端就可以通過這個連接器來訪問MBeanServer中的MBean. 可以理解為ConnectorServerFactoryBean的對應物, server與client之間就是這兩種連接器建立通訊連接
MBeanProxyFactoryBean: 用來創建客戶端訪問遠程MBeanServer中的MBean的代理, 客戶端要訪問服務器端的bean, 除了客戶端連接器之外, 還需要一個代理, 相當於一個服務器端的stub.
通過給exporter指定MBeanServer之后, exporter中暴露的MBean就會注入到MBeanServer中.
spring中要找到一個MBeanServer並注入到exporter中有多種方式(如果不指定MBeanServer, spring會自己找), MBeanServerFactoryBean是一種方式, Locator也是一種.
exporter暴露出去的MBean不會立即就放到MBeanServer中, 只有調用方發起了對MBeanServer中MBean的調用的時候, 才會初始化。
本章內容:
- 將Spring bean暴露為MBean
- 遠程管理Spring Bean
- 處理JMX通知
Spring對DI的支持是通過在應用中配置bean屬性,這是一種非常不錯的方法。不過,一旦應用 已經部署並且正在運行,單獨使用DI並不能幫助我們改變應用的配置。假設我們希望深入了 解正在運行的應用並要在運行時改變應用的配置,此時,就可以使用Java管理擴展(Java Manage- ment Extensions,JMX)了。
JMX這項技術能夠讓我們管理、監視和配置應用。這項技術最初作為Java的獨立擴展,從Java 5開始,JMX已經成為標准的組件。
使用JMX管理應用的核心組件是托管bean(managed bean,MBean)。所謂的MBean就是暴露特 定方法的JavaBean,這些方法定義了管理接口。JMX規范定義了如下4種類型的MBean:
- 標准MBean:標准MBean的管理接口是通過在固定的接口上執行反射確定的,bean類會實 現這個接口;
- 動態MBean:動態MBean的管理接口是在運行時通過調用DynamicMBean接口的方法來 確定的。因為管理接口不是通過靜態接口定義的,因此可以在運行時改變;
- 開放MBean:開放MBean是一種特殊的動態MBean,其屬性和方法只限定於原始類型、原 始類型的包裝類以及可以分解為原始類型或原始類型包裝類的任意類型;
- 模型MBean:模型MBean也是一種特殊的動態MBean,用於充當管理接口與受管資源的中 介。模型Bean並不像它們所聲明的那樣來編寫。它們通常通過工廠生成,工廠會使用元信 息來組裝管理接口。
Spring的JMX模塊可以讓我們將Spring bean導出為模型MBean,這樣我們就可以查看應用程序 的內部情況並且能夠更改配置——甚至在應用的運行期。接下來,將會介紹如何使用Spring對 JMX的支持來管理Spring應用上下文中的bean。
1、將Spring bean導出為MBean
這里有幾種方式可以讓我們通過使用JMX來管理Spittr應用中的bean。為了讓事情盡量保持簡 單,我們對程序清單5.10中SpittleController只做適度的改變,增加一個新的 spittlesPerPage屬性:
package com.dxz.mvcdemo1.web; import static org.springframework.web.bind.annotation.RequestMethod.GET; import java.util.HashMap; import java.util.Map; import org.springframework.context.annotation.Bean; import org.springframework.jmx.export.MBeanExporter; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/biz") public class SpittleCntroller { // 默認每個頁面的大小 public static final int DEFAULT_SPITTLES_PER_PAGE = 25; // 每頁的大小 private int spittlesPerPage = DEFAULT_SPITTLES_PER_PAGE; public int getSpittlesPerPage() { return spittlesPerPage; } public void setSpittlesPerPage(int spittlesPerPage) { this.spittlesPerPage = spittlesPerPage; } @RequestMapping(value = "/test", method = GET) public String test() { String result = spittlesPerPage + " - test()"; System.out.println(result); return "home"; } @Bean public MBeanExporter mbeanExporter(SpittleCntroller spittleController) { MBeanExporter exporter = new MBeanExporter(); Map<String, Object> beans = new HashMap<String, Object>(); beans.put("spitter:name=SpittleController", spittleController); exporter.setBeans(beans); return exporter; } }
Spring的MBeanExporter是將Spring Bean轉變為MBean的關鍵。MBeanExporter可以把一個或多個Spring bean導出為MBean服務器(MBean server)內的模型 MBean。MBean服務器(有 時候也被稱為MBean代理)是MBean生存的容器。對MBean的訪問,也是通過MBean服務器來 實現的。
啟動測試:
打開java監視和管理控制台(jconsole.exe)如下圖:
在控制台修改參數的值,如下圖:
測試修改后的效果,通過瀏覽器訪問:http://localhost:8080/biz/test
日志打印結果如下:
為了對MBean的屬性和操作獲得更細粒度的控制,Spring提供了幾種選擇,包括:
- 通過名稱來聲明需要暴露或忽略的bean方法;
- 通過為bean增加接口來選擇要暴露的方法;
- 通過注解標注bean來標識托管的屬性和操作。
我們會嘗試每一種方式來決定哪一種最適合SpittleControllerMBean。
MBean服務器從何處而來
根據以上配置,MBeanExporter會假設它正在一個應用服務器中(例如Tomcat)或提供MBean 服務器的其他上下文中運行。但是,如果Spring應用程序是獨立的應用或運行的容器沒有提供 MBean服務器,我們就需要在Spring上下文中配置一個MBean服務器。
在XML配置中,元素可以為我們實現該功能。如果使用Java配置的話,我們需要更直接的方式,也就是配置類型為MBeanServerFactoryBean的bean(這 也是在XML中元素所作的事情)。
MBeanServerFactoryBean會創建一個MBean服務器,並將其作為Spring應用上下文中的 bean。默認情況下,這個bean的ID是mbeanServer。了解到這一點,我們就可以將它裝配 到MBeanExporter的server屬性中用來指定MBean要暴露到哪個 MBean服務器中。
1、通過名稱暴露方法
MBean信息裝配器(MBean info assembler)是限制哪些方法和屬性將在MBean上暴露的關鍵。 其中有一個MBean信息裝配器是MethodNameBasedMBeanInfoAssembler。這個裝配器 指定了需要暴露為MBean操作的方法名稱列表。對於SpittleController bean來說,我們 希望把spittlePerPage暴露為托管屬性。基於方法名的裝配器如何幫我們導出一個托管屬 性呢?
我們回顧下JavaBean的規則(這不是Spring Bean所必需的),spittlesPerPage屬性需要定 義對應的存取器(accessor)方法,方法名必須為setSpittlesPerPage()和 getSpittlesPerPage()。為了限制MBean所暴露的內容,我們需要告訴 MethodNameBaseMBeanInfoAssembler僅在MBean的接口中包含這兩個方法。如 下MethodNameBaseMBeanInfoAssembler的bean聲明就配置了這些方法:
在SpittleCntroller.java中增加如下方法:
@Bean public MethodNameBasedMBeanInfoAssembler assembler() { MethodNameBasedMBeanInfoAssembler assembler = new MethodNameBasedMBeanInfoAssembler(); assembler.setManagedMethods(new String[] { "getSpittlesPerPage", "setSpittlesPerPage" }); return assembler; } @Bean public MBeanExporter mbeanExporter(SpittleCntroller spittleController, MBeanInfoAssembler assembler) { MBeanExporter exporter = new MBeanExporter(); Map<String, Object> beans = new HashMap<String, Object>(); beans.put("spitter:name=SpittleController", spittleController); exporter.setBeans(beans); exporter.setAssembler(assembler); return exporter; }
managedMethods屬性可以接受一個方法名稱的列表,指定了哪些方法將暴露為MBean的操 作。因為本示例所配置的是spittlesPerPage屬性的存取器方法,所以 spittlesPerPage屬性也自然成為了MBean的托管屬性。為了讓這個裝配器能夠生效,我們需要將它裝配進MBeanExporter中。
再測試一把如下:
2、使用接口定義MBean的操作和屬性
Spring的InterfaceBasedMBeanInfoAssembler是另一種MBean信息裝配器,可以讓我 們通過使用接口來選擇bean的哪些方法需要暴露為MBean的托管操 作。InterfaceBasedMBeanInfoAssembler與基於方法名稱的裝配器很相似,只不過不 再通過羅列方法名稱來確定暴露哪些方法,而是通過列出接口來聲明哪些方法需要暴露。 例如,假設我們定義了一個名為SpittleControllerManagedOperations的接口,如下 所示:
package com.dxz.mvcdemo1.web.jmx; public interface SpittleControllerManagedOperations { public int getSpittlesPerPage(); public void setSpittlesPerPage(int spittlesPerPage); }
上面的接口中定義的是要暴露的get/set方法,我們只需要使用如下的assemblerbean替換之前基 於方法名稱的裝配器即可:
package com.dxz.mvcdemo1.web.jmx; import static org.springframework.web.bind.annotation.RequestMethod.GET; import java.util.HashMap; import java.util.Map; import org.springframework.context.annotation.Bean; import org.springframework.jmx.export.MBeanExporter; import org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler; import org.springframework.jmx.export.assembler.MBeanInfoAssembler; import org.springframework.jmx.export.assembler.MethodNameBasedMBeanInfoAssembler; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/biz2") public class SpittleCntroller2 { // 默認每個頁面的大小 public static final int DEFAULT_SPITTLES_PER_PAGE = 25; // 每頁的大小 private int spittlesPerPage = DEFAULT_SPITTLES_PER_PAGE; public int getSpittlesPerPage() { return spittlesPerPage; } public void setSpittlesPerPage(int spittlesPerPage) { this.spittlesPerPage = spittlesPerPage; } @RequestMapping(value = "/test2", method = GET) public String test() { String result = spittlesPerPage + " - test()"; System.out.println(result); return "home"; } @Bean public InterfaceBasedMBeanInfoAssembler assembler() { InterfaceBasedMBeanInfoAssembler assembler = new InterfaceBasedMBeanInfoAssembler(); assembler.setManagedInterfaces(new Class<?>[] {SpittleControllerManagedOperations.class}); return assembler; } @Bean public MBeanExporter mbeanExporter(SpittleCntroller2 spittleController, MBeanInfoAssembler assembler) { MBeanExporter exporter = new MBeanExporter(); Map<String, Object> beans = new HashMap<String, Object>(); beans.put("spitter:name=SpittleController2", spittleController); exporter.setBeans(beans); exporter.setAssembler(assembler); return exporter; } }
結果:
最終,這些托管操作必須在某處聲明,無論是在Spring配置中還是在某個接口中。此外,從代 碼角度看,托管操作的聲明是一種重復——在接口中或Spring上下文中聲明的方法名稱與實 現中所聲明的方法名稱存在重復。之所以存在這種重復,沒有其他原因,僅僅是為了滿足 MBeanExporter的需要而產生的。
3、使用注解驅動的MBean
除了我向你展示的MBean信息裝配器,Spring還提供了另一種裝配器——MetadataMBeanInfoAssembler,這種裝配器可以使用注解標識哪些bean的方法需要暴露為MBean的 托管操作和屬性。我完全可以向你展示如何使用這種裝配器,但我不會這么做。這是因為手工 裝配它非常繁雜,僅僅是為了使用注解並不值得這么做。相反,我將向你展示如何使用Spring context配置命名空間中的元素。這個便捷的元素裝配了 MBean導出器以及為了在Spring啟用注解驅動的MBean所需要的裝配器。我們所需要做的就是 使用它來替換我們之前所使用的 MBeanExporterbean:
現在,要把任意一個Spring bean轉變為MBean,我們所需要做的僅僅是使 用@ManagedResource注解標注bean並使用@ManagedOperation 或@ManagedAttribute注解標注bean的方法。例如,如下的程序清單展示了如何使用注解 把SpittleController導出為MBean。
package com.dxz.mvcdemo1.web.jmx3; import static org.springframework.web.bind.annotation.RequestMethod.GET; import java.util.HashMap; import java.util.Map; import org.springframework.context.annotation.Bean; import org.springframework.jmx.export.MBeanExporter; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler; import org.springframework.jmx.export.assembler.MBeanInfoAssembler; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.dxz.mvcdemo1.web.jmx.SpittleCntroller2; import com.dxz.mvcdemo1.web.jmx.SpittleControllerManagedOperations; @Controller @RequestMapping("/biz3") @ManagedResource(objectName="spitter:name=SpittleController3") //將SpittleController導出為MBean public class SpittleCntroller3 { // 默認每個頁面的大小 public static final int DEFAULT_SPITTLES_PER_PAGE = 25; // 每頁的大小 private int spittlesPerPage = DEFAULT_SPITTLES_PER_PAGE; //@ManagedOperation@ManagedOperation注解替換@ManagedAttribute注解來標注存取器方法 @ManagedAttribute //將spittlesPerPage暴露為托管屬性 public int getSpittlesPerPage() { return spittlesPerPage; } //@ManagedOperation@ManagedOperation注解替換@ManagedAttribute注解來標注存取器方法 @ManagedAttribute //將spittlesPerPage暴露為托管屬性 public void setSpittlesPerPage(int spittlesPerPage) { this.spittlesPerPage = spittlesPerPage; } @RequestMapping(value = "/test3", method = GET) public String test() { String result = spittlesPerPage + " - test()"; System.out.println(result); return "home"; } }
在類級別使用了@ManagedResource注解來標識這個bean應該被導出為 MBean。objectName屬性標識了域(Spitter)和MBean的名稱(SpittleController)。 spittlesPerPage屬性的存取器方法都使用了@ManagedAttribute注解來進行標注,這 表示該屬性應該暴露為MBean的托管屬性。注意,其實並不需要使用注解同時標注這兩個存取 器方法。如果我們選擇僅標注setSpittlesPerPage()方法,那我們仍可以通過JMX設置 該屬性,但這樣的話我們將不能查看該屬性的值。相反,如果僅僅標注 getSpittlesPerPage()方法,那我們可以通過JMX查看該屬性的值,但無法修改該屬性 的值。
同樣需要提醒一下,我們還可以使用@ManagedOperation注解替換@ManagedAttribute 注解來標注存取器方法。這會將方法暴露為MBean的托管操作,但是並不會把spittlesPerPage屬性暴露為MBean 的托管屬性。這是因為在暴露MBean功能時,使用@ManagedOperation注解標注方法是嚴 格限制方法的,並不會把它作為JavaBean的存取器方法。因此,使用@ManagedOperation可 以用來把bean的方法暴露為MBean托管操作,而使用@ManagedAttribute可以把bean的屬 性暴露為MBean托管屬性。
測試如下:
處理MBean沖突
到目前為止,我們已經看到可以使用多種方式在MBean服務器中注冊MBean。在所有的示例 中,我們為MBean指定的對象名稱是由管理域名和key-value對組成的。如果MBean服務器中不 存在與我們MBean名字相同的已注冊的MBean,那我們的MBean注冊時就不會有任何問題。但 是如果名字沖突時,將會發生什么呢?
默認情況下,MBeanExporter將拋出InstanceAlreadyExistsException異常,該異常 表明MBean服務器中已經存在相同名字的MBean。不過,我們可以通過MBeanExporter的 registrationBehaviorName屬性或者的registration 屬性指定沖突處理機制來改變默認行為。 Spring提供了3種借助registrationBehaviorName屬性來處理MBean名字沖突的機制:
FAIL_ON_EXISTING:如果已存在相同名字的MBean,則失敗(默認行為);
IGNORE_EXISTING:忽略沖突,同時也不注冊新的MBean;
REPLACING_EXISTING:用新的MBean覆蓋已存在的MBean;
例如,如果我們使用MBeanExporter,我們可以通過設置registration-BehaviorName 屬性為RegistrationPolicy.IGNORE_EXISTING來忽略沖突,如下所示:
@Bean public MBeanExporter mbeanExporter(SpittleCntroller spittleController, MBeanInfoAssembler assembler) { MBeanExporter exporter = new MBeanExporter(); Map<String, Object> beans = new HashMap<String, Object>(); beans.put("spitter:name=SpittleController", spittleController); exporter.setBeans(beans); exporter.setAssembler(assembler); exporter.setRegistrationPolicy(RegistrationPolicy.IGNORE_EXISTING); return exporter; }
registrationBehaviorName屬性可以接受RegistrationPolicy中所定義的枚舉值, 每一個取值分別對應3種沖突處理機制的一種。 現在我們已使用MBeanExporter注冊了我們的MBean,我們還需要一種方式來訪問它們並進行 管理。正如之前所看到的,我們可以使用諸如JConsole之類的工具來訪問本地的MBean服務 器,進而顯示和操縱MBean,但是像JConsole之類的工具並不適合在程序中對MBean進行管 理。