这篇文章主要对 Instrumentation Level 提供的组件有个基本认识~本文绝大多数理论支撑来自于《JMX Specification 1.4》规范


MBean

MBean是提供资源的对象称呼,它一共有四种类型:

  1. Standard MBean
  2. Dynamic MBean
  3. Open MBean
  4. Model MBean

这里会主要讲一下 Standard MBeanDynamic MBean 以及 MXBean 。因为这几个比较常用一些,其他的影后如果有用到再加吧😋~

Standard MBean

Standard MBean 通过定义接口将可管理的资源暴露出去。 Standard MBean 的创建规则必须要遵守以下两个:

  1. 创建接口 SomethingMBean
  2. 实现步骤一的接口,类名必须是 Something

其次接口里的各个方法定义遵循JavaBeans component model 的命名规则:

  • 可以读的属性,需要定义带前缀 get 的方法,且必须要有方法返回值。比如 String getUsername()
  • 可以写的属性,需要定义带前缀 set 的方法,且有且仅有一个方法参数。比如 void setUsername(String)
  • 除上面以外的方法,都视为可执行的操作方法

除此之外,还有其他的一些资源数据暴露规则:

  • 公有的构造方法会暴露出去
  • 广播出去的 Notification 对象会暴露出去

    The management interface of a standard MBean is composed of: ■ Its constructors: only the public constructors of the MBean class are exposed ■ Its attributes: the properties that are exposed through getter and setter methods ■ Its operations: the remaining methods exposed in the MBean interface ■ Its notifications: the notification objects and types that the MBean broadcasts ——FROM《JMX Specification 1.4》

既然不同的资源有不同的接口定义,那 MBean Server 是怎么加载/暴露各种各样的资源数据的?

The process of inspecting the MBean interface and applying these design patterns is called introspection. The JMX agent uses introspection to look at the methods and superclasses of a class, determine if it represents an MBean that follows the design patterns, and recognize the names of both attributes and operations. ——FROM《JMX Specification 1.4》

规范手册里提到使用一个 introspection 的方法来处理 MBean ,从而获取各种各样的关于 MBean 的信息。笔者在 com.sun.jmx.interceptor.DefaultMBeanServerInterceptor 里找到了解析痕迹:

  1. private ObjectInstance registerObject(String classname,
  2. Object object, ObjectName name)
  3. throws InstanceAlreadyExistsException,
  4. MBeanRegistrationException,
  5. NotCompliantMBeanException {
  6. if (object == null) {
  7. final RuntimeException wrapped =
  8. new IllegalArgumentException("Cannot add null object");
  9. throw new RuntimeOperationsException(wrapped,
  10. "Exception occurred trying to register the MBean");
  11. }
  12. // 在registerObject()的时候会解析MBean
  13. DynamicMBean mbean = Introspector.makeDynamicMBean(object);
  14. return registerDynamicMBean(classname, mbean, name);
  15. }

追踪下去后可以在 com.sun.jmx.mbeanserver.MBeanSupport 里发现 MBeanInfo 的创建过程:

  1. <T> MBeanSupport(T resource, Class<T> mbeanInterfaceType)
  2. throws NotCompliantMBeanException {
  3. if (mbeanInterfaceType == null)
  4. throw new NotCompliantMBeanException("Null MBean interface");
  5. if (!mbeanInterfaceType.isInstance(resource)) {
  6. final String msg =
  7. "Resource class " + resource.getClass().getName() +
  8. " is not an instance of " + mbeanInterfaceType.getName();
  9. throw new NotCompliantMBeanException(msg);
  10. }
  11. ReflectUtil.checkPackageAccess(mbeanInterfaceType);
  12. this.resource = resource;
  13. MBeanIntrospector<M> introspector = getMBeanIntrospector();
  14. this.perInterface = introspector.getPerInterface(mbeanInterfaceType);
  15. // 构建MBeanInfo,这个会暴露给Manager看
  16. this.mbeanInfo = introspector.getMBeanInfo(resource, perInterface);
  17. }

Standard MBean 的好处就是方便易用;但是如果要暴露复合类型的话,在 Manager Client 端上会无法获取复合类型对象的详细信息(因为客户端没有这个复合类型的字节文件)。

MemoryUsage is a model-specific class. With Standard MBeans, a client of the MBean Server cannot access the Usage attribute if it does not know the class MemoryUsage. Suppose the client is a generic console based on JMX technology. Then the console would have to be configured with the model-specific classes of every application it might connect to. The problem is even worse for clients that are not written in the Java language. Then there may not be any way to tell the client what a MemoryUsage looks like. ——MXBean.java

Standard MBean 的使用可以看这里:

  1. 👉《Github Standard MBean 学习样例》
  2. 👉《语雀 Standard MBeans学习示例》

MXBean

