Spring JMX之一:使用JMX管理Spring Bean
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进行管 理。