Archive

Posts Tagged ‘adapter’

Uniform Caching

October 13th, 2009

Typically object caching in Java is managed by the container or framework in use. Occasionally however there is a need to manually cache domain-specific objects, whereby a java.util.Map implementation will not suffice.

Using the popular ehcache framework as an example, the following pattern is typically observed:

public class SomeClass {

  private final Cache cache = ...

  ...

  public void doSomethingWithObject(Object key) {
    SomeObject o = getSomeObject(key);
    o.doSomething();
  }

  public SomeObject getSomeObject(Object key) {
    SomeObject o = null;

    Element element = cache.get(key);
    if (element != null) {
      o = element.getValue();
    }
    else {
      o = load(key);
      cache.put(new Element(key, o));
    }
    return o;
  }

  private Object load(Object key) {
    ...
  }
}

The common aspects of this pattern are as follows:

  • Cache – the cache instance
  • Key – the unique key of the cachable object
  • load() – the mechanism for loading objects not in the cache

Key Uniformity

As most caching frameworks will allow any object to be used as a key, there is potential for different types of errors, such as a value specified as a key, mixing object types in a single cache, added to the wrong cache instance, etc. We can avoid some of these problems by enforcing a uniform approach to defining cache keys:

public enum CacheEntry {

  SomeObject("org.mnode.example.someObject.%s");

  private String key;

  public String getKey(Object uid) {
    return String.format(key, uid);
  }
}

public class SomeClass {
  ...

  public SomeObject getSomeObject(Object uid) {
    SomeObject o = null;
    String key = CacheEntry.SomeObject.getKey(uid);
    ...
  }
}

As this approach enforces a key ‘namespace’ for specific object types, it also makes it easier to store mixed data in a single cache, thus simplifying the management of cached objects:

public class SomeClass {
  ...

  public <T> T get(CacheEntry entry, Object uid) {
    T o = null;
    String key = entry.getKey(uid);

    Element element = cache.get(key);
    if (element != null) {
      o = (T) element.getValue();
    }
    else {
      o = (T) load(key);
      cache.put(new Element(key, o));
    }
    return o;
  }
}

Object Loading

Different types of cached data will also usually require specific code for loading the data initially. We can refactor this to be defined as part of the CacheEntry:

interface Loader<T> {
  T load(Object...args);
}

public enum CacheEntry {

  SomeObject("org.mnode.example.someObject.%s", new Loader<SomeObject> {
    SomeObject load(Object...args) {
      Object uid = args[0];
      // load data from backing store..
      ...
    }
  });

  private String key;

  private Loader<?> loader;

  public String getKey(Object...args) {
    return String.format(key, args);
  }

  public Object load(Object...args) {
    loader.load(args);
  }
}

Using this combined object loader and key namespace support we can extract the caching logic to a generic adapter:

public class CacheAdapter {

  private final Cache cache;

  public CacheAdapter(Cache cache) {
    this.cache = cache;
  }

  public <T> T get(CacheEntry entry, Object...args) {
    T o = null;
    String key = entry.getKey(args);

    Element element = cache.get(key);
    if (element != null) {
      o = (T) element.getValue();
    }
    else {
      o = (T) entry.load(args);
      if (o != null) {
        cache.put(new Element(key, o));
      }
    }
    return o;
  }
}

public class SomeClass {

  private final CacheAdapter cache = ...

  public void doSomethingWithObject(Object uid) {
    SomeObject o = cache.get(CacheEntry.SomeObject, uid);
    o.doSomething();
  }

  public SomeObject getSomeObject(Object uid) {
    return cache.get(CacheEntry.SomeObject, uid);
  }
}

A Real Example

Caching XMPP VCard objects:

import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.packet.VCard;

public enum CacheEntry {

  VCard("vcard.%s", new Loader<VCard> {
    VCard load(Object...args) {
      String user = (String) args[0];
      XMPPConnection connection = (XMPPConnection) args[1];
      try {
        VCard card = new VCard();
        card.load(connection, user);
      } catch (XMPPException e) {
        return null;
      }
    }
  });
}

public class AvatarRepository {

  private final CacheAdapater vcardCache = ...

  private final XMPPConnection connection = ...

  public Image getAvatar(String user) {
    Image avatar = null;

    VCard vcard = vcardCache.get(CacheEntry.VCard, user, connection);
    if (vcard != null) {
      avatar = new ImageIcon(vcard.getAvatar()).getImage();
    }

    return avatar;
  }
}

Conclusion

By defining a key namespace and object loading mechanism for cachable data types we can improve the manageability of object caching in the following ways:

  • Improved support for mixed data type caching
  • Increased decoupling from the caching implementation
  • Uniformity in object loading and caching

Java , ,

Uniform Logging

September 25th, 2009

Application logging always seems to become one of those code smells, typically regarding duplication of code, or conversely, non-uniform log messages.