这货可以说是进阶版的 MBean ,它相比 Standard MBean 使用 CompositeData 数据结构来表示复合类型数据,这种数据结构可以在各种客户端上实现和表示。所以有了它就能解决 Standard MBean 无法暴露复合类型的资源问题。

<T> MBeanSupport(T resource, Class<T> mbeanInterfaceType)
        throws NotCompliantMBeanException {
    if (mbeanInterfaceType == null)
        throw new NotCompliantMBeanException("Null MBean interface");
    if (!mbeanInterfaceType.isInstance(resource)) {
        final String msg =
            "Resource class " + resource.getClass().getName() +
            " is not an instance of " + mbeanInterfaceType.getName();
        throw new NotCompliantMBeanException(msg);
    }
    ReflectUtil.checkPackageAccess(mbeanInterfaceType);
    this.resource = resource;
    // 获取到com.sun.jmx.mbeanserver.MXBeanSupport
    // 追踪下去里面会有提供获取OpenType的方法,这里就不放了
    MBeanIntrospector<M> introspector = getMBeanIntrospector();
    this.perInterface = introspector.getPerInterface(mbeanInterfaceType);
    this.mbeanInfo = introspector.getMBeanInfo(resource, perInterface);
}

要创建MXBean也需要两个步骤,但是条件没有 Standard MBean 这样苛刻了:

  1. 创建接口 SomethingMBean
  2. 实现步骤一的接口,类名可以自定义

MXBean 的使用可以看这里👉《MXBean 学习样例》

Dynamic MBean

前面在学习 Standard MBean 时,提出过 MBean Server 如何识别不同的资源对象呢?我们说到了introspection ,但是吧在 registerObject() 方法里面 Introspector.makeDynamicMBean() 会返回一个 Dynamic MBean 对象。前面无论是 Standard MBean 还是 MXBean ,它们最终都会转换为 Dynamic MBean 进行处理
如果要创建一个 Dynamic MBean 需要实现下面这个接口:

public interface DynamicMBean {
    /**
     * 根据传入的Attribute,返回该Attribute对应的值。
     * 相当于开发人员根据传入的attribute参数做一个映射,把资源里的attribute数据取出来返回
     */
    public Object getAttribute(String attribute) throws AttributeNotFoundException,
        MBeanException, ReflectionException;
    // 同上,只不过传入的是一串attribute数组,查出来一串数据列表回去
    public AttributeList getAttributes(String[] attributes);
    /**
     * 根据传入的Attribute,给内部的资源设置对应的属性值。
     * 相当于开发人员根据传入的attribute参数,给内部资源设置对应的值
     * 注意,传入的参数类型是Attribute,有key,有value;key是attribute,value是要设置的值
     */
    public void setAttribute(Attribute attribute) throws AttributeNotFoundException,
        InvalidAttributeValueException, MBeanException, ReflectionException ;
    // 同上
    public AttributeList setAttributes(AttributeList attributes);
    // 调用operation方法时
    public Object invoke(String actionName, Object params[], String signature[])
        throws MBeanException, ReflectionException ;
    // 获取描述一个MBean的基本信息
    public MBeanInfo getMBeanInfo();
 }

总的来说, Dynamic MBean 就是一个中间层,它将 Standard MBeanMXBean 抽象了出来, MBean Server 直接和 Dynamic MBean 打交道就行了,并不需要关系它究竟是 Standard MBean 还是 MXBean

所以如果我们想实现多个相近的类,我们就可以利用这个中间层做转换,最终只需要一个 Dynamic Bean 就能注册许多种 MBean
Dynamic MBean 的使用可以看这里👉《Dynamic MBean学习样例》

小结

上面给我们展示了各种各样的 MBean 的开发方式,但无论是哪种 MBean 都不会影响 MBean Server 的注册,仅仅只会影响开发人员的开发方式。所以 MBean 只需要知道几个常用的即可,能因地制宜就可以了。

Notification

JMX Notification 主要由五个组件构成:

  • Notification ,基本的消息载体,它可以表示任意的事件
  • NotificationListener ,需要接收消息的对象都要实现该接口
  • NotificationFilter ,通过实现该接口可以过滤 Notification
  • NotificationBroadcaster ,通过实现该接口, MBean 就能发送 Notification
  • NotificationEmitter ,通过实现该接口,可以监听 MBean 的移除 Listener 事件

JMX 里面,所有的消息都是广播出去的,如果没有配置 Filter ,那么任意实现了 ListenerMBean 都会收到消息;而配置了 Filter 就能过滤不需要的消息。这和安卓的广播过滤差不多的。

Notification

Notification 是消息的通用载体,任何的消息都能转到 Notification 对象里。接下来我们先康康创建一个 Notification 需要哪些参数:

public Notification(String type, Object source, 
                    long sequenceNumber, long timeStamp, String message)
  • type :定义消息的类型。规范上仅要求用一个 String 类型来定义不同的消息类型。只不过这个类型字符串需要遵守一定的规则(最好):
    • 官方的 JMX 架构内所有的消息都是以 jmx.mbean.eventType 来命名的;如果是商家自定义的,最好以 VendorName.resouceA.eventTypeJMX Specification 上给了一张图:

image.png

  • source ,产生这条消息的事件源,通常是给发送消息的 MBean 对象本身
  • sequenceNumber ,可以视为该条消息的ID
  • timeStamp ,创建这条消息的时间
  • message ,描述这条消息的内容
  • userData ,这个属性需要在创建出 Notification 后手动 setUserData()

一般情况下, Notification 够用了,适用于绝大多数场景;但是如果想增加一些语义(让开发更加简便, Notification 的用户数据全都保存在 Object userData 里面,就没面向对象内味;而如果把需要传递的数据通过方法直接取到,就很爽),可以通过继承 Notification 实现定义自己的数据和方法,可以参考 AttributeChangeNotification

NotificationBroadcaster & NotificationEmitter

NotificationBroadcaster 是用来接收监听器注册/移除的接口NotificationEmitter 继承了NotificationBroadcaster 接口,并在其之上增加了一个接收监听器移除的方法
NotificationBroadcaster 的接口代码如下所示:

public interface NotificationBroadcaster {

    /**
     * 添加Listener给实现 NotificationBroadcaster 的MBean时触发该方法
     * @param listener  要注册的NotificationListener。当MBean发送消息时调用listener发送
     * @param filter    当注册NotificationListener时,和Listener一起发来Filter。用来判断某条消息应不应该发送给一起来的Listener
     * @param handback  和listener对应的handback,用来保存一些携带的参数。当回调listener时一起发送给listener,帮助listener处理消息的。
     */
    public void addNotificationListener(NotificationListener listener,
                                        NotificationFilter filter,
                                        Object handback)
            throws java.lang.IllegalArgumentException;

    /**
     * 当移除该MBean的一个Listener时会触发该方法
     * @param listener 要删除的NotificationListener
     */
    public void removeNotificationListener(NotificationListener listener)
            throws ListenerNotFoundException;

    /**
     * 用来定义这个MBean会发送哪些Notification和Notification Type。
     * 规范里推荐我们把要发送的Notification和Notification Type定义在这里
     * 但是实际上也可以不这么做,只不过有的比较严谨的客户端会使用这个方法
     * 保证消息完整性
     */
    public MBeanNotificationInfo[] getNotificationInfo();
}

NotificationEmitter 的接口代码如下所示:

/**
 * 是上面的removeNotificationListener()的增强版,这里面还带有filter和handback保证listener的唯一性
 */
public void removeNotificationListener(NotificationListener listener,
                                           NotificationFilter filter,
                                           Object handback)

总而言之,这里定义了一套关于 监听器注册/删除时回调 的方法。

NotificationListener

这个接口就是监听器接口啦,实现了监听器并注册到对应的 MBean 之后, MBean 发送消息给监听器,监听器就能收到了。

public interface NotificationListener extends java.util.EventListener   {
    /**
    * 当产生消息通知时就该调用该方法。该方法应该尽快返回,避免阻塞
    * @param notification 消息通知
    * @param handback 当注册时(addNotificationListener)会要求传入该对象,该对象会在MBean发送者发送消息的时候传给监听器
    */
    public void handleNotification(Notification notification, Object handback);
}

NotificationFilter

该接口是用来判断是否应该发送消息给对应的监听器:

public interface NotificationFilter extends java.io.Serializable {
    /**
     * Invoked before sending the specified notification to the listener.
     * 在发送消息通知给特定的监听器之前判断一遍,
     * @param notification 待发送的消息通知
     * @return true表示该消息可以发送;false表示不可以发送
     */
    public boolean isNotificationEnabled(Notification notification);
}

NotificationFilter 通常会在注册监听器的时候一起和监听器注册,消息发送者应该保存监听器、监听器过滤器,然后在消息发送前通过监听过滤器判断待发送的消息是否应该发送

Notification 的使用可以看这里👉《Notification 学习样例》

小结

该部分说了一些和 Notification 相关的接口, Notification 各组件关系图如下所示:
点击查看【processon】

Metadata

MBean 注册到 MBeanServer 时,会自动创建 MBean 的元数据信息类 MBeanInfoMBeanInfo 里面保存了 MBean 的属性、操作方法、支持的消息通知类型等等。

Metadata 的使用可以看这里👉《Metadata 学习样例》

总结

Instrumentation Level 这层基本都是为上层搭建一些基础设施,全文下来,能对这一层提供的大部分内容有一个基本的认识了。