Developing custom MBeans to manage J2EE Applications (Part III)
- by philippe Le Mouel
This is the third and final part in a series of blogs, that demonstrate how to add management capability to your own application using JMX MBeans.
In Part I we saw:
How to implement a custom MBean to manage configuration associated with an application.
How to package the resulting code and configuration as part of the application's ear file.
How to register MBeans upon application startup, and unregistered them upon application stop (or undeployment).
How to use generic JMX clients such as JConsole to browse and edit our application's MBean.
In Part II we saw:
How to add localized descriptions to our MBean, MBean attributes, MBean operations and MBean operation parameters.
How to specify meaningful name to our MBean operation parameters.
We also touched on future enhancements that will simplify how we can implement localized MBeans.
In this third and last part, we will re-write our MBean to simplify how we added localized descriptions. To do so we will take advantage of the functionality we already described in part II and that is now part of WebLogic 10.3.3.0.
We will show how to take advantage of WebLogic's localization support to localize our MBeans based on the client's Locale independently of the server's Locale. Each client will see MBean descriptions localized based on his/her own Locale. We will show how to achieve this using JConsole, and also using a sample programmatic JMX Java client.
The complete code sample and associated build files for part III are available as a zip file. The code has been tested against WebLogic Server 10.3.3.0 and JDK6. To build and deploy our sample application, please follow the instruction provided in Part I, as they also apply to part III's code and associated zip file.
Providing custom descriptions take II
In part II we localized our MBean descriptions by extending the StandardMBean class and overriding its many getDescription methods. WebLogic 10.3.3.0 similarly to JDK 7 can automatically localize MBean descriptions as long as those are specified according to the following conventions:
Descriptions resource bundle keys are named according to:
MBean description: <MBeanInterfaceClass>.mbean
MBean attribute description: <MBeanInterfaceClass>.attribute.<AttributeName>
MBean operation description: <MBeanInterfaceClass>.operation.<OperationName>
MBean operation parameter description: <MBeanInterfaceClass>.operation.<OperationName>.<ParameterName>
MBean constructor description: <MBeanInterfaceClass>.constructor.<ConstructorName>
MBean constructor parameter description: <MBeanInterfaceClass>.constructor.<ConstructorName>.<ParameterName>
We also purposely named our resource bundle class MBeanDescriptions and included it as part of the same package as our MBean.
We already followed the above conventions when creating our resource bundle in part II, and our default resource bundle class with English descriptions looks like:
package blog.wls.jmx.appmbean;
import java.util.ListResourceBundle;
public class MBeanDescriptions extends ListResourceBundle {
protected Object[][] getContents() {
return new Object[][] {
{"PropertyConfigMXBean.mbean",
"MBean used to manage persistent application properties"},
{"PropertyConfigMXBean.attribute.Properties",
"Properties associated with the running application"},
{"PropertyConfigMXBean.operation.setProperty",
"Create a new property, or change the value of an existing property"},
{"PropertyConfigMXBean.operation.setProperty.key",
"Name that identify the property to set."},
{"PropertyConfigMXBean.operation.setProperty.value",
"Value for the property being set"},
{"PropertyConfigMXBean.operation.getProperty",
"Get the value for an existing property"},
{"PropertyConfigMXBean.operation.getProperty.key",
"Name that identify the property to be retrieved"}
};
}
}
We have now also added a resource bundle with French localized descriptions:
package blog.wls.jmx.appmbean;
import java.util.ListResourceBundle;
public class MBeanDescriptions_fr extends ListResourceBundle {
protected Object[][] getContents() {
return new Object[][] {
{"PropertyConfigMXBean.mbean",
"Manage proprietes sauvegarde dans un fichier disque."},
{"PropertyConfigMXBean.attribute.Properties",
"Proprietes associee avec l'application en cour d'execution"},
{"PropertyConfigMXBean.operation.setProperty",
"Construit une nouvelle proprietee, ou change la valeur d'une proprietee existante."},
{"PropertyConfigMXBean.operation.setProperty.key",
"Nom de la propriete dont la valeur est change."},
{"PropertyConfigMXBean.operation.setProperty.value",
"Nouvelle valeur"},
{"PropertyConfigMXBean.operation.getProperty",
"Retourne la valeur d'une propriete existante."},
{"PropertyConfigMXBean.operation.getProperty.key",
"Nom de la propriete a retrouver."}
};
}
}
So now we can just remove the many getDescriptions methods from our MBean code, and have a much cleaner:
package blog.wls.jmx.appmbean;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.net.URL;
import java.util.Map;
import java.util.HashMap;
import java.util.Properties;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.MBeanRegistration;
import javax.management.StandardMBean;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
public class PropertyConfig extends StandardMBean implements
PropertyConfigMXBean, MBeanRegistration {
private String relativePath_ = null;
private Properties props_ = null;
private File resource_ = null;
private static Map operationsParamNames_ = null;
static {
operationsParamNames_ = new HashMap();
operationsParamNames_.put("setProperty", new String[] {"key", "value"});
operationsParamNames_.put("getProperty", new String[] {"key"});
}
public PropertyConfig(String relativePath) throws Exception {
super(PropertyConfigMXBean.class , true);
props_ = new Properties();
relativePath_ = relativePath;
}
public String setProperty(String key,
String value) throws IOException {
String oldValue = null;
if (value == null) {
oldValue = String.class.cast(props_.remove(key));
} else {
oldValue = String.class.cast(props_.setProperty(key, value));
}
save();
return oldValue;
}
public String getProperty(String key) {
return props_.getProperty(key);
}
public Map getProperties() {
return (Map) props_;
}
private void load() throws IOException {
InputStream is = new FileInputStream(resource_);
try {
props_.load(is);
}
finally {
is.close();
}
}
private void save() throws IOException {
OutputStream os = new FileOutputStream(resource_);
try {
props_.store(os, null);
}
finally {
os.close();
}
}
public ObjectName preRegister(MBeanServer server, ObjectName name)
throws Exception {
// MBean must be registered from an application thread
// to have access to the application ClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
URL resourceUrl = cl.getResource(relativePath_);
resource_ = new File(resourceUrl.toURI());
load();
return name;
}
public void postRegister(Boolean registrationDone) { }
public void preDeregister() throws Exception {}
public void postDeregister() {}
protected String getParameterName(MBeanOperationInfo op,
MBeanParameterInfo param,
int sequence) {
return operationsParamNames_.get(op.getName())[sequence];
}
}
The only reason we are still extending the StandardMBean class, is to override the default values for our operations parameters name. If this isn't a concern, then one could just write the following code:
package blog.wls.jmx.appmbean;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.net.URL;
import java.util.Properties;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.MBeanRegistration;
import javax.management.StandardMBean;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
public class PropertyConfig implements
PropertyConfigMXBean, MBeanRegistration {
private String relativePath_ = null;
private Properties props_ = null;
private File resource_ = null;
public PropertyConfig(String relativePath) throws Exception {
props_ = new Properties();
relativePath_ = relativePath;
}
public String setProperty(String key,
String value) throws IOException {
String oldValue = null;
if (value == null) {
oldValue = String.class.cast(props_.remove(key));
} else {
oldValue = String.class.cast(props_.setProperty(key, value));
}
save();
return oldValue;
}
public String getProperty(String key) {
return props_.getProperty(key);
}
public Map getProperties() {
return (Map) props_;
}
private void load() throws IOException {
InputStream is = new FileInputStream(resource_);
try {
props_.load(is);
}
finally {
is.close();
}
}
private void save() throws IOException {
OutputStream os = new FileOutputStream(resource_);
try {
props_.store(os, null);
}
finally {
os.close();
}
}
public ObjectName preRegister(MBeanServer server, ObjectName name)
throws Exception {
// MBean must be registered from an application thread
// to have access to the application ClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
URL resourceUrl = cl.getResource(relativePath_);
resource_ = new File(resourceUrl.toURI());
load();
return name;
}
public void postRegister(Boolean registrationDone) { }
public void preDeregister() throws Exception {}
public void postDeregister() {}
}
Note: The above would also require changing the operations parameters name in the resource bundle classes. For instance: PropertyConfigMXBean.operation.setProperty.key would become: PropertyConfigMXBean.operation.setProperty.p0
Client based localization
When accessing our MBean using JConsole started with the following command line:
jconsole -J-Djava.class.path=$JAVA_HOME/lib/jconsole.jar:$JAVA_HOME/lib/tools.jar: $WL_HOME/server/lib/wljmxclient.jar -J-Djmx.remote.protocol.provider.pkgs=weblogic.management.remote -debug
We see that our MBean descriptions are localized according to the WebLogic's server Locale. English in this case:
Note: Consult Part I for information on how to use JConsole to browse/edit our MBean.
Now if we specify the client's Locale as part of the JConsole command line as follow:
jconsole -J-Djava.class.path=$JAVA_HOME/lib/jconsole.jar:$JAVA_HOME/lib/tools.jar: $WL_HOME/server/lib/wljmxclient.jar -J-Djmx.remote.protocol.provider.pkgs=weblogic.management.remote -J-Dweblogic.management.remote.locale=fr-FR -debug
We see that our MBean descriptions are now localized according to the specified client's Locale. French in this case:
We use the weblogic.management.remote.locale system property to specify the Locale that should be associated with the cient's JMX connections. The value is composed of the client's language code and its country code separated by the - character. The country code is not required, and can be omitted. For instance: -Dweblogic.management.remote.locale=fr
We can also specify the client's Locale using a programmatic client as demonstrated below:
package blog.wls.jmx.appmbean.client;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.MBeanInfo;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.JMXConnectorFactory;
import java.util.Hashtable;
import java.util.Set;
import java.util.Locale;
public class JMXClient {
public static void main(String[] args) throws Exception {
JMXConnector jmxCon = null;
try {
JMXServiceURL serviceUrl =
new JMXServiceURL(
"service:jmx:iiop://127.0.0.1:7001/jndi/weblogic.management.mbeanservers.runtime");
System.out.println("Connecting to: " + serviceUrl);
// properties associated with the connection
Hashtable env = new Hashtable();
env.put(JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES,
"weblogic.management.remote");
String[] credentials = new String[2];
credentials[0] = "weblogic";
credentials[1] = "weblogic";
env.put(JMXConnector.CREDENTIALS, credentials);
// specifies the client's Locale
env.put("weblogic.management.remote.locale", Locale.FRENCH);
jmxCon = JMXConnectorFactory.newJMXConnector(serviceUrl, env);
jmxCon.connect();
MBeanServerConnection con = jmxCon.getMBeanServerConnection();
Set mbeans =
con.queryNames(
new ObjectName(
"blog.wls.jmx.appmbean:name=myAppProperties,type=PropertyConfig,*"),
null);
for (ObjectName mbeanName : mbeans) {
System.out.println("\n\nMBEAN: " + mbeanName);
MBeanInfo minfo = con.getMBeanInfo(mbeanName);
System.out.println("MBean Description: "+minfo.getDescription());
System.out.println("\n");
}
}
finally {
// release the connection
if (jmxCon != null)
jmxCon.close();
}
}
}
The above client code is part of the zip file associated with this blog, and can be run using the provided client.sh script. The resulting output is shown below:
$ ./client.sh
Connecting to: service:jmx:iiop://127.0.0.1:7001/jndi/weblogic.management.mbeanservers.runtime
MBEAN: blog.wls.jmx.appmbean:type=PropertyConfig,name=myAppProperties
MBean Description: Manage proprietes sauvegarde dans un fichier disque.
$
Miscellaneous
Using Description annotation to specify MBean descriptions
Earlier we have seen how to name our MBean descriptions resource keys, so that WebLogic 10.3.3.0 automatically uses them to localize our MBean. In some cases we might want to implicitly specify the resource key, and resource bundle. For instance when operations are overloaded, and the operation name is no longer sufficient to uniquely identify a single operation. In this case we can use the Description annotation provided by WebLogic as follow:
import weblogic.management.utils.Description;
@Description(resourceKey="myapp.resources.TestMXBean.description",
resourceBundleBaseName="myapp.resources.MBeanResources")
public interface TestMXBean {
@Description(resourceKey="myapp.resources.TestMXBean.threshold.description",
resourceBundleBaseName="myapp.resources.MBeanResources" )
public int getthreshold();
@Description(resourceKey="myapp.resources.TestMXBean.reset.description",
resourceBundleBaseName="myapp.resources.MBeanResources")
public int reset(
@Description(resourceKey="myapp.resources.TestMXBean.reset.id.description",
resourceBundleBaseName="myapp.resources.MBeanResources",
displayNameKey=
"myapp.resources.TestMXBean.reset.id.displayName.description")
int id);
}
The Description annotation should be applied to the MBean interface. It can be used to specify MBean, MBean attributes, MBean operations, and MBean operation parameters descriptions as demonstrated above.
Retrieving the Locale associated with a JMX operation from the MBean code
There are several cases where it is necessary to retrieve the Locale associated with a JMX call from the MBean implementation. For instance this can be useful when localizing exception messages. This can be done as follow:
import weblogic.management.mbeanservers.JMXContextUtil;
......
// some MBean method implementation
public String setProperty(String key, String value) throws IOException {
Locale callersLocale = JMXContextUtil.getLocale();
// use callersLocale to localize Exception messages or
// potentially some return values such a Date ....
}
Conclusion
With this last part we conclude our three part series on how to write MBeans to manage J2EE applications. We are far from having exhausted this particular topic, but we have gone a long way and are now capable to take advantage of the latest functionality provided by WebLogic's application server to write user friendly MBeans.