30 June 2008

Application MBean NotificationListeners

And another example from an OTN question.

http://forums.oracle.com/forums/thread.jspa?messageID=2619654&tstart=0#2619654

I'm using OC4J 10.1.3 and I see, in Oracle MBean browser, that a mbean is created and registered each time an EAR is deployed :

"oc4j:j2eeType=J2EEApplication,name=,J2EEServer=standalone"

I tried to registerer to notifications of this mbean (j2ee.state.starting and j2ee.state.stopping) using web console. It works, I can see notifications in web console.

Now, I would to like to registered to these notifications in my application. I don't find sample in oc4j documentation on how doing this.

Has anybody already use this functionality ?


Update 03-July-2008: I posted an example of this on the OC4J How-To page. The zip file is available here:
http://www.oracle.com/technology/tech/java/oc4j/10130/how_to/application-jmx-listener.zip.

The short answer is that you can register for notifications from any of the OC4J MBeans using the standard JMX API -- no worries at all.

However there are a couple of tricks to keep in mind when working with OC4J.

Trick 1 : from within OC4J, an application has restricted access to the OC4J MBean set. We provide applications with a proxy to the MBeanServer that permits access to only the MBeans that the application itselt registers. Or in other words, it prohibits applications from seeing and using the OC4J MBeans when using the MBeanServerFactory.getMBeanServer() call from within the application code. There are basically two ways to deal with this.

I. You can disable the security aspect of the proxy. This can be done by setting the Java System property "oc4j.jmx.security.proxy.off" when OC4J is launched. Beware however that this applies to the entire container, so any deployed applications will have access to the OC4J MBeans. Beware.

II. You can establish what amounts to a loopback remote connection using the JMX Remote API. When doing this, you can authenticate yourself as a user from within the oc4j-app-administrators role, and thus the proxy you receive does not have any restrictions placed on it. The downside to doing this is that the connection must be constructed using the URL and username/password of the respective user you want to connect as. Therefore you are dealing with ports and password visibility issues.

With that understood, lets assume that you now have access to the OC4J MBeans using JMXConnector.

A JMX listener is simply a class that implements the javax.management.NotificationListener interface. You create a class that implements this interface, and then you register your interest with the MBeanServer for receiving Notifications that are emitted from a specific MBean using its addNotificationListener method.

Trick 2: in order to receive Notifications from an MBean, the connection you used to register with the MBeanServer must be maintained. If the connection closes, the link between the MBean and your Listener is gone. Therefore when you are designing your application, you need to use a model that allows the connection to be maintained across requests, but at the same time, you need to do it such that the connections do not place an undue resource tax on the operation of the server. You probably don't want to store a JMXConnector in each HttpSession of each client for example.

As a demonstration of these tricks, I have pulled together a demo application to show what an example may look like.

Points of Interest:

JMXListenerBean -- this JavaBean implements the NotificationListener interface. When Notifications are received they are stored in a List maintained by the JavaBean. This enables the Notifications to be retrieved and rendered in a client of some form.

GlobalListenerMap -- the application uses one global class -- GlobalListenerMap -- to create and maintain the connection to the MBeanServer. This class is also where the individual MBean listeners are registered and stored for each client. The class is created and placed in the ServletContext by a ServletContextListener; it is also closed when the ServletContext is destroyed, where it closes its JMXConnector. When a new browser client asks to register a listener with a specific MBean, a servlet creates the actual listening class and submits it to the GlobalListenerMap to register with the MBeanServer. The GlobalListenerMap then stores the listener in List, which is then stored in a Map using the client sessionId as the key.

ServletContextListener -- a ServletContextListener is used to manage the GlobalListenerMap. When the ServletContext is created, it creates an instance of the GlobalListenerMap and places it in the ServletContext to be shared by all the artifacts of the application. When the ServletContext is destroyed, it closes down the GlobalListenerMap allowing it to close its JMXConnector. This class also implements the HttpSessionListener interface, so that as a client session ends/is invalidated, any listeners the client had previously registered are removed from the GlobalListenerMap.

