The JMX forum on SDN is a lively place. Recently, Athahar was trying to expose a POJO resource through JMX - and quite naturally, decided to try out doing this with a ModelMBean. However, ModelMBeans are not the only way to this - and here are a few thoughts, which borrow heavily on some of Eamonn's most recent blog entries.
This is the traditional way of exposing resources using the JMX API: have the Java Object you want to expose implement an interface which groups all the attributes (getters and setters) and operations (methods) you want to expose for management.
If you are using JDK 6, then tag this interface as MXBean.
If you are using earlier versions of the JDK, then name this interface by appending "MBean" to your object's java class name - or wrap your object in a StandardMBean.
Then at runtime, register your object(s) in the (platform) MBeanServer.
The platform MBeanServer is the MBeanServer that is used by the JVM default Management Agent, so if you register your MBeans in there, you will be able to see them directly when connecting JConsole to the default JVM agent.
This is the easiest way to proceed, and often the best way. Having an interface makes it possible to create MBean proxies, and to code remote clients against that interface. Using MXBean ensure that remote clients which do not have access to your interface/classes will still be able to interact with your MBeans. The MBean (or MXBean) interface makes it also possible to separate those method that are suited for remote invocation from those that should only be called locally, from your application.
However, there can be some cases where such a method is not possible. For instance, if the interface of your MBean is not known at compile time (rare but possible), or if the object you want to manage is a third party object, obtained e.g. from a legacy application whose source code cannot be modified. In those cases, you may want to turn to the other methods listed below.
Model MBeans were designed to dynamically map some java object resource to a JMX Model specified at runtime. In theory, Model MBeans could be a perfect match for what we are attempting to do here. In practice, you might find that Model MBeans with their very powerful list of configurable features are a bit of an over kill. But should you wish to create and configure a Model MBean to expose your Java resource, here is an example of code that creates a trivial Model MBean to exposes all getter, setters, and operations of a simple POJO. The method makeModelMBean(Object resource) shown below introspect the given resource in order to configure aRequiredModelMBean that will expose that resource. This code example outline a few details which may not be intuitive:
- All getters and setters must also be declared as operations.
- When creating a ModelMBeanAttributeInfo object you need to explicitely specify, for that attribute, the names of the getter and setter methods in the Descriptor, even when you pass those methods to the attribute info constructor.
public static ModelMBean makeModelMBean(Object resource)
throws JMException, InvalidTargetObjectTypeException {
final Method[] methods = resource.getClass().getMethods();
final List<Method> operations = new ArrayList<Method>();
final List<Method> getters = new ArrayList<Method>();
final Map<String,Method> setters = new LinkedHashMap<String,Method>();
for (Method method : methods) {
if (method.getDeclaringClass().equals(Object.class)) continue;
if (method.getName().startsWith("get") &&
!method.getName().equals("get") &&
!method.getName().equals("getClass") &&
method.getParameterTypes().length == 0 &&
method.getReturnType() != void.class) {
getters.add(method);
}
if (method.getName().startsWith("set") &&
!method.getName().equals("set") &&
method.getParameterTypes().length == 1 &&
method.getReturnType().equals(void.class)) {
setters.put(method.getName(),method);
}
operations.add(method);
}
final List<ModelMBeanAttributeInfo> attrinfo =
new ArrayList<ModelMBeanAttributeInfo>();
for (Method getter:getters) {
final String attrName = getter.getName().substring(3);
final String setterMethod = "set"+attrName ;
Method setter = setters.remove(setterMethod);
if (setter != null) {
if (!getter.getReturnType().equals(
setter.getParameterTypes()[0])) {
System.err.println("Warning: setter "+setter.getName()+
" doesn't have the expected type: setter ignored.");
setter = null;
}
}
attrinfo.add( makeAttribute(getter,setter));
}
for (Method setter:setters.values()) {
System.err.println("Warning: setter "+setter.getName()+
" has no corresponding getter!");
attrinfo.add( makeAttribute(null,setter));
}
final ModelMBeanAttributeInfo[] attrs =
attrinfo.toArray(new ModelMBeanAttributeInfo[attrinfo.size()]);
final int opcount = operations.size();
final ModelMBeanOperationInfo[] ops =
new ModelMBeanOperationInfo[opcount];
for (int i=0;i<opcount;i++){
final Method m = operations.get(i);
ops[i] = new ModelMBeanOperationInfo(m.getName(),m);
}
ModelMBeanInfo mmbi =
new ModelMBeanInfoSupport(resource.getClass().getName(),
resource.getClass().getName(),
attrs,
null, ops,
null); ModelMBean mmb = new RequiredModelMBean(mmbi);
mmb.setManagedResource(resource, "ObjectReference");
return mmb;
}
private static ModelMBeanAttributeInfo makeAttribute(Method getter,
Method setter)
throws IntrospectionException {
final String attrName;
if (getter != null)
attrName = getter.getName().substring(3);
else
attrName = setter.getName().substring(3);
final List<String> descriptors = new ArrayList<String>();
descriptors.add("name=" + attrName);
descriptors.add("descriptorType=attribute");
if (getter!=null) {
descriptors.add("getMethod=" + getter.getName());
}
if (setter!=null) {
descriptors.add("setMethod=" + setter.getName());
}
final Descriptor attrD = new DescriptorSupport(
descriptors.toArray(new String[descriptors.size()]));
return new ModelMBeanAttributeInfo(attrName, attrName, getter, setter,
attrD);
}
|
To test my Model MBean, I have created a dummy resource POJO, wrapped it in a Model MBean, and registered it in the platform MBeanServer.
public static class MyPojo {
private String foo = "foo!";
private long weird = -1;
private Object bad = new Long(0);
public String getFoo() {return foo;}
public void setFoo(String foo) {this.foo=foo;}
public int getBar() {return 1;}
public void setWeird(long weird) {this.weird=weird;}
public Object getBad() {return bad;}
public void setBad(Number bad) {this.bad = bad;}
public long doIt() {
System.out.println("doIt!");
return weird+1;
}
}
public static void main(String[] args) throws Exception {
final MyPojo obj = new MyPojo();
final ModelMBean mbean = makeModelMBean(obj);
ManagementFactory.getPlatformMBeanServer().
registerMBean(mbean,new ObjectName("test:type=MyPojo"));
System.out.println("Connect now with JConsole.");
System.out.println("Strike <Return> to exit.");
System.in.read();
}
|
Here is how it appears in JConsole:
A lighter solution might be to write a DynamicMBean that will introspect the class of your resource object. Here is the code of aDynamicPOJOMBean that does exactly that. If your resource can emit some notifications, then you could modify this DynamicPOJOMBean
to extend NotificationBroadcasterSupport - and arrange to call sendNotification() when a Notification needs to be sent, or code aDynamicPOJOEmitterMBean
that extends DynamicPOJOMBean
and implements NotificationEmitter, and delegates notification handling to a wrappedNotificationBroadcasterSupport object (see the Advanced JMX Example for JDK 6 for a discussion of these two notifications patterns).
The code required for our DynamicPOJOMBean is quite simple:
public class DynamicPOJOMBean implements DynamicMBean {
private static final Logger LOG =
Logger.getLogger(DynamicPOJOMBean.class.getName());
final Map<String,Method> getters;
final Map<String,Method> setters;
final Set<Method> operations;
final Object resource;
final MBeanInfo info;
public DynamicPOJOMBean(Object obj) {
getters = new LinkedHashMap<String,Method>();
setters = new LinkedHashMap<String,Method>();
operations = new LinkedHashSet<Method>();
resource = obj;
try {
info = initialize();
} catch (IntrospectionException ex) {
throw new IllegalArgumentException(obj.getClass().getName(),ex);
}
}
private MBeanInfo initialize() throws IntrospectionException {
final List<MBeanAttributeInfo> attributesInfo =
new ArrayList<MBeanAttributeInfo>();
final List<MBeanOperationInfo> operationsInfo =
new ArrayList<MBeanOperationInfo>();
final Set<String> attributesName = new HashSet<String>();
final ArrayList<Method> ops = new ArrayList<Method>();
for (Method m:resource.getClass().getMethods()) {
if (m.getDeclaringClass().equals(Object.class)) continue;
if (m.getName().startsWith("get") &&
!m.getName().equals("get") &&
!m.getName().equals("getClass") &&
m.getParameterTypes().length == 0 &&
m.getReturnType() != void.class) {
getters.put(m.getName().substring(3),m);
} else if (m.getName().startsWith("is") &&
!m.getName().equals("is") &&
m.getParameterTypes().length == 0 &&
m.getReturnType() == boolean.class) {
getters.put(m.getName().substring(2),m);
} else if (m.getName().startsWith("set") &&
!m.getName().equals("set") &&
m.getParameterTypes().length == 1 &&
m.getReturnType().equals(void.class)) {
setters.put(m.getName().substring(3),m);
} else {
ops.add(m);
}
}
attributesName.addAll(getters.keySet());
attributesName.addAll(setters.keySet());
for (String attrName : attributesName) {
final Method get = getters.get(attrName);
Method set = setters.get(attrName);
if (get != null && set != null &&
get.getReturnType() != set.getParameterTypes()[0]) {
set = null;
ops.add(setters.remove(attrName));
}
final MBeanAttributeInfo mbi =
getAttributeInfo(attrName,get,set);
if (mbi == null && get != null) {
ops.add(getters.remove(attrName));
}
if (mbi == null && set != null) {
ops.add(setters.remove(attrName));
}
if (mbi != null) attributesInfo.add(mbi);
}
for (Method m:ops) {
final MBeanOperationInfo opi = getOperationInfo(m);
if (opi == null) continue;
operations.add(m);
operationsInfo.add(opi);
}
return getMBeanInfo(resource,attributesInfo.
toArray(new MBeanAttributeInfo[attributesInfo.size()]),
operationsInfo.
toArray(new MBeanOperationInfo[operationsInfo.size()]));
}
protected MBeanAttributeInfo getAttributeInfo(String attrName,
Method get, Method set) throws IntrospectionException {
return new MBeanAttributeInfo(attrName,attrName,get,set);
}
protected MBeanOperationInfo getOperationInfo(Method m) {
if (m.getDeclaringClass()==Object.class) return null;
return new MBeanOperationInfo(m.getName(),m);
}
protected MBeanInfo getMBeanInfo(Object resource,
MBeanAttributeInfo[] attrs, MBeanOperationInfo[] ops) {
return new MBeanInfo(resource.getClass().getName(),
resource.getClass().getName(),attrs,null,ops,null);
}
public Object getAttribute(String attribute)
throws AttributeNotFoundException, MBeanException, ReflectionException {
final Method get = getters.get(attribute);
if (get == null) throw new AttributeNotFoundException(attribute);
try {
return get.invoke(resource);
} catch (IllegalArgumentException ex) {
throw new ReflectionException(ex);
} catch (InvocationTargetException ex) {
final Throwable cause = ex.getCause();
if (cause instanceof Exception)
throw new MBeanException((Exception)cause);
throw new RuntimeErrorException((Error)cause);
} catch (IllegalAccessException ex) {
throw new ReflectionException(ex);
}
}
public void setAttribute(Attribute attribute)
throws AttributeNotFoundException, InvalidAttributeValueException,
MBeanException, ReflectionException {
final Method set = setters.get(attribute);
if (set == null)
throw new AttributeNotFoundException(attribute.getName());
try {
set.invoke(resource,attribute.getValue());
} catch (IllegalArgumentException ex) {
throw new ReflectionException(ex);
} catch (InvocationTargetException ex) {
final Throwable cause = ex.getCause();
if (cause instanceof Exception)
throw new MBeanException((Exception)cause);
throw new RuntimeErrorException((Error)cause);
} catch (IllegalAccessException ex) {
throw new ReflectionException(ex);
}
}
public AttributeList getAttributes(String[] attributes) {
if (attributes == null) return new AttributeList();
final List<Attribute> result =
new ArrayList<Attribute>(attributes.length);
for (String attr : attributes) {
final Method get = getters.get(attr);
try {
result.add(new Attribute(attr,get.invoke(resource)));
} catch (Exception x) {
continue;
}
}
return new AttributeList(result);
}
public AttributeList setAttributes(AttributeList attributes) {
if (attributes == null) return new AttributeList();
final List<Attribute> result =
new ArrayList<Attribute>(attributes.size());
for (Object item : attributes) {
final Attribute attr = (Attribute)item;
final String name = attr.getName();
final Method set = setters.get(name);
try {
set.invoke(resource,attr.getValue());
final Method get = getters.get(name);
final Object newval =
(get==null)?attr.getValue():get.invoke(resource);
result.add(new Attribute(name,newval));
} catch (Exception x) {
continue;
}
}
return new AttributeList(result);
}
public Object invoke(String actionName, Object[] params, String[] signature)
throws MBeanException, ReflectionException {
Method toInvoke = null;
if (params == null) params = new Object[0];
if (signature == null) signature = new String[0];
for (Method m : operations) {
if (!m.getName().equals(actionName)) continue;
final Class[] sig = m.getParameterTypes();
if (sig.length == params.length) {
if (sig.length == 0) toInvoke=m;
else if (signature.length == sig.length) {
toInvoke = m;
for (int i=0;i<sig.length;i++) {
if (!sig[i].getName().equals(signature[i])) {
toInvoke = null;
break;
}
}
}
}
if (toInvoke != null) break;
}
if (toInvoke == null)
throw new ReflectionException(new NoSuchMethodException(actionName));
try {
return toInvoke.invoke(resource,params);
} catch (IllegalArgumentException ex) {
throw new ReflectionException(ex);
} catch (InvocationTargetException ex) {
final Throwable cause = ex.getCause();
if (cause instanceof Exception)
throw new MBeanException((Exception)cause);
throw new RuntimeErrorException((Error)cause);
} catch (IllegalAccessException ex) {
throw new ReflectionException(ex);
}
}
public MBeanInfo getMBeanInfo() {
return info;
}
}
|
I have tested this DynamicPOJOMBean with the same dummy resource that I used for my ModelMBean above. Here is a JConsole window that shows both MBeans: "test:type=MyPojo,name=Model" is the previously coded ModelMBean, "test:type=MyPojo,name=dynamic" is my lighter DynamicMBean version...
The drawback of the two methods described above is when your POJO use custom classes (other POJOs) as input/output to its getter/setter and/or operations. In that case - if you use a plain MBean to expose your POJO, you will need to:
- make sure that those custom classes are serializable
- distribute the jars containing those classes to the clients that monitor your POJO.
In that case, using an MXBean would be the best answer. We have already discussed how to turn your POJO into an MXBean, in the first section of this blog. However, this implies that you can modify the source code of your object, and make it implement an MXBean interface.
An alternative to that would be to wrap your POJO inside a java.lang.reflect.Proxy that would implement the appropriate MXBean interface. This is one of the use cases described by Eamonn in one of his recent blog entries. Here are the steps that would be required:
- You would need to write an MXBean interface containing all the getters, setters, and methods you want to expose.
- You would need to write an InvocationHandler, which when invoked on a method m defined in 1), finds out the method with the same name and signature on your object, and invokes it. The code of the InvocationHandler below is extracted from Eamon's blog: Build Your Own Interface
The MBeanInvocationHandler used in the constructed Proxy looks like this:
public class MBeanInvocationHandler implements InvocationHandler {
public MBeanInvocationHandler(Object wrapped) {
this.wrapped = wrapped;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Class wrappedClass = wrapped.getClass();
Method methodInWrapped =
wrappedClass.getMethod(method.getName(), method.getParameterTypes());
try {
return methodInWrapped.invoke(wrapped, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
private final Object wrapped;
}
- Using this invocation handler, you would be able to create a Proxy implementing your MXBean interface created in 1) and forward all its methods to the wrapped Plain Old Java Object.
Provided that the name of the interface you provide ends with "MXBean", the proxy obtained will implement that MXBean interface, and is therefore itself an MXBean, that you can directly register in an MBeanServer.
Of course, in this case, you still need to write an MXBean interface for your POJO, but your POJO no longer needs to implement it - it simply needs to have the "same" methods than those defined in the interface. The code required for the InvocationHandler is completely generic, and is driven by introspection of the provided interface. If your reason for not turning your POJO into an MBean was simply that you couldn't change its codebase, then statically writing an MXBean interface for creating an MXBean proxy (or statically generating that interface by introspecting your POJO class) might be enough to suit your needs.
This latter case is a use case that exactly corresponds to what is described by Eamonn in his blog about generating interfaces. Instead of statically writing (or generating) the interface needed to create the MXBean Proxy for your POJO, you could dynamically generate it at runtime.
Before you start generating code however, make really sure that none of the previously described methods can be used!
One of the RFE considered for JMX 2.0 is Annotations to simplify MBean development. This could come in the form of e.g. @Managed
annotations as suggested in Eamonn's Blog. Having such annotation would not solve our original problem, which was to expose a resource whose source code cannot be changed, but it could simplify the development of MBean wrappers, such as those discussed in this entry.
What? What time is it? So late? When I started writing this entry, I thought it would take me ten minutes! I had only these two small classes that I wanted to share! Sorry to have been so long, now I need to run!
Cheers,
-- daniel
See also: javax.management.StandardMBean: When and Why.
Look also for other JMX related articles in this blog...
Tags: java jmx management mbean
Posted by dfuchs ( Nov 22 2006, 09:01:43 PM CET ) Permalink Comments [3]