Auction Example Horizontal Services

   

Building the Auction Logging Service

We are going to build a logging service for the auction site that supports the capability for both EJB and non-EJB components to use the same logging APIs to log messages. Each container will log messages to a location specified by the logging configuration settings. This might be a local file system or to a central location.

We will follow the architecture for other Java APIs in that we will have a set of APIs mostly made up of interfaces, and then we will build a logging Service Provider Interface (SPI) that will translate the logging API calls to a specific logging implementation. By taking this approach, we can build different implementations and plug them in as our requirements change. We could also build an SPI that plugs into a third-party logging service.

Developing the Logging Architecture

Obviously, there are many steps in arriving at a system or application architecture design. By performing requirements gathering, building use-cases , and understanding both functional and nonfunctional requirements, you can arrive at a high-level picture of what the architecture should look like. We are not going to discuss how to perform analysis and design and arrive at a proper design of the architecture. You'll just have to take our word for it that we have done it for our auction example. There are many books on doing analysis and design. We suggest you buy one of these if you have never done it. Some say that doing design is more of an art form than a science, but that's probably a stretch.

We will, however, list the basic requirements that we have placed on the logging system:

  • It must work for both the Web tier and the EJB tier .

  • Multiple logging implementations should be supported so that the Web tier could log locally or in a distributed manner. The client code should not be affected if the logging implementation changes.

  • It's recommended that a naming service be used to locate the logging facilities, because it is a resource like JMS or JDBC. The logging implementation should handle the details of locating the logging service through the naming service, not the clients .

  • The Web tier might or might not use the naming service, depending on whether it's logging locally or to a remote service. This is not known by the clients and is handled by the logging implementation entirely.

  • The logging implementation should use resource files to control which specific logging implementation is used and to load environment properties for the logging service.

  • The clients that use the logging service should always use a single logging interface, both in the Web container and in the EJB container.

After studying the three requirements for the logging service, we arrived at the logging architecture shown in Figure 21.2.

Figure 21.2. This logging architecture will be used for both the Web tier and application tier.

graphics/21fig02.gif

We could have come up with several designs that all satisfied the logging requirements. There is hardly ever a single correct design. Although some are better than others, there's usually not just a single design that will fit the solution.

You should probably be warned that the architecture that was chosen to achieve this logging functionality might seem overly complicated at first, especially if you are not familiar with how Java separates the API, SPI, and the implementation. The design of the architecture is similar to the JNDI architecture, so if you were interested in how the JNDI SPI works, this will give you a good understanding of it. We are going to walk through all these interfaces and classes and explain to you what the purpose is of each. At the end, we will test them by executing an example of the logging horizontal service.

Creating the Logging API

We need to develop the logging APIs first. These are the classes and interfaces that the clients will be using to log messages. Not only is everything dependent on these, doing these first would also allow client components to partially build in the logging interface methods early and allow for parallel development.

The first API component to develop is the main logging interface. This is the one that will be used by the client components that need to log messages. We will not let the client components determine where the log messages go and which logging implementation is going to be used; that's the job for the application deployer, assembler, and administrator. Together, they will use the deployment files and the logging configuration properties to establish the particular logging implementation used by the clients.

The clients will only be allowed to log a message and inform the service which type of log message it is. If the message type is a warning or error, an exception can also be passed and the details of the exception will get logged as well. The first class we need to develop is the class that encapsulates the log message. Listing 21.2 shows the LogMessage class.

Listing 21.2 The LogMessage Class Used to Encapsulate a Message Logged from a Client
 /**   * Title:        LogMessage<p>   * Description:  A wrapper for the information about a particular log message.   */  package com.que.ejb20.services.logging;  import java.sql.Timestamp;  import java.text.DateFormat;  import java.text.SimpleDateFormat;  import java.io.PrintWriter;  import java.io.StringWriter;  public class LogMessage implements java.io.Serializable {   // Log Constants    public static final int ERROR = 4;    public static final int WARNING = 3;    public static final int INFO = 2;    public static final int DEBUG = 1;    private String message = null;    private String subsystem = null;    private int severity;    private Exception exception = null;    private Timestamp timestamp = null;    // Used to help format timestamp's    private static DateFormat timestampFormat =      new SimpleDateFormat( "MMM dd, yyyy hh:mm:ss:SSSS a z" );    public LogMessage( String msg, int severity ) {     super();      message = msg;      this.severity = severity;      timestamp = new Timestamp( System.currentTimeMillis() );    }    public LogMessage( String msg, int severity, Exception ex ) {     this( msg, severity );      exception = ex;    }    public String getTimestamp(){     return timestampFormat.format( timestamp );    }    public String getMessage() {     return message;    }    public void setMessage(String newMessage) {     message = newMessage;    }    public String getSubsystem() {     return subsystem;    }    public void setSubsystem(String system) {     this.subsystem = system;    }    public void setSeverity( int severity ) {     this.severity = severity;    }    public int getSeverity() {     return severity;    }    public void setException(Exception newException) {     exception = newException;    }    public Exception getException() {     return exception;    }    // Convenience method to help print out a user-friendly string that    // represents the log message.    private String getMsgString(){     StringBuffer buf = new StringBuffer();      buf.append( "<" );      buf.append( getTimestamp() );      buf.append( "> " );      buf.append( "<" );      buf.append( getSeverityString() );      buf.append( "> " );      buf.append ("\n" );      buf.append( "<" );      if ( getSubsystem() != null ){       buf.append( getSubsystem() );        buf.append( "> " );        buf.append ("\n" );        buf.append( "<" );      }      buf.append( getMessage() );      buf.append( ">" );      if ( getException() != null ) {       StringWriter strWriter = new StringWriter();        PrintWriter writer = new PrintWriter( strWriter );        getException().printStackTrace( writer );        String exStr = writer.toString();        buf.append( strWriter.toString() );      }      return buf.toString();    }    // Override the default toString method    public String toString() {     return getMsgString();    }    // Since the severity is an int, this method converts it to the    // appropriate string for display purposes.    public String getSeverityString(){     switch( getSeverity() ) {       case 1:          return "DEBUG";        case 2:          return "INFO";        case 3:          return "WARNING";        case 4:          return "ERROR";        default : return "Unknown";      }    }  } 

Although the LogMessage class seems sort of complex, most of the work has to do with formatting the message string. A client uses the LogMessage class by calling one of the two constructor methods and passing the required parameters. The following code fragment illustrates how a client might create a new instance of the class:

 LogMessage msg = new LogMessage( "This is a test warning",      LogMessage.WARNING ); 

The LogMessage class declares four static constants that are used to declare the severity of the message. Remember that our logging architecture must support both local and remote logging. Because the log messages might have to be sent across the network, the LogMessage class must implement the Serializable interface.

Now that we've determined what the log message is going to look like, we need to decide on the API that the client will use to log a message. We will use a Java interface for the client so that we can plug in different implementations at startup. If we restrict the client to using the interface, this gives us the freedom to have multiple implementations that use the interface and perform different actions with the log messages. Listing 21.3 shows the interface that will be used by all components that need to log a message.

Listing 21.3 The Interface Used to Log a Message
 /**   * Title:        ILogger<p>   * Description:  The logging interface used by all clients wishing to log   *               messages to the horizontal logging service.<p>   */  package com.que.ejb20.services.logging;  public interface ILogger {   public void logMessage( LogMessage msg ) throws LoggingException;    public void close() throws LoggingException;  } 

The ILogger interface is a pretty straightforward Java interface for the clients to use to log messages. The LogMessage class determines what the output of the message looks like, so really only one log method is needed. We probably could extend the LogMessage class if we wanted the output to look differently.

If we want, we possibly could extend this interface to support additional functionality later if necessary. The logMessage method in the ILogger interface can throw a LoggingException . This exception is raised when something bad occurs during an attempt to log a message. You'll see the details of this class shortly. There is also a close method that the clients can call to release any resources the ILogger instance is holding. For log messages that are sent directly to system out, there might be no resources and the method would just be a no-op. However, with a remote logging implementation, the close method may close a JMS session.

Just like the InitialContext class in the JNDI APIs, the clients need a logging class that implements the ILogger interface, which will provide the implementation to use for logging messages. Listing 21.4 shows the Logger class that a client must instantiate and use to log messages.

Listing 21.4 The Logger Class that Clients Create and Use to Log Messages
 /**   * Title:        Logger<p>   * Description:  The logging class that should be instantiated by the client   *               to log messages. The underlying implementation is determined   *               by the environment properties in the logging resource file.   */  package com.que.ejb20.services.logging;  import java.util.Hashtable;  import java.util.Properties;  import com.que.ejb20.services.logging.spi.LogManager;  public class Logger implements ILogger {   private Hashtable environmentProps = null;    private ILogger defaultLogger = null;    private boolean gotDefaultLogger = false;    // Default Constructor    public Logger() throws LoggingException {     environmentProps = null;      defaultLogger = null;      init();    }    // Read the logging properties and instantiate a default logger    // based on the configuration properties in the logging properties file    private void init() throws LoggingException {     if (!gotDefaultLogger){       Properties props = new Properties();        try{         props.load( getClass().getResourceAsStream( "/logging.properties" ) );        }catch( Exception ex ){         ex.printStackTrace();          throw new LoggingException( "Can't find the logging.properties file" );        }        this.environmentProps = props;        getDefaultLogger();      }    }    public ILogger getDefaultLogger() throws LoggingException {     if(!gotDefaultLogger) {       // Delegate the building of the logger to the SPI level        defaultLogger = LogManager.getInitialLogger( environmentProps );        if ( defaultLogger != null )          gotDefaultLogger = true;      }      // Make sure there is a default logger      if(defaultLogger == null)        throw new NoInitialLoggingException( "Could not create the logger" );      else        return defaultLogger;    }    // implement the neccessary interface methods    public void logMessage( LogMessage msg ) throws LoggingException {     defaultLogger.logMessage( msg );    }    public void close() throws LoggingException {     this.defaultLogger.close();    }  } 

When a client needs to log messages, it would create a new instance of the Logger class or reuse an existing instance and call the logMessage method. The following code fragment illustrates an example of this:

 ILogger logger = new Logger();  LogMessage msg = new LogMessage( "This is a test", LogMessage.WARNING );  logger.logMessage( msg );  logger.close(); 

Notice that the client does not have to specify anything about the type of logger or where the log messages are going. That again is determined by the environment settings. All the client must do is create an instance of the Logger class and pass an instance of a LogMessage to it.

The final two classes that are part of the logging API are the exceptions classes. Two exceptions can be thrown. The first is a general logging exception called LoggingException . This exception is raised when anything goes wrong with the client's normal use of the logging horizontal service. Listing 21.5 shows the LoggingException class.

Listing 21.5 The LoggingException Class Thrown When a Problem Occurs During an Attempt to Send a Log Message
 /**   * Title:        LoggingException<p>   * Description:  A exception for the logging horizontal service.<p>   */  package com.que.ejb20.services.logging;  public class LoggingException extends Exception {   public LoggingException( String msg ) {     super( msg );    }  } 

The other exception class is thrown only during the initial creation of the Logger class. If for one reason or another a logger can't be created, the NoInitialLoggerException will be thrown. Listing 21.6 shows this exception.

Listing 21.6 The Exception That Is Thrown When a Problem Occurs During the Initial Creation of the Logger
 /**   * Title:        NoInitialLoggingException<p>   * Description:  An exception that is thrown when the logging system can't   *               create the initial logger for the client.<p>  */  package com.que.ejb20.services.logging;  public class NoInitialLoggingException extends LoggingException {   public NoInitialLoggingException( String msg ) {     super( msg );    }  } 

This exception normally indicates that the logging configuration has not been correctly set up or that no logging implementation has been configured.

That's the entire set of classes and interfaces that are exposed to the client components for the logging API. The next section covers the classes and interfaces that make up the service provider interface (SPI) for the logging service. These classes and interfaces are not used directly by the clients, but get invoked by the logging infrastructure. They couple the logging APIs to actual logging implementations.

Creating the Logging SPI

The next step in our process is to create the SPI for our logging service. We described what an SPI is in Chapter 4, "Java Naming and Directory Interface." In case you have forgotten, the logging SPI enables us to support multiple logging implementations without having to modify the client code and only modify the environment properties to switch logging implementations. It does this by looking at the environment properties and determining which runtime logging implementation should be created. All this is done without the knowledge of the client components. All they care about is that they get an instance of the ILogger interface and can send messages to it.

The first class that we need to build is the LogManager class. The LogManager is responsible for creating the logging implementation factory. The Logger class from Listing 21.4 calls the getInitialLogger method on the LogManager class, which in turn creates a logger factory that is specific to the implementation being used. Listing 21.7 shows the LogManager class.

Listing 21.7 The LogManager Class, Which Is Responsible for Creating the Factory for the Logging Implementation
 /**   * Title:        LogManager<p>   * Description:  The class that is responsible for creating the initial logger   *               factory.   */   package com.que.ejb20.services.logging.spi;  import java.util.Hashtable;  import com.que.ejb20.services.logging.*;  public class LogManager {   private static InitialLoggerFactory factory = null;    // Default Constructor    public LogManager() {     super();    }    // Static method called when a client needs a new logger    public static ILogger getInitialLogger( Hashtable props )      throws LoggingException {     // If the logger factory was not built, build it      if( getInitialLoggerFactory() == null ){       String factoryName = null;        if ( props != null ){         factoryName = (String)props.get("java.logging.factory.initial");        }        if( factoryName == null){         String msg = "Need to specify logging factory name in properties";          throw new NoInitialLoggingException( msg );        }        // Try to instantiate the factory class directly        try{         Class factoryClass = Class.forName( factoryName );          // Assign the instance to the class instance          factory = (InitialLoggerFactory)factoryClass.newInstance();        }catch( Exception ex ){         String msg = "Cannot instantiate class: " + factoryName;          throw new NoInitialLoggingException( ex.toString() );        }      }      // Return an instance of the initial logger      return getInitialLoggerFactory().getInitialLogger( props );    }    private static InitialLoggerFactory getInitialLoggerFactory(){     return factory;    }  } 

The LogManager class in Listing 21.7 is doing several things, but basically it is looking at the environment properties and determining which logging implementation should be created. It does this by looking for a resource file called logging.properties and examining the environment properties for a property named java.logging.factory.initial . This property should specify a fully qualified logging factory class. If this property can't be found or the logging factory can't be instantiated for some reason, a NoInitialLoggingException is thrown. After an initial logger factory is found and instantiated, the factory is asked to return an initial logger back to the client.

The other interface in the SPI package is the InitialLoggerFactory . It's implemented by the various logging implementations that can be plugged into this architecture. The InitialLoggerFactory interface appears in Listing 21.8.

Listing 21.8 The Java Interface That All Logging Factories Must Implement
 /**   * Title:        InitialLoggerFactory<p>   * Description:  The interface that all logging factories must implement.<p>   */  package com.que.ejb20.services.logging.spi;  import java.util.Hashtable;  import com.que.ejb20.services.logging.ILogger;  import com.que.ejb20.services.logging.LoggingException;  public interface InitialLoggerFactory {   public ILogger getInitialLogger( Hashtable hashtable )          throws LoggingException;  } 

The getInitialLogger method returns an instance of the ILogger interface that a client can use to send log messages. All logging factory implementations must implement this interface.

Building Two Logging Service Implementations

Finally, we now need to build an implementation for the logging service. You'll see two different logging implementations. One will be used to log messages locally to the system console and the other to send the messages remotely using JMS. Most servlet engines redirect standard out to a file on the Web tier file system. The local logging implementation might be used in the Web container to send messages to wherever standard out is getting redirected.

The remote logging implementation could be used in the EJB container. If, for example, you had multiple EJB containers running in a cluster, you would probably want all the messages going to the same location. If each container were running on a different physical box, a local implementation would not work because each container would send the log messages to a location on its own file system. If we send all the messages to a remote service, then that service is totally responsible for storing the messages from all clients into a single location. You could also use the remote logging implementation in the Web tier if you wanted log messages to go to the same location as the EJB container.

We will walk through the local implementation first, including running an example of it. You'll then see how easy it is to change to the remote implementation without changing the client or any of the existing code.

Before you see the source code for the local implementation, we need to show the resource file that is used by the logging service to determine which logging implementation to instantiate. The resource file is called logging.properties . This is similar to the jndi.properties for JNDI. In fact, the properties file contains key=value pairs very similar to the naming service properties, but modified slightly for our logging needs. The following fragment illustrates the logging.properties file:

 java.logging.factory.initial=            com.que.ejb20.services.logging.local.ConsoleInitialLoggerFactory 

To specify which logging implementation that the system will be using, the java.logging.factory.initial must specify a fully qualified class that implements the InitialLoggerFactory interface from Listing 21.8.

Each logging implementation might require different properties for the environment. For logging to the console, only the initial factory class property must be specified, but if we were using a remote implementation, more properties might have to be specified. We'll talk about those properties when we show a remote logging implementation later in this chapter.

This resource file should be somewhere in the system classpath so that it can be picked up by the JVM. The implementation for local logging appears in Listing 21.9.

Listing 21.9 The Initial Logging Factory Class for Logging to the Console
 /**   * Title:        ConsoleInitialLoggerFactory<p>   * Description:  The factory responsible for creating instances of the ILogger   *               interface that log messages to the console.<p>   */  package com.que.ejb20.services.logging.local;  import java.util.Hashtable;  import com.que.ejb20.services.logging.ILogger;  import com.que.ejb20.services.logging.LoggingException;  import com.que.ejb20.services.logging.spi.InitialLoggerFactory;  public class ConsoleInitialLoggerFactory implements InitialLoggerFactory {   // Default Constructor    public ConsoleInitialLoggerFactory() {     super();    }    // Create a new instance of the ConsoleLogger    public ILogger getInitialLogger( Hashtable hashtable )      throws LoggingException {     return new ConsoleLogger();    }  } 

The ConsoleInitialLoggerFactory in Listing 21.9 just creates an instance of the ConsolerLogger . This class provides the implementation of the ILogger interface by sending the log messages to standard out. The ConsoleLogger appears in Listing 21.10.

Listing 21.10 The Implementation of the ILogger Interface That Logs Messages to Standard Out or Wherever It Has Been Redirected
 /**   * Title:        ConsoleLogger<p>   * Description:  The implementation of the ILogger interface that logs   *               messages to standard out or wherever it's redirected to.<p>   */  package com.que.ejb20.services.logging.local;  import java.io.*;  import java.text.DateFormat;  import java.util.Locale;  import java.sql.Timestamp;  import java.text.SimpleDateFormat;  import com.que.ejb20.services.logging.ILogger;  import com.que.ejb20.services.logging.LogMessage;  import com.que.ejb20.services.logging.LoggingException;  public class ConsoleLogger implements ILogger {   public ConsoleLogger() {     super();    }    public void logMessage( LogMessage msg ) throws LoggingException {     printMsgToConsole( msg );    }    public void close() throws LoggingException {     // No op for this implementation    }    private void printMsgToConsole( LogMessage msg ){     System.out.println( msg.toString() );    }  } 

There's nothing that special about the ConsoleLogger class in Listing 21.10. Because it implements the ILogger interface, it must provide the logMessage method in that interface. For each different type of log message, it just calls the toString method on the LogMessage class.

To test the local logging implementation, you simply need to compile the API classes, SPI classes, and the local implementation classes and ensure that they are all in your classpath and in the proper directories. You will also need to ensure that the logging.properties is in your classpath. The Logger class from Listing 21.4 must be able to find this file and load the logging environment properties.

To help test the local logging, you can use the LoggerTest class shown in Listing 21.11.

Listing 21.11 Test Class Used to Test the Local Logging Implementation
 import com.que.ejb20.services.logging.*;  public class LoggerTest {   // Default Constructor    public LoggerTest() {     super();    }    public static void main(String[] args) {     ILogger logger = null;      try{       // Create an instance of the logger        logger = new Logger();        // Create a warning message        LogMessage msg = new LogMessage( "This is a test warning",             LogMessage.WARNING );        logger.logMessage( msg );        // Always close open resources        logger.close();      }catch( LoggingException ex ){       ex.printStackTrace();      }    }  } 

To run this class from the command line, just type

 java LoggerTest 
graphics/01icon01.gif

You must have all the compiled logging classes in your classpath. If you are having trouble running the local logging example, see " Compiling and Running the Local Logging Service ," in the " Troubleshooting " section at the end of this chapter.

The output should be similar to this:

 C:\ejb20book\ejb20book\classes>java LoggerTest  <Apr 10, 2001 07:01:10:0000 AM EDT> <WARNING>  <This is a test warning>  C:\ejb20book\ejb20book\classes> 

Now you'll see how easy it is to add a second implementation. As we mentioned earlier, a remote implementation would probably be used in an EJB container. However, it could also be used in a Web container if you wanted the Web container log messages and the EJB container messages to go to the same location.

For this example, the client would use the same APIs from Listings 21.1 through 21.5. In fact, we'll also use the same test class from Listing 21.11 without any changes to test the remote implementation when we are finished.

The first step is to build the required initial logger factory for the remote implementation that implements the InitialLoggerFactory interface from the SPI package. This factory will be responsible for creating the instances of the remote logger that implement the ILogger interface. For remote logging implementation, this class is called RemoteInitialLoggerFactory and it's shown in Listing 21.12.

Listing 21.12 The Factory That Is Used to Create Instances of the Remote Logger
 /**   * Title:        RemoteInitialLoggerFactory<p>   * Description:  The factory that is responsible for creating instances   *               of the remote logger.<p>   */  package com.que.ejb20.services.logging.remote;  import java.util.Hashtable;  import java.util.StringTokenizer;  import com.que.ejb20.services.logging.ILogger;  import com.que.ejb20.services.logging.LoggingException;  import com.que.ejb20.services.logging.spi.InitialLoggerFactory;  public class RemoteInitialLoggerFactory implements InitialLoggerFactory {   // Default constructor    public RemoteInitialLoggerFactory() {     super();    }    // Method required by the InitialLoggerFactory interface    public ILogger getInitialLogger( Hashtable props )      throws LoggingException {     // get the url for the provider      String providerUrl = (String)props.get( "java.logging.provider.url" );      if ( providerUrl == null ){       throw new LoggingException("Could not find provider url" );      }      // Figure out the protocol. For now, we are only supporting jms,      // but could later have rmi, ejb, etc...      StringTokenizer tokenizer = new StringTokenizer( providerUrl, ":" );      String protocol = tokenizer.nextToken();      if ( protocol.equalsIgnoreCase("jms") ){       // Since it's jms, get the connection factory and the destination names        String connFactory = tokenizer.nextToken();        String dest = tokenizer.nextToken();        try{         // Create an instance of the delegate that handles the JMS support          // and wrap it with a RemoteLogger          return new RemoteLogger( new JMSLoggerDelegate( connFactory, dest ));        }catch( Exception ex ){         ex.printStackTrace();          throw new LoggingException( "Failure creating the remote logger" );        }      }else{       throw new LoggingException("Unknown protocol in provider url" );      }    }  } 

The RemoteInitialLoggerFactory gets the java.logging.provider.url from the logging.properties resource file and parses the string to determine which remote protocol is specified in the property. For now, only the JMS protocol is supported, but you might also build a RMI protocol or even an EJB protocol. Because we are communicating remotely, we must use some type of remote protocol; that's what this value represents.

For this implementation, because the protocol is JMS, we create a delegate that knows how to communicate using JMS and wrap an instance of a RemoteLogger around the delegate instance. If we were going to be using RMI, then we might have an RMILoggerDelegate instead. The JMSLoggerDelegate class appears in Listing 21.13 and the RemoteLogger appears in Listing 21.14.

Listing 21.13 The Delegate Class Used When the Protocol for Remote Logging Is JMS
 /**   * Title:        JMSLoggerDelegate<p>   * Description:  The delegate class used when the remote logging protocol is   *               JMS.<p>   */  package com.que.ejb20.services.logging.remote;  import javax.naming.*;  import javax.jms.*;  import com.que.ejb20.services.logging.ILogger;  import com.que.ejb20.services.logging.LogMessage;  import com.que.ejb20.services.logging.LoggingException;  public class JMSLoggerDelegate implements ILogger{   // JMS Administrative object references    private TopicSession session = null;    private TopicPublisher publisher = null;    // Default Constructor    public JMSLoggerDelegate( String connFactoryName, String destinationName )      throws NamingException, JMSException {       super();        try{         // Create all of the necessary JMS objects          TopicConnectionFactory connFactory = null;          Topic destination = null;          TopicConnection connection = null;          // We must use JNDI to locate the jms objects          Context ctx = new InitialContext();          connFactory = (TopicConnectionFactory)ctx.lookup( connFactoryName );          destination = (Topic)ctx.lookup( destinationName );          connection = connFactory.createTopicConnection();          // get a session and publisher that will be used for the          // lifecycle of a logger          session = connection.createTopicSession( false,                  Session.AUTO_ACKNOWLEDGE );          publisher = session.createPublisher( destination );        }catch( NamingException ex ){         ex.printStackTrace();        }    }    // Clients can close their logging    public void close() throws LoggingException {     try{       session.close();      }catch( Exception ex ){       ex.printStackTrace();          throw new LoggingException( "Problem closing logger" );      }    }    // The method that actually publishes a remote message    public void logMessage( LogMessage msg ) throws LoggingException {     try{       Message jmsMsg = session.createObjectMessage( msg );        publisher.publish( jmsMsg );      }catch( Exception ex ){       ex.printStackTrace();        throw new LoggingException( "Logging failure" );      }    }  } 

The JMSLoggerDelegate acts as a remote proxy. It handles all the responsibilities of connecting and communicating with the JMS service. When a client sends an instance of a LogMessage to the RemoteLogger , the message gets forwarded to the delegate. In this case, the RemoteLogger delegates the responsibility to the JMSLoggerDelegate instance. The JMSLoggerDelegate publishes the LogMessage to a JMS Topic . The main work being performed in the JMSLoggerDelegate is to establish the necessary JMS connections. When a message is sent to the JMSLoggerDelegate , it just publishes the message to the Topic .

The RemoteLogger really does nothing except to forward the message to whichever delegate it knows about. The RemoteLogger appears in Listing 21.14.

Listing 21.14 The Remote Logger Delegates the Logging of the Message to a Delegate
 /**   * Title:        RemoteLogger<p>   * Description:  The RemoteLogger delegates a LogMessage to a particular   *               delegate based on the remote protocol.<p>   */  package com.que.ejb20.services.logging.remote;  import java.util.Hashtable;  import com.que.ejb20.services.logging.ILogger;  import com.que.ejb20.services.logging.LogMessage;  import com.que.ejb20.services.logging.LoggingException;  public class RemoteLogger implements ILogger {   // The delegate that handles the real remote logging    private ILogger delegate = null;    public RemoteLogger( ILogger logDelegate ) {     super();      this.delegate = logDelegate;    }    public void logMessage( LogMessage msg ) throws LoggingException {     delegate.logMessage( msg );    }    public void close() throws LoggingException {     delegate.close();    }  } 

The RemoteLogger takes any LogMessage sent to it and just forwards it on to the delegate instance. In this way, the same RemoteLogger class can support many different remote-logging implementations by just swapping out the delegate instance.

Running a remote logging example is a little more complicated than the local one. This is because the necessary JMS administrative objects must be configured. You'll also have to build a Topic subscriber that receives the log messages and does something with them. We chose a Topic over a Queue because it's possible to have log message subscribers hook up to the Topic and do various things with the log messages. For example, one subscriber could just store the message into a relational database, while another could only be looking for error log messages and then sending them as a pager message to the system administrator.

For our example, we'll just build a single subscriber that subscribes to the Topic and prints the messages out to the console. Before we begin, the environment properties must be modified to support the new initial logger factory. Here is the logging.properties file that has been modified to support the remote logging implementation.

 java.logging.factory.initial=      com.que.ejb20.services.logging.remote.RemoteInitialLoggerFactory  java.logging.provider.url=jms:LoggingConnectionFactory:LoggingTopic 

The logging.properties specifies the new InitialLoggerFactory implementation that we'll be using. It also contains java.logging.provider.url , which specifies the protocol and rest of the information that the delegate requires. Remember that for each implementation, the format of the provider URL might be different. For the JMSLoggerDelegate , the JMS connection factory and destination name is specified after the protocol in this property.

Listing 21.15 shows the Topic subscriber that will be used to receive messages from the Topic and print them out.

Note

We also could have used a message-driven bean as the message handler. We chose just to use a Java client to keep this example simple.


Listing 21.15 The Client Subscriber for the Logging Implementation
 import javax.jms.*;  import java.io.*;  import java.util.*;  import javax.naming.*;  import com.que.ejb20.services.logging.LogMessage;  public class LogSubscriber implements javax.jms.MessageListener, Runnable {   // The JNDI and JMS object references    private Context ctx = null;    private TopicConnectionFactory tcf = null;    private TopicSubscriber subscriber = null;    private TopicConnection topicConnection = null;    private Topic topic = null;    // Default Constructor    public LogSubscriber() {     super();    }    public void onMessage( Message msg ) {     if ( msg instanceof ObjectMessage) {       try {         Object obj = ((ObjectMessage)msg).getObject();          System.out.println( obj );        } catch( JMSException ex ) {         ex.printStackTrace();        }      }    }    public void init() throws JMSException, NamingException {     try{       // Lookup the jndi factory         ctx = new InitialContext();        // Get a connection to the TopicConnectionFactory        tcf = (TopicConnectionFactory)ctx.lookup( "LoggingConnectionFactory" );      // Create a connection      topicConnection = tcf.createTopicConnection();      // Create a session that is nontransacted and is notified automatically      TopicSession ses =        topicConnection.createTopicSession( false, Session.AUTO_ACKNOWLEDGE );      // Lookup a destination      topic = (Topic)ctx.lookup( "LoggingTopic" );      // Create the receiver with a msgSelector. The msgSelector may      // be null. The noLocal parameter is set so that this subscriber      // will not receive copies of its own messages      subscriber = ses.createSubscriber( topic );      // It's a good idea to always put a final block so that the      // context is closed      }catch( NamingException ex ) {       ex.printStackTrace();        System.exit( -1 );      }finally {       try {         // Close up the JNDI connection since we have found what we needed          ctx.close();        }catch ( Exception ex ) {         ex.printStackTrace();        }      }      // Inform the received that the callbacks should be sent to this instance      subscriber.setMessageListener( this );      // Start listening      topicConnection.start();      System.out.println( "Listening on topic" );    }    /**     * The run method is neccessary because this method implements the     * Runnable interface to keep the thread alive and waiting for messages.     * Otherwise, this would not stay alive and would not be able to     * listen for messages asynchronously.     */    public void run() {     while( true ) {       synchronized( this ){         try{           wait();          }catch( InterruptedException ex ){         }        }      }    }    /**     * Main Method     * This is the main entry point that starts the Topic subscriber listening     * for messages.     */    public static void main( String args[]) {     // Create an instance of the client      LogSubscriber subscriber = null;      try {       subscriber = new LogSubscriber();        subscriber.init();      }catch( NamingException ex ){       ex.printStackTrace();      }catch( JMSException ex ){       ex.printStackTrace();      }      // Start the client running      Thread newThread = new Thread( subscriber );      newThread.start();    }  } 

Although it might look like there is much going on in the LogSubscriber , there's really not. The client just initializes itself to listen on the Topic and waits for messages via the onMessage method. When a JMS message arrives, it simply calls the toString method on the object. If this were a production client, it might use JDBC to store the message attributes into a relational table or send them to a call center that was monitoring the application.

The two JMS administrative objects that must be configured for this example to work correctly are the LoggingConnectionFactory and the LoggingTopic . These are used in the LogSubscriber class in Listing 21.15 to allow the subscriber to connect to the JMS service. They are also specified in the logging.properties resource file.

To run the remote example, you should start the LogSubscriber first by typing

 java LogSubscriber 

on the command line. The client will print out that it's listening on the Topic. Once the LogSubscriber is running, run the LoggerTest class from Listing 21.11 as before. The LogSubscriber class should print out the LogMessage information to the console.

graphics/01icon01.gif

You must have all the compiled logging classes in your classpath. If you are having trouble running the remote logging example, see " Compiling and Running the Remote Logging Service ," in the " Troubleshooting " section at the end of this chapter.

Commercially Available Logging Services

Some free and commercial logging services are available. Most are quite complete and can be configured for various conditions. In some cases, they actually offer too much functionality and you'll find yourself trying to trim down what's included.

There's no reason why you couldn't use the previous architecture and build a factory that creates an instance that communicates with a third-party product. At least by doing this, you'll have the peace of mind that you could get rid of the third-party product and not have to change anything but the logging.properties file.

One of the most popular free Java logging frameworks can be found at

http://jakarta.apache.org/log4j/docs/index.html



Special Edition Using Enterprise JavaBeans 2.0
Special Edition Using Enterprise JavaBeans 2.0
ISBN: 0789725673
EAN: 2147483647
Year: 2000
Pages: 223

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net