Front End -- a simple front end to allow a client to register interest with an MBean is provided, and then view the notifications.

JMXListenerBean.java
package sab.demo.jmx.listener;

import java.text.DateFormat;
import java.util.*;
import java.util.logging.Logger;
import javax.management.*;

/**
* This JavaBean acts as a JMX NotificationListener. It
* gets populated with an MBean name and it can then be registered
* with a given MBeanServer. This is quick and dirty, it does no
* validate of the MBean name, etc.
*
* Any notifications this listener receives are stored in both the messages
* and notifications lists -- these can then be used on the client side as
* desired.
*
*/

public class JMXListenerBean implements NotificationListener {

Logger logger = Logger.getLogger(this.getClass().getCanonicalName());
ArrayList<String> messages = new ArrayList<String>();
ArrayList<Notification>notifications = new ArrayList<Notification>();

private final Date created = new Date();
private String mBean = null;
private final DateFormat DF = DateFormat.getDateTimeInstance();

public JMXListenerBean() {
logger.info("JMXListenerBean: constructor");
}

/**
* Register this listener with the given MBeanServer.
*
@param mbs - MBeanServerConnection
* @throws Exception
*/
public void register(MBeanServerConnection mbs) throws Exception{
logger.info("registering mbean:" + mBean);
if("".equalsIgnoreCase(mBean) || mBean == null) {
throw new Exception("MBean name must be set before calling register");
}

try {
ObjectName on = new ObjectName(mBean);
mbs.addNotificationListener(on, this, null, null);
} catch (Exception ex) {
ex.printStackTrace();
throw ex;
}
}

public void unregister(MBeanServerConnection mbs) throws Exception{
logger.info("unregistering mbean:" + mBean);
if("".equalsIgnoreCase(mBean) || mBean == null) {
throw new Exception("MBean name must be set before calling register");
}

try {
ObjectName on = new ObjectName(mBean);
mbs.removeNotificationListener(on, this);
} catch (Exception ex) {
ex.printStackTrace();
throw ex;
}
}


/**
* This is where the notifications are delivered and handled.
*
@param notification
* @param handback
*/
public void handleNotification(Notification notification,
Object handback) {
logger.info("handleNotification");
// Add the notification to the list
notifications.add(notification);
// Make a string from the notification
messages.add(
String.format("[%s]\t%s\t%s",
DF.format(new Date(notification.getTimeStamp())),
notification.getType(),
notification.getMessage()));
}

public void setMBean(String mbean) {
this.mBean = mbean;
}

public String getMBean() {
return mBean;
}

/**
* Get the list of messages.
*
@return List of messages as Strings
*/
public ArrayList<String> getMessages() {
return messages;
}

/**
* Get the list of raw notifications
*
@return List of notifications
*/
public ArrayList<Notification> getNotifications() {
return notifications;
}

@Override
public String toString() {
return mBean;
}
}

GlobalListenerMap.java

package sab.demo.jmx.map;

import java.sql.Date;
import java.text.DateFormat;
import java.util.*;
import java.util.logging.Logger;
import javax.management.*;
import javax.management.remote.*;
import sab.demo.jmx.listener.JMXListenerBean;

/**
* This class acts as a manager for JMXListenerBeans, and the OC4J MBeanServer
* that they are registered with. It enables an application to signal interest
* for notifications from an MBean, whereupon it handles the registration of the
* listener with the MBean. It maintains the JMXConnection while the class is
* in use, providing a route for the MBeanServer to get the notifications from
* the MBean to the listener.
*
*/

public class GlobalListenerMap {

private Date created = null;

private HashMap<String, List<JMXListenerBean>> sessionListenerList = new HashMap<String, List<JMXListenerBean>>();
private JMXConnector jmxConnector= null;
private Object MUTEX = new Object();
private boolean eagerCloseConnection = true;

private static final String URL = "service:jmx:rmi://localhost:23791";
private static final String USERNAME = "oc4jadmin";
private static final String PASSWORD = "welcome1";
private final Logger logger = Logger.getLogger(this.getClass().getName());

public GlobalListenerMap() {
this(true);
}

public GlobalListenerMap(boolean eagerCloseConnection) {
this.eagerCloseConnection = eagerCloseConnection;
created = new Date(System.currentTimeMillis());
}

public synchronized void addListenerForSession(String sessionId, String mbeanName) throws Exception {
JMXListenerBean listener = new JMXListenerBean();
listener.setMBean(mbeanName);
addListenerForSession(sessionId, listener);
}

public synchronized void addListenerForSession(String sessionId, JMXListenerBean listener) throws Exception {
List<JMXListenerBean> listeners = sessionListenerList.get(sessionId);
if(listeners == null) {
listeners = new ArrayList<JMXListenerBean>();
}
if(jmxConnector == null) {
jmxConnector = getJMXConnector();
}
MBeanServerConnection mbs = jmxConnector.getMBeanServerConnection();
listener.register(mbs);
listeners.add(listener);
sessionListenerList.put(sessionId, listeners);
}

public List<JMXListenerBean>getListenerListForSession(String sessionId) {
return sessionListenerList.get(sessionId);
}

public synchronized void removeListenerListForSession(String sessionId) {
List<JMXListenerBean> listeners = sessionListenerList.get(sessionId);
// Unregister any listeners this session had established
try {
for(JMXListenerBean listener: listeners) {
listener.unregister(jmxConnector.getMBeanServerConnection());
}
} catch(Exception e) {
// TODO: handle unregister errors gracefully, ignore for now
}

// Now remove the session from the list
sessionListenerList.remove(sessionId);

// Eaglerly close the connection if configured to do so
if(eagerCloseConnection && sessionListenerList.size()==0) {
try {
close(true);
} catch (Exception ex) {
// Ignore the still registered error
}
}
}

/**
* Performs a shutdown on the listener map, will throw an exception if there
* are listeners still registered.
*
@throws Exception
*/
public void shutdown() throws Exception {
// care
close(true);
}

public void shutdownDontCare() throws Exception {
// don't care
close(false);
}

/**
* Close the GlobalMapListener down.
*
@param care -- whether to care if there are still registered listeners.
* @throws Exception
*/
private synchronized void close(boolean care) throws Exception {
if(care) {
if (sessionListenerList.size() != 0) {
throw new Exception("Can't close myself as listeners are still registered");
}
}
sessionListenerList.clear();
if (jmxConnector != null) {
jmxConnector.close();
jmxConnector = null;
}
}

/**
* Get a JMXConnector using the hardcoded URL, USERNAME, PASSWORD
*
@return
* @throws Exception
*/
private JMXConnector getJMXConnector() throws Exception {
return getJMXConnector(URL, USERNAME, PASSWORD);
}

/**
* Get a JMXConnector using the supplied parameters.
*
@param url
* @param username
* @param password
* @return JMXConnector
* @throws Exception
*/
private JMXConnector getJMXConnector(String url, String username,
String password) throws Exception {
JMXConnector jmxcon = null;

// If not, create one and store it for future use.
JMXServiceURL jmxurl = new JMXServiceURL(url);
Hashtable credentials = new Hashtable();
credentials.put("login", username);
credentials.put("password", password);

// Properties required to use the OC4J ORMI protocol
Hashtable env = new Hashtable();
env.put(JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES, "oracle.oc4j.admin.jmx.remote");
env.put(JMXConnector.CREDENTIALS, credentials);

// Get a connection
jmxcon = JMXConnectorFactory.newJMXConnector(jmxurl, env);
jmxcon.connect();
return jmxcon;
}

/**
* Just print out some debug messages
*/

public void debug() {
logger.fine("GlobalListenerMap: created " +
DateFormat.getDateTimeInstance().format(created));
logger.fine("Sessions in use: " + sessionListenerList.keySet().size());
for(String sessionid: sessionListenerList.keySet()) {
logger.fine("[" + sessionid + "]");
for(JMXListenerBean listener: sessionListenerList.get(sessionid)) {
logger.fine(listener.getMBean() + ", has received [" +
listener.getNotifications().size() +
"] notifications" );
}
}
}

/**
* Quick and dirty query to get the list of mbean names for the deployed
* applications.
*
@return Set of Stringified ObjectNames
*/
public Set<String> getJ2EEApplicationNameList() {
logger.info("entering");
SortedSet<String> appnames = new TreeSet<String>();
Set<ObjectName>mbeans = null;
try {
if(jmxConnector == null) {
jmxConnector = getJMXConnector();
}
ObjectName query = new ObjectName("*:j2eeType=J2EEApplication,*");
mbeans = (Set<ObjectName>)
jmxConnector.getMBeanServerConnection().queryNames(query, null);
for(ObjectName mbean: mbeans) {
appnames.add(mbean.getCanonicalName());
}
} catch (Exception ex) {
ex.printStackTrace();
}
return appnames;
}

}
ServletContextListener

package sab.demo.jmx.listener;

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import sab.demo.jmx.map.GlobalListenerMap;


public class ServletContextListener implements javax.servlet.ServletContextListener,
HttpSessionListener {
private static final String GLM_KEY = "GlobalListenerMap";
private ServletContext context = null;
private HttpSession session = null;
private final Logger logger = Logger.getLogger(this.getClass().getName());

/**
* Create a new GlobalListenerMap and put it in the context for use by
* the application artifacts.
*
@param event
*/
public void contextInitialized(ServletContextEvent event) {
logger.fine("contextInitialized");
context = event.getServletContext();
GlobalListenerMap glm = new GlobalListenerMap();
context.setAttribute(GLM_KEY, glm);

}

/**
* When the application is being shutdown, close the GlobalListenerMap so
* that the JMXConnector is closed.
*
@param event
*/
public void contextDestroyed(ServletContextEvent event) {
context = event.getServletContext();
logger.info("Closing GlobalListenerMap");
GlobalListenerMap glm = (GlobalListenerMap)context.getAttribute(GLM_KEY);
try {
if(glm!=null) {
glm.shutdownDontCare();
}
} catch(Exception e) {
e.printStackTrace();
}
}

/**
* When a new session is created ... don't really do anything.
*
@param event
*/
public void sessionCreated(HttpSessionEvent event) {
session = event.getSession();
ServletContext context = event.getSession().getServletContext();
GlobalListenerMap glm = (GlobalListenerMap)context.getAttribute(GLM_KEY);
// Could proactively create a session list here if we wanted
if(glm == null && logger.isLoggable(Level.SEVERE)) {
logger.severe("GlobalListenerMap in ServletContext is null");
}

}

/**
* When a specific user session is closed, remove any listeners that may
* have been registered by them from the GlobalListenerMap.
*
@param event
*/
public void sessionDestroyed(HttpSessionEvent event) {
session = event.getSession();
logger.info("Removing listeners for session: " + session.getId());
// Clean up ..
ServletContext context = event.getSession().getServletContext();
GlobalListenerMap glm = (GlobalListenerMap)context.getAttribute(GLM_KEY);
if(glm!=null) {
glm.removeListenerListForSession(session.getId());
}
}
}
RegistrationServlet

package sab.demo.jmx.web;

import java.io.IOException;
import java.io.PrintWriter;

import java.util.HashSet;
import java.util.Hashtable;


import java.util.List;
import java.util.logging.Logger;

import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import javax.servlet.*;
import javax.servlet.http.*;

import sab.demo.jmx.map.GlobalListenerMap;
import sab.demo.jmx.listener.JMXListenerBean;

