How to Integrate Custom Services via MBeans

Author:Scott Stark <Scott_Stark@displayscape.com>

Introduction

The best way to add services to the JBoss server is to write your own JMX MBeans. An MBean is a Java object which implement resources and their instrumentation interfaces as defined in the Java Management Extensions(JMX) specification. Once written they are added to JBoss using the jboss.jcml which is used to configure the core JBoss MBeans. The best way for your EJBs to access your new services is to make them accessible via JNDI.

There are two classes of MBeans; those that are independent of JBoss services and those that are dependent on JBoss services. MBeans that are independent of JBoss services are the trival case and can be written per the JMX specification and added to a JBoss server by adding their MLET tag to the jboss.conf file. Writing an MBean that relies on a JBoss service like naming requires one to follow the JBoss service pattern. Before describing how to write such an MBean, let's go over the two JBoss services that manage the configuration and depdendencies of MBean services; ConfigurationService and ServiceControl.

The ConfigurationService MBean

JBoss manages configuration its MBean services via a custom MBean that loads an xml variation of the standard MLet configuration file. This custom MBean is implemented in the org.jboss.configuration.ConfigurationService class. The ConfigurationService MBean is loaded when JBoss starts up by the JMX MLet due to its entry in the jboss.conf file. The jboss.jcml configuration is then loaded by invoking loadConfiguration() on the ConfigurationService MBean. The loadConfiguration method performs the following steps:

  1. Parse the jboss.jcml file and instantiate all MBeans

  2. Parse the optional jboss-auto.jcml

    1. If jboss-auto.jcml exists, parse the file and instantiate any MBeans that were not created by the parse of the jboss.jcml file.

    2. Apply the MBean attribute settings from the jboss-auto.jcml file.

  3. Apply the attribute settings from the jboss.jcml file.

The ServiceControl MBean

JBoss manages dependencies between MBeans via the org.jboss.util.ServiceControl custom MBean. . The ServiceControl MBean is also loaded by the JMX MLet on JBoss startup. It implements the javax.management.NotificationListener interface to receive AttributeChangeNotification, MBeanServerNotification.REGISTRATION_NOTIFICATION and MBeanServerNotification.UNREGISTRATION_NOTIFICATION JMX events. AttributeChangeNotification are simply logged to the JBoss server log. The REGISTRATION_NOTIFICATION and UNREGISTRATION_NOTIFICATION are used to determine which MBeans are candidate JBoss service MBeans. The order of MBean registration is used as the order of service initialization and startup. The ServiceControl MBean has four key methods: init, start, stop and destroy.

The init Method

The ServiceControl init method is called by the JBoss server main entry point after the jboss.jcml configuration has been loaded. The process of loading the jboss.jcml file will have caused the names of all loaded MBeans to have been registered with the ServiceControl MBean due to the registration events posted by the MBeanServer. The init method makes a copy of curent list of MBean names and then proceeds to invoke the init method on each MBean. If successful, the ServiceControl MBean then registers as an NotificationListener on the MBean's name to receive AttributeChangeNotification from the MBean. The order of initialization is the order of registration which is the same as the ordering of mbean entries in the jboss.jcml file. When an MBean's init method is called, all MBeans that were registered ahead of it have also had their init method invoked. This gives an MBean an opportunity to check that required MBeans or resources exist. The MBean typically cannot utilize other MBean services at this point as most JBoss MBean services do not become fully functional until they have been started.

The start Method

The ServiceControl start method is called by the JBoss server main entry point after all MBeans have been initd. The start method makes a copy of curent list of MBean names and then proceeds to invoke the start method on each MBean. When an MBean's start method is called, all MBeans that were registered ahead of it have also had their start method invoked. It is within the start method that signals an MBean service to become fully operational.

The stop Method

The stop method is invoked by the by the JBoss server shutdown process which is managed by the org.jboss.util.Shutdown MBean. The stop method makes a copy of the current list of MBean names and then invokes the stop method on each MBean in reverse order.

The destroy Method

The destroy method is invoked by the by the JBoss server shutdown process after the stop method. The destroy method makes a copy of the current list of MBean names and then invokes the destroy method on each MBean in reverse order.

Writing JBoss MBean Services