There are many different ways to log a message in Java, but variations on the following pattern are common:

public class SomeClass {

  private static final Log LOG = LogFactory.getLog(SomeClass.class);

  ...

  public void someMethod() {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Some message - someObject is: " + someObject);
    }

    try {
      ...
    } catch (SomeException e) {
      LOG.error("Unexpected error: " + e.getMessage(), e);
    }
  }
}

The following information can be extracted from this pattern:

  • Category – classification for log entries
  • Level – the severity of a log entry
  • Message – a log entry message
  • Message arguments – variable components of a log message
  • Exception – an exception for logging a stack trace

Message Uniformity

One problem with this pattern is that we tend to duplicate the same message strings when logging similar scenarios (e.g. unexpected exceptions).

Messages are also generally constructed by concatenating messages and message arguments – a practice that generally should be avoided if possible. We can solve these issues using message templates:

import java.text.MessageFormat;

public class SomeClass {

  private static final String UNEXPECTED_ERROR_MESSAGE = "Unexpected error: {0}";

  ...

  public void someMethod() {

    try {
      ...
    } catch (SomeException e) {
      LOG.error(MessageFormat.format(UNEXPECTED_ERROR_MESSAGE, e.getMessage()), e);
    }
  }
}

Inconsistent log messages also result from having the message strings defined across multiple classes. Using message templates we can refactor these messages to be defined in a single location:

public enum LogEntry {
  UnexpectedError("Unexpected Error: {0}");

  private final String message;

  public String getMessage(Object...args) {
    return MessageFormat.format(message, args);
  }
}

public class SomeClass {

  ...

  public void someMethod() {

    try {
      ...
    } catch (SomeException e) {
      LOG.error(LogEntry.UnexpectedError.getMessage(e.getMessage()), e);
    }
  }
}

Log Levels

In the majority of cases, log entries that share the same message will also be logged at the same level. By associating a default log level with a log entry we can enforce uniformity of log levels:

public enum LogLevel {
  Trace, Debug, Info, Warn, Error;
}

public enum LogEntry {
  UnexpectedError("Unexpected Error: {0}", LogLevel.Error);

  ...

  private final LogLevel level;

  public LogLevel getLevel() {
    return level;
  }
}

public class LogAdapter {
  private final Log log;

  ...

  public void log(LogEntry entry, Object...args) {
    if (entry.getLevel() == LogLevel.Error) {
      log.error(entry.getMessage(args));
    }
    else if (entry.getLevel() == LogLevel.Warn) {
      log.warn(entry.getMessage(args));
    }
    else ...
  }

  public void log(LogEntry entry, Throwable e, Object...args) {
    if (entry.getLevel() == LogLevel.Error) {
      log.error(entry.getMessage(args), e);
    }
    else if (entry.getLevel() == LogLevel.Warn) {
      log.warn(entry.getMessage(args), e);
    }
    else ...
  }
}

public class SomeClass {

  private static final LogAdapater LOG = new LogAdapter(LogFactory.getLog(SomeClass.class));
  ...

  public void someMethod() {

    try {
      ...
    } catch (SomeException e) {
      LOG.log(LogEntry.UnexpectedError, e, e.getMessage());
    }
  }
}

To avoid the expensive construction of frequently logged message strings we use conditionals to check if a log level is enabled prior to message construction:

  ...
  if (LOG.isDebugEnabled()) {
    LOG.debug("Some message - someObject status is: " + someObject.expensiveMethod());
  }
  ...

Such conditionals are prone to error however, especially if the log level is changed:

  ...
  if (LOG.isDebugEnabled()) {
    LOG.warn("Some message - someObject status is: " + someObject.expensiveMethod());
  }
  ...

Uniform logging can help to avoid such mistakes:

public enum LogEntry {
  SomeObjectStatus("Some message - someObject status is: {0}", LogLevel.Debug);
  ...
}

public class LogAdapter {
  ...

  public boolean isLoggable(LogEntry entry) {
    if (entry.getLevel() == LogLevel.Debug) {
      return log.isDebugEnabled();
    }
    ...
  }

  public void log(LogEntry entry, Object...args) {
    if (isLoggable(entry) {
      ...
    }
  }

  public void log(LogEntry entry, Throwable e, Object...args) {
    if (isLoggable(entry) {
      ...
    }
  }
}

public class SomeClass {
  ...

  public void someMethod() {
    if (LOG.isLoggable(SomeObjectStatus) {
      LOG.log(SomeObjectStatus, someObject.expensiveMethod());
    }
  }
}

Note that a level check conditional is only required whereby an expensive method must be called to retrieve message arguments. The expense of the message string construction is handled by the LogAdapter.

Conclusion

Logging can be a repetitive, expensive and often error prone exercise. By centralising the log entries we reduce code duplication and potential for bugs through uniformity and re-use.

Java , ,