public class RegistrationServlet extends HttpServlet {
private static final String CONTENT_TYPE = "text/html; charset=windows-1252";

private final Logger logger = Logger.getLogger(this.getClass().getName());
private ServletConfig config = null;

public void init(ServletConfig config) throws ServletException {
super.init(config);
this.config = config;
}

public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {response.setContentType(CONTENT_TYPE);

try {
// Get the MBean name parameter
String mbean = request.getParameter("mbean");
if(mbean==null || "".equalsIgnoreCase(mbean)) {
throw new ServletException("MBean parameter was not provided.");
}
logger.info("Request for registering MBean: " + mbean);

ServletContext ctx = config.getServletContext();
GlobalListenerMap glm = (GlobalListenerMap) ctx.getAttribute("GlobalListenerMap");

// Create a new listener bean and add it to the global list
JMXListenerBean listener = new JMXListenerBean();
listener.setMBean(mbean);
glm.addListenerForSession(request.getSession().getId(), listener);

// Tack on some debug information to the output
if(request.getParameterMap().containsKey("debug")) {
glm.debug();
}
} catch (Exception ex) {
ex.printStackTrace();
request.setAttribute("javax.servlet.jsp.jspException", ex);
request.getRequestDispatcher("error.jsp").forward(request, response);
} finally {
}

request.getRequestDispatcher("listnotifications.jsp").forward(request, response);
}

public void doPost(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws ServletException, IOException {
doGet(httpServletRequest, httpServletResponse);
}
}
listnotifications.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<%@ page contentType="text/html;charset=windows-1252"%>
<%@ page import="sab.demo.jmx.listener.*"%>
<%@ page import="sab.demo.jmx.map.*"%>
<%@ page import="java.util.*"%>
<%@ page import="javax.management.*"%>
<%@ page import="java.text.DateFormat"%>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252"/>
<title>Index</title>
<STYLE TYPE="text/css">
td, body{font-family: Arial; font-size: 10pt;}
span.listener { color: #FF1111; font-weight: bold;}
td.state { text-transform: uppercase;}
</STYLE>
</head>
<body>
<a href="registerlistener.jsp">Register a Listener</a>
&nbsp;&nbsp;
<a href="listnotifications.jsp">Reload Page</a>
<h3>Registered Listeners</h3>

<%
DateFormat DF = DateFormat.getDateTimeInstance();
ServletContext ctx = config.getServletContext();
GlobalListenerMap glm = (GlobalListenerMap)ctx.getAttribute("GlobalListenerMap");
List<JMXListenerBean> listeners = glm.getListenerListForSession(session.getId());


if(listeners!=null) {
for (JMXListenerBean listener: listeners) {
out.println("<p><span class=\"listener\">" + listener.getMBean() + "</span></p>");
out.println("<table>");
for(Notification notification: listener.getNotifications()) {
String line = String.format("<tr font=\"arial\"><td>[%s]</td><td class=\"state\">%s</b></td><td>%s</td></tr>",
DF.format(new Date(notification.getTimeStamp())),
notification.getType(),
notification.getMessage());
out.println(line);
}
out.println("</table>");
}
}
%>
</body>
</html>
addregistration.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<%@ page contentType="text/html;charset=windows-1252"%>
<%@ page import="sab.demo.jmx.map.*"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252"/>
<title>Index</title>
<STYLE TYPE="text/css">
TD, BODY {font-family: Arial; font-size: 10pt;}
</STYLE>
</head>
<body>
<a href="listnotifications.jsp">Cancel</a>
<h3>Register MBean Listener</h3>
<form action="registrationservlet" method="post">
<p>
<table>
<tr>
<td>MBean Name:</td>
</tr>
<tr>
<td>
<select name="mbean">
<%
ServletContext ctx = config.getServletContext();
GlobalListenerMap glm = (GlobalListenerMap)ctx.getAttribute("GlobalListenerMap");
if(glm==null) System.out.println("glm was null");
for(String mbean: glm.getJ2EEApplicationNameList()) {
%>
<option><%=mbean%></option>
<%
}
%>


</td>
</tr>
<tr>
<td colspan="2" align="right">
<input type="submit"/>
</td>
</tr>
</table>
</p>
</form>
</font>
</body>
</html>

----------------
Listening to: The Stone Roses - She Bangs The Drums