|
|
DynamicMBeans, ModelMBeans, and Pojos... |
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 a RequiredModelMBean 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 a DynamicPOJOMBean 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 a
DynamicPOJOEmitterMBean that extends DynamicPOJOMBean and
implements NotificationEmitter, and delegates notification handling to
a wrapped NotificationBroadcasterSupport 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
|
Or you could just use Spring :-) The org.springframework.jmx.export package does all the cruft for you ...
Map<String, Object> mBeans = new HashMap<String, Object>(); ... populate using jmx object names ... mBeans.put(objectNameString, pojo); MBeanExporter mBeanExporter = new MBeanExporter(); mBeanExporter.setServer(mbeanServer); mBeanExporter.setBeans(mBeans); mBeanExporter.afterPropertiesSet();Cheers Matthias
Posted by Matthias Ernst on November 23, 2006 at 08:11 AM CET #
Hi Daniel, I'm using this in my graduation work, I hope it's ok to you.
I want to use your approach to automate the instrumentation of any application on the system, especially those already running, and add some functionalities like a helper to create notifications and so on.
My problem is to get an object reference for another process or any other way to link my generated MBean with its resource(the application). Do you know how can I do this?
Thanks
Posted by Gustavo on September 10, 2007 at 09:20 PM CEST #
It's a great piece of code. It should find place in Apache Commons.
In DynamicPOJOMBean, method setAttribute(Attribute attribute) there should be final Method set = setters.get(attribute.getName());
And placing imports in your code would be very helpful.
Posted by Łukasz Lech on March 17, 2009 at 03:00 PM CET #