Writing a custom service that integrates into the JBoss server requires the use of the org.jboss.util.Service interface pattern if the custom service is depdendent on other JBoss services. This means that you cannot perform any JBoss service dependent intialization in any of the javax.management.MBeanRegistration interface methods. Instead, you must do this in the Service interface init and/or start methods. You need to do one of the following:

  • Add any of the Service methods that you want called on your MBean to your MBean interface.

  • Have your MBean interface extend the org.jboss.util.Service interface.

  • Have your MBean interface extend the org.jboss.util.ServiceMBean interface. This is a subinterface of org.jboss.util.Service that adds String getName(), int getState(), and String getStateString() methods.

Which approach you choose depends on whether or not you want to be associated with JBoss specific code. If you don't, then you would use the first approach. If you don't mind, then the simplest approach is to have your MBean interface extend from org.jboss.util.ServiceMBean and your MBean implementation class extend from the abstract org.jboss.util.ServiceMBeanSupport class. This class implements the org.jboss.util.ServiceMBean interface except for the String getName() method. ServiceMBeanSupport provides implementations of the init, start, stop and destroy methods that integrate logging and JBoss service state management. Each method delegates any subclass specific work to initService, startService, stopService, and destroyService methods respectively. When subclassing ServiceMBeanSupport you would override one or more of the initService, startService, stopService, and destroyService methods in addition to getName as required.

Examples

This section uses a simple MBean that binds a HashMap into the JBoss JNDI namespace at a location determined by its JndiName attribute. Because the MBean uses JNDI it depdends on the JBoss naming service MBean and therefore must use the JBoss MBean service pattern to be notified of when the naming service is available.

JNDIMap MBean That Implements Service Methods

Figure 11.8. gives the MBean interface for the JNDIMap MBean that incorporates that Service interface methods that it needs to startup correctly. The interface includes the Service start method to be informed when all required services have been started and the stop method to clean up the service.

Figure 11.8. JNDIMap MBean Interface and Implementation Based On the Service Interface

// The JNDIMap MBean interface
import javax.naming.NamingException;

public interface JNDIMapMBean
{
    public String getJndiName();
    public void setJndiName(String jndiName) throws NamingException;
    public void start() throws Exception;
    public void stop() throws Exception;
}
// The JNDIMap MBean implementation
import java.io.InputStream;
import java.util.HashMap;
import javax.naming.CompositeName;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Name;
import javax.naming.NamingException;
import org.jboss.naming.NonSerializableFactory;

public class JNDIMap implements JNDIMapMBean
{
    private String jndiName;
    private HashMap contextMap = new HashMap();
    private boolean started;

    public String getJndiName()
    {
       return jndiName;
    }
    public void setJndiName(String jndiName) throws NamingException
    {
        String oldName = this.jndiName;
        this.jndiName = jndiName;
        if( started )
        {
            unbind(oldName);
            try
            {
                rebind();
            }
            catch(Exception e)
            {
                NamingException ne = new NamingException("Failed to update jndiName");
                ne.setRootCause(e);
                throw ne;
            }
        }
    }

    public void start() throws Exception
    {
        started = true;
        rebind();
    }

    public void stop()
    {
        started = false;
        unbind(jndiName);
    }

    private static Context createContext(Context rootContext, Name name) throws NamingException
    {
        Context subctx = rootContext;
        for(int n = 0; n < name.size(); n ++)
        {
            String atom = name.get(n);
            try
            {
                Object obj = subctx.lookup(atom);
                subctx = (Context) obj;
            }
            catch(NamingException e)
            {	// No binding exists, create a subcontext
                subctx = subctx.createSubcontext(atom);
            }
        }

        return subctx;
    }

    private void rebind() throws NamingException
    {
        InitialContext rootCtx = new InitialContext();
        // Get the parent context into which we are to bind
        Name fullName = rootCtx.getNameParser("").parse(jndiName);
        System.out.println("fullName="+fullName);
        Name parentName = fullName;
        if( fullName.size() > 1 )
            parentName = fullName.getPrefix(fullName.size()-1);
        else
            parentName = new CompositeName();
        Context parentCtx = createContext(rootCtx, parentName);
        Name atomName = fullName.getSuffix(fullName.size()-1);
        String atom = atomName.get(0);
        NonSerializableFactory.rebind(parentCtx, atom, contextMap);
    }
    private void unbind(String jndiName)
    {
        try
        {
            Context rootCtx = (Context) new InitialContext();
            rootCtx.unbind(jndiName);
            NonSerializableFactory.unbind(jndiName);
        }
        catch(NamingException e)
        {
            e.printStackTrace();
        }
    }
}

