Item 6: Use hook points to inject optimizations, customizations, or new functionality





Item 6: Use hook points to inject optimizations, customizations, or new functionality

Almost every system, whether J2EE-oriented in nature or not, provides a number of "hook points" by which developers can "hook" into standard processing and influence what happens next. Within the servlet stack, for example, filters represent the most obvious hook point, allowing servlet developers the opportunity to block, redirect, or modify any incoming HTTP request or outgoing HTTP response. You can use filters to implement compression of the HTTP body, encryption of parts or all of the content, or any other authentication and/or authorization requests ladled into the HTTP message exchange.

Another such hook point presents itself within the RMI stack, via the custom socket factories that can be plugged into the RMI client and/or server. Secure RMI exchange can be added simply by replacing the standard socket factory objects with customized ones that force transmission over SSL/TLS sockets instead of the standard TCP/IP socket. In an all-Windows network, RMI could even use a custom socket factory to exchange data over a (faster) customized Windows named-pipe channel, for example, instead of TCP/IP entirely.

In each of these cases, a hook point is used to add new behavior without requiring any client code changes; the additional processing is added at some point in the standard processing chain. This additional processing can take many forms, either by providing an opportunity to pass some kind of out-of-band data, such as security context established from one tier and passed to another, or by consulting an external resource for shortcut processing (such as a cache for previously obtained immutable data, or perhaps an optimizer to rearrange SQL statements in optimal form), and so on. For example, the Java Data Objects Specification explicitly provides callers with the ability to pass a vendor-specific query to the JDO PersistenceManager, thus allowing the clients that wish to take advantage of proprietary vendor-value-added features (see Item 11 for why you might want to do this) to do so.

A classic way to provide a hook point is to use the Interception pattern [POSA2, 109]. After hearing how the J2EE containers make use of interception to add all this behavior around your code, interception may seem a deep, dark, mysterious secret that only programmers who are part of the Cult of the Container Implementor can understand. Rubbish. Interception isn't reserved exclusively for J2EE's own use. Starting with the JDK 1.3 release, Java provides dynamic proxies, which allow a programmer to build an anonymous object that implements an interface and wraps around another object implementing that same interface, thus receiving the calls bound for that original proxied object and providing additional behavior.

To put that into English, dynamic proxies permit you to write interceptors around normal Java objects, as long as they're referenced through interfaces.

For example, presume we're back to figuring out how we can add some diagnostic-logging behavior into code without requiring massive code changes all over the codebase. Instead of more exotic code-weaving mechanisms, we could capture this in a first-class concept using dynamic proxies in front of any object that exposes itself via a Java interface:




public LoggingProxy

  implements InvocationHandler

{

  public static Object

    newLoggingProxyAround(Logger logger, Object obj)

  {

    return Proxy.newProxyInstance(

      obj.getClass().getClassLoader(),

      obj.getClass().getInterfaces(),

      new LoggingProxy(logger, obj));

  }



  private Object proxiedObject;

  private Logger logger;



  private LoggingProxy(Logger l, Object obj)

  {

    logger = l;

    proxiedObject = obj;

  }



  public Object invoke(Object proxy,

                       Method m, Object[] args)

    throws Throwable

  {

    Object result;

    try

    {

      l.info("Entering method " + m.getName());

      result = m.invoke(proxiedObject, args);

    }

    catch (Exception x)

    {

      l.warn("Unexpected exception " + x + " invoking "

          + m.getName());

      throw x;

    }

    finally

    {

      l.info("Exiting method " + m.getName());

    }

    return result;

  }

}


A LoggingProxy will thus wrap any object it is constructed around (using the newLoggingProxyAround factory method call) and write diagnostic messages for every method call that comes in through that interface. Using it would look something like the following:




public interface Person

{

  public String getFirstName();

  public void setFirstName(String value);



  public String getLastName();

  public void setLastName(String value);



  public int getAge();

  public void setAge(int value);

}



public class PersonImpl

  implements Person

{

  // other details omitted for brevity



  private String fname;

  private String lname;

  private int age;



  public String getFirstName() { return fname; }

  public void setFirstName(String value) { fname = value; }



  public String getLastName() { return lname; }

  public void setLastName(String value) { lname = value; }



  public int getAge() { return age; }

  public void setAge(int value) { age = value; }

}



