The Java Management eXtensions are really good to dynamically alter the behaviour of your application. The JMX API is standard in Java 5, but if you’re stuck with an old version then you have to build your own or use an existing API. In WAS 5 you can easily integrate with the WebSphere API. This post will describe how to do that. I will do this by an example of how to get a JMX Managed Log4J configuration in WAS 5.
The integration in WAS 5 is done by making an XML description of your managed bean (MBean) and making the implementation extend from com.ibm.websphere.management.RuntimeCollaborator. The implementing class must contain the methods that are defined in the XML.
The XML must be based on the MbeanDescriptor.dtd (attached) and can include MBeans with attributes, operations and notifications.
- An attribute is read-only information that can be accessed through the MBean.
- An operation is callable on the MBean
- A notification is an event that gets thrown by the MBean
The Logging MBean consists of three attributes and one operation:
- Attribute: version (the log4j version)
- Attribute: current loggers (all the current Log4j loggers)
- Attribute: identification (The name of the root logger)
- Operation: reload (Reload the logging configuration file)
The XML of the MBean:
And the implementation:
import com.ibm.websphere.management.RuntimeCollaborator; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.apache.log4j.helpers.OptionConverter; import org.apache.log4j.xml.DOMConfigurator; import java.net.URL; import java.util.Enumeration; public class LoggingCollaborator extends RuntimeCollaborator { private Logger logger; private URL configurationLocation; private static final Logger LOG = Logger.getLogger(LoggingCollaborator.class); public LoggingCollaborator(Logger root, URL configurationLocation) { if (root == null) { throw new IllegalArgumentException("root logger must not be null."); } else { logger = root; } if (configurationLocation == null) { throw new IllegalArgumentException("configurationLocation must not be null."); } else { this.configurationLocation = configurationLocation; } } public String getIdentification() { if (LOG.isDebugEnabled()) LOG.debug("Getting identification " + logger.getName()); return logger.getName(); } public String getCurrentLoggers() { StringBuffer loggers = new StringBuffer(200); Enumeration enum = LogManager.getCurrentLoggers(); boolean first = true; while (enum.hasMoreElements()) { Logger log = (Logger) enum.nextElement(); if (first) first = false; else loggers.append(", "); loggers.append(log.getName()); if (log.getEffectiveLevel() != null) loggers.append("=").append(log.getEffectiveLevel()); } return loggers.toString(); } public String getVersion() { return LogManager.class.getPackage().getImplementationVersion(); } public void reload() throws NamingException { LOG.info("Reloading configuration file through URL: " + configurationLocation “); new DOMConfigurator().doConfigure(configurationLocation, logger.getLoggerRepository()); LOG.info("Configuration URL: " + configurationLocation + " is reloaded"); } }
We now have an MBean description and its implementation. The implementation needs the location of the log4j.xml and the root logger in its constructor to work.
To let WAS know we have a custom MBean we have to register the MBean in WAS. This can be done in multiple ways: the bean can be defined in the ”Extension MBean Providers” section of a WAS server in the Admin Console (see ref 3) and can be automatically started as the server starts. (see ref 3). Because Log4j works (at least for me) on an application basis and not on server basis, I will do it a little different. I will register the MBean programmatically and keep the description XML in the application classpath. (Words of warning: the application provided MBean XML yields for the server defined “Extension MBean Providers” with normal classloading: “PARENT_FIRST”.
To register the MBean I use a ServletContextListener (the complete class implementation is at the end of this blog) that I define in the web.xml of my web application so that whenever my application gets started the MBean will be registered and when my application is stopped, I can de-register the MBean.
private void startJMXLog4j(String applicationName, URL configurationURL) { // Initialize the MBean MBeanFactory factory = AdminServiceFactory.getMBeanFactory(); LoggingCollaborator collab = new LoggingCollaborator(LogManager.getRootLogger(), configurationURL); String loggingMBeanName = applicationName + "LoggingMBean"; Logger logger = Logger.getLogger(Log4JInitializer.class); logger.info("Activating MBean with name [" + loggingMBeanName + "] in class handle [" + getClass().hashCode() + "] for version " + LogManager.class.getPackage().getImplementationVersion()); try { ObjectName foundName = factory.findMBean(loggingMBeanName); logger.info("Name " + loggingMBeanName + " already exists, deactivating it now ...."); factory.deactivateMBean(foundName); if (logger.isDebugEnabled()) { logger.debug("Deactivated MBean with name " + applicationName + "."); } } catch (InstanceNotFoundException e1) { logger.debug("No instance found for name " + applicationName); } catch (AdminException e) { logger.error("Error deactivating bean, will try to add a new one with name " + applicationName, e); } try { ObjectName objName = factory.activateMBean(LOGGING_TYPE, collab, loggingMBeanName, LOGGING_XML_CONFIG); logger.info("Added the mbean to the server, objectName=[" + objName + "]"); } catch (AdminException e) { if (e.getCause() instanceof InstanceAlreadyExistsException) { logger.error("Instance already exists for " + loggingMBeanName + ", removal must have failed."); } logger.error("Exception activating MBean for " + loggingMBeanName, e); e.printStackTrace(); throw new IllegalStateException(e.toString()); } }
And now there is a working MBean extension.
To manipulate the MBean we can use the wsadmin application.
The following JACL commands can be used (these are a subset):
[code]
# Get all the loggers
set loggers [$AdminControl queryNames type=Log4JLogger,*]
# Get the first log
set log [lindex $loggers 0]
# Get all the attibutes of the log
$AdminControl getAttributes $log
# Reload the logging of the log
$AdminControl invoke $log reload {}
[/code]
References:
Complete ServletContextListener class:
import java.net.URL; import javax.management.InstanceAlreadyExistsException; import javax.management.InstanceNotFoundException; import javax.management.ObjectName; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.apache.log4j.helpers.LogLog; import org.apache.log4j.xml.DOMConfigurator; import LoggingCollaborator; import com.ibm.websphere.management.AdminServiceFactory; import com.ibm.websphere.management.MBeanFactory; import com.ibm.websphere.management.exception.AdminException; /** * ServletContextListener initializes the Log4J configuration with an URL from * the JNDI repository and starts a JMX MBean to allow dynamic changes of the * Log4J configuration. */ public class Log4JInitializer implements ServletContextListener { private static final String LOGGING_XML_CONFIG = "LoggingMBean.xml"; private static final String LOGGING_TYPE = "Logger"; /** * Logger name for checking if the logging is already initialized */ private static final String LOGGING_INITIALIZED_KEY = "INITIALIZED"; /** * Application name for this instance */ private String applicationName; /** * Application name extension for this instance */ private static final String LOGGING_MBEAN_NAME_EXTENSION = "LoggingMBean"; /** * Initialize Log4J using a Serice provider for the application name and initialize the mBean * * @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent) */ public void contextInitialized(ServletContextEvent event) { LogLog.debug("Log4JInitializer being initialized"); Logger existingLog = LogManager.exists(LOGGING_INITIALIZED_KEY); if (existingLog != null) { existingLog .info("Logging is already initialized. It will not be initialized again"); return; } try { // Get the application name, there is a change of getting an Exception applicationName = ApplicationName.getApplicationName(); } catch (Throwable e) { // Listeners don't show Stack traces in WAS. Use LogLog LogLog .error( "Log4JInitializer - Failed to get the ApplicationName because of ERROR", e); throw new IllegalStateException( "No ApplicationName provider could be found, could not start logging. Cause: " + e.getMessage()); } LogLog.debug("Log4JInitializer - ApplicationName found is: " + applicationName); if (applicationName == null || applicationName.trim().length() == 0) { String msg = "ApplicationName not found in provider"; LogLog.error("Log4JInitializer - " + msg); throw new IllegalStateException(msg); } // Get the url to the Log4J configuration from JNDI String jndiName = "url/config/" + applicationName + "/log4j"; LogLog.debug("Log4JInitializer - Lookup from jndi name " + jndiName); try { DefaultServiceLocator locator = new DefaultServiceLocator(); // Utility class that encapsulates the JNDI call URL url = locator.getURL(jndiName); LogLog.debug("Log4JInitializer - Found url " + url + ", configuring Log4j now."); DOMConfigurator.configure(url); startJMXLog4j(applicationName, url); } catch (ServiceLocatorException e) { String msg = "Retrieval of URL from jndi " + jndiName + " failed with error: " + e.getLocalizedMessage(); LogLog.error("Log4JInitializer - " + msg, e); throw new IllegalStateException(msg); } // Logging is initialized register the INITIALIZED KEY with Log4j existingLog = Logger.getLogger(LOGGING_INITIALIZED_KEY); existingLog.info("Logging is initialized"); LogLog.debug("Log4JInitializer - Log4J is initialized now"); } /** * Start a Log4J mBean for the current application * * @param applicationName * @param URL configurationURL */ private void startJMXLog4j(String applicationName, URL configurationURL) { // Initialisaze the mBean MBeanFactory factory = AdminServiceFactory.getMBeanFactory(); LoggingCollaborator collab = new LoggingCollaborator(LogManager .getRootLogger(), configurationURL); String loggingMBeanName = applicationName + "LoggingMBean"; Logger logger = Logger.getLogger(Log4JInitializer.class); logger.info("Activating MBean with name [" + loggingMBeanName + "] in class handle [" + getClass().hashCode() + "] for version " + LogManager.class.getPackage().getImplementationVersion()); try { ObjectName foundName = factory.findMBean(loggingMBeanName); logger.info("Name " + loggingMBeanName + " already exists, deactivating it now ...."); factory.deactivateMBean(foundName); if (logger.isDebugEnabled()) logger.debug("Deactivated MBean with name " + applicationName + "."); } catch (InstanceNotFoundException e1) { logger.debug("No instance found for name " + applicationName); } catch (AdminException e) { logger.error( "Error deactivating bean, will try to add a new one with name " + applicationName, e); } try { ObjectName objName = factory.activateMBean(LOGGING_TYPE, collab, loggingMBeanName, LOGGING_XML_CONFIG); logger.info("Added the mbean to the server, objectName=[" + objName + "]"); } catch (AdminException e) { if (e.getCause() instanceof InstanceAlreadyExistsException) { logger.error("Instance already exists for " + loggingMBeanName + ", removal must have failed."); } logger.error("Exception activating MBean for " + loggingMBeanName, e); e.printStackTrace(); throw new IllegalStateException(e.toString()); } } /** * Remove the mBean and stop Log4j if the application is unloaded. * * @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent) */ public void contextDestroyed(ServletContextEvent event) { Logger logger = Logger.getLogger(Log4JInitializer.class); String loggingMBeanName = applicationName + "LoggingMBean"; logger.info("Deactivating MBean with name [" + loggingMBeanName + "] in class handle [" + getClass().hashCode() + "] for version " + LogManager.class.getPackage().getImplementationVersion()); MBeanFactory factory = AdminServiceFactory.getMBeanFactory(); try { ObjectName foundName = factory.findMBean(loggingMBeanName); logger.info("Name " + loggingMBeanName + " exists, deactivating it now ...."); factory.deactivateMBean(foundName); if (logger.isDebugEnabled()) logger.debug("Deactivated MBean with name " + loggingMBeanName + "."); } catch (InstanceNotFoundException e1) { logger.debug("No instance found for name " + loggingMBeanName); } catch (AdminException e) { logger.error("Error deactivating bean, ignoring...", e); } // Safely shutdown Log4J logger.info("Shutting down Log4J..."); LogManager.shutdown(); } }