JNDIMap MBean That Extends ServiceMBean

Figure 11.9. gives the MBean interface for the JNDIMap MBean that extends th ServiceMBean interface. The implementation class then extends the ServiceMBeanSupport class and overrides the startService method to be informed when all required services have been started and the stopService method to clean up the service. It also implements the abstract getName to return a descriptive name for the MBean.

Figure 11.9. JNDIMap MBean Interface and Implementation Based On the ServiceMBean Interface and ServiceMBeanSupport Class

// The JNDIMap MBean interface
import javax.naming.NamingException;

public interface JNDIMapMBean extends org.jboss.util.ServiceMBean
{
    public String getJndiName();
    public void setJndiName(String jndiName) throws NamingException;
}
// The JNDIMap MBean implementation
import java.io.InputStream;
import java.util.HashMap;
import javax.naming.CompositeName;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Name;
import javax.naming.NamingException;
import org.jboss.naming.NonSerializableFactory;

public class JNDIMap extends org.jboss.util.ServiceMBeanSupport implements JNDIMapMBean
{
    private String jndiName;
    private HashMap contextMap = new HashMap();

    public String getJndiName()
    {
       return jndiName;
    }
    public void setJndiName(String jndiName) throws NamingException
    {
        String oldName = this.jndiName;
        this.jndiName = jndiName;
        if( super.getState() == STARTED )
        {
            unbind(oldName);
            try
            {
                rebind();
            }
            catch(Exception e)
            {
                NamingException ne = new NamingException("Failed to update jndiName");
                ne.setRootCause(e);
                throw ne;
            }
        }
    }

    public String getName()
    {
        return "JNDIMap(" + jndiName + ")";
    }

    public void startService() throws Exception
    {
        rebind();
    }

    public void stopService()
    {
        unbind(jndiName);
    }

    private static Context createContext(Context rootContext, Name name) throws NamingException
    {
        Context subctx = rootContext;
        for(int n = 0; n < name.size(); n ++)
        {
            String atom = name.get(n);
            try
            {
                Object obj = subctx.lookup(atom);
                subctx = (Context) obj;
            }
            catch(NamingException e)
            {	// No binding exists, create a subcontext
                subctx = subctx.createSubcontext(atom);
            }
        }

        return subctx;
    }

    private void rebind() throws NamingException
    {
        InitialContext rootCtx = new InitialContext();
        // Get the parent context into which we are to bind
        Name fullName = rootCtx.getNameParser("").parse(jndiName);
        log.debug("fullName="+fullName);
        Name parentName = fullName;
        if( fullName.size() > 1 )
            parentName = fullName.getPrefix(fullName.size()-1);
        else
            parentName = new CompositeName();
        Context parentCtx = createContext(rootCtx, parentName);
        Name atomName = fullName.getSuffix(fullName.size()-1);
        String atom = atomName.get(0);
        NonSerializableFactory.rebind(parentCtx, atom, contextMap);
    }
    private void unbind(String jndiName)
    {
        try
        {
            Context rootCtx = (Context) new InitialContext();
            rootCtx.unbind(jndiName);
            NonSerializableFactory.unbind(jndiName);
        }
        catch(NamingException e)
        {
            log.exception(e);
        }
    }
}

Sample jboss.jcml mbean entry

A sample jboss.jcml mbean entry for the JNDIMap MBean is given in Figure 11.10.. This binds a HashMap object under the "inmemory/maps/MapTest" JNDI name.

Figure 11.10. Sample jboss.jcml entry for the JNDIMap MBean

<server>
...
  <mbean code="org.jboss.naming.NamingService" name="DefaultDomain:service=Naming">
    <attribute name="Port">1099</attribute>
  </mbean>

  <!-- Add the JNDIMap entry after the NamingService since the NamingService must
	be running in order for the JNDIMap bean to start.
  -->
  <mbean code="JNDIMap" name="DefaultDomain:service=JNDIMap,jndiName=inmemory/maps/MapTest">
    <attribute name="JndiName">inmemory/maps/MapTest</attribute>
  </mbean>
...
</server>
				
				
// Sample lookup code
InitialContext ctx = new InitialContext();
HashMap map = (HashMap) ctx.lookup("inmemory/maps/MapTest");