public class PersonManager

{

  public Logger personLogger = ...; // some Logger instance

  public boolean loggingEnabled = false;

    // Set to "true" to turn on logging on objects



  public static Person

    getPerson(String firstName, String lastName)

  {

    Person p = new PersonImpl();

      // get Person from someplace; in a real system, we

      // would obviously do a database lookup or something

      // similar here

      //



    if (loggingEnabled)

      p = (Person)

        LoggingProxy.newLoggingProxyAround(personLogger, p);



    return p;

  }

}


To put this into words, when the static PersonManager.getPerson method is invoked, we'll go through the work of digging the appropriate person's data out of the database (or wherever it happens to be stored) and dump it into a concrete class representative of the domain object for Person instances. We do it through a static factory method [Bloch, Item 1], however, rather than a constructor, because we want to go one extra step of indirection from the client in constructing the object.

Remember, the point of all this was to get some diagnostic logging behavior "laced in" inside the objects in question. Since we can't (or rather, don't want) to modify the actual bytecode itself, we just want to spit out a diagnostic logging message at the start and end of each call we make. So we'll present an interceptor object in front of the actual PersonImpl object; the interceptor will keep a reference to the PersonImpl and forward all calls it receives on to the PersonImpl, but only after spitting out the diagnostic logging message.

The beauty of this approach is that regardless of the actual object type on the other side of the LoggingProxy, the method interception approach still works. The key is that the proxy requires all calls to the proxied object (the PersonImpl in this example) to go through an interface rather than a concrete class—this is why PersonManager returns a Person, as opposed to a PersonImpl. Better yet, there is no structural compile-time relationship between the Person implementation and the act of logging. Instead, it can be turned on or off at will, simply by changing the value of the loggingEnabled flag, which is presumably a hot-configured configuration item (see Item 13).

By building components instead of objects (see Item 1), we can make use of interceptors in whatever desired fashion to provide the behavior desired without changing any domain-specific or client-facing code. Use interfaces to define what clients will see outside of the component, use public static factory methods to provide construction facilities, and hide the actual object implementation returned behind one—or more—dynamic proxies that provide the desired crosscutting functionality. As a bonus, we get loose coupling (see Item 2) for free, too, since implementation can change without clients knowing or caring.

Not all hook points within the J2EE family of specifications are Interceptors [POSA2, 109], however. For example, the Serialization Specification (see Item 71) represents another such hook point within a J2EE system, since J2EE itself makes use of Serialization in a number of places, including marshaling of RMI method call parameters and (frequently, although this isn't a mandatory part of the specification) as a passivation mechanism for stateful EJB objects like stateful session beans and entity beans. In fact, for most normal Data Transfer Objects [Fowler, 401], the default Serialization logic is overkill, since most Data Transfer Objects don't require deep inheritance hierarchies or have extensive object references. Thus, for Data Transfer Objects that are used frequently, it might behoove you to replace the standard Serialization behavior by writing something extremely fast and customized for that particular Data Transfer Object—but only after profiling and determining that marshaling of this particular Data Transfer Object is a bottleneck in the communications stack (see Item 10).

While not all hook points within a system are defined as Interceptors, many of them are, owing to the passive client-oriented nature of J2EE systems in general (see Item 1). For example, the Java API for XML RPC (JAX-RPC) Specification, the Java side of WSDL-specified Web Services, defines Interceptors as Handler-implementing classes that are able to inspect SOAP packets coming back and forth between incoming requests and the outgoing responses. This permits developers to write vendor-neutral extensions to process new SOAP headers, such as those defined by WS-Security and/or WS-Routing, without having to change endpoint code itself. This will be particularly important as we begin to use EJB beans as Web Service endpoints, since your particular J2EE/EJB container may not support the latest-and-greatest WS- specification you're looking for.

It's unfortunate that one of the most widely known specifications in the J2EE family, the EJB Specification, provides no vendor-independent interception mechanism. This is a large part of at least one vendor's widespread popularity, since it exposes, in a vendor-specific fashion, a rich and powerful interception-oriented stack. Hopefully this can be corrected in future versions of the EJB Specification.


     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows