Channels ▼
RSS

Design

Custom Configuring Java Apps: Extending log4j


Is or Uses?

Let's look at a class that attempts to solve these configuration problems. There is an important design decision to make before starting to code. In general, you'd like your logger to work just like the standard log4j Logger. Here's how the standard Logger works:

class MyClass
{
    private static final Logger log = Logger.getLogger(MyClass.class);
    //...
    f()
    {   log.debug( "hello world" );
    }
}

and in an ideal world, you'd use your own logger in an identical way:

class MyClass
{
    private static final Logger log = MyLogger.getLogger(MyClass.class);
    //...
    f()
    {   log.debug( "hello world" );

        ExtendedLogger elog = (ExtendedLogger) log; // use an extended feature
        elog.myExtendedFeature( "abcd" );
    }

That is, the MyLogger class would define a static getLogger(String name) method that returned one of your magic loggers instead of a lowly log4j logger, but your logger would extend the standard Logger class.

Unfortunately, you can't do that.

The problem is that log4j guarantees that every time you request a logger using a given name, you get the same logger, even of those requests are made from different files. The log4j subsystem keeps a global cache of loggers, indexed by logger name (which usually is, but doesn't have to be, the class name), and getLogger() always returns the logger from the cache if one's there (otherwise, it creates one).

Imagine, then, that we have the following line in one file (which happens to load first):

private static final Logger log = Logger.getLogger("my.logger.name");

and the following line in a different file (which happens to load after the first file):

private static final Logger log2 = MyLogger.getLogger("my.logger.name");

The second call must return the same logger that the first call returns. The problem is the following line of code, which fails with a ClassCastException because log points at the Logger object created by the first call.

   MyLogger myLog = (MyLogger) log2;

By the same token, you'll get the ClassCastException even if you get your logger like this:

    private static final MyLogger log = MyLogger.getLogger(MyClass.class);

(because getLogger must still return the org.apache.log4j.Logger that was created first, and the assignment won't work because of the JVM effectively checks the types at runtime when it does the assignment. In this situation, you just can't create a MyLogger that has the same name as an existing standard Logger.

You can't solve this problem, but you can work around it by using a wrapping strategy rather than extension — a uses relationship rather than an is relationship. My logger is an independent class that extends nothing, but it implements all the methods of org.apache.log4j.Logger, so anybody who's used to log4j will be comfortable with it. My logger contains an org.apache.log4j.Logger, and delegates all the logging operations to the contained object, so my logger can happily interact with other log4j loggers. You can't, however, convert one of my loggers to a log4j Logger with a simple cast operation — the two classes are in no way related.

Though it's an accepted design principle that "uses" is better than "is," the implementation that you're forced to use by log4j makes that decision more difficult than necessary — log4j is missing an essential interface. Ideally, the log4j Logger and my extended version would implement a common interface so that I can use them interchangeably. The Log4j subsystem defines no such interface, however.

Let's look at the issue in depth. The correct way to replace derivation with encapsulation is the Decorator design pattern. Think about how the Java I/O system works. Though you could use extension (A PrintWriter could extend BufferedWriter, which could extend FileWriter, which could extend Writer), but the number of classes required to get every combination that you can get with the current system would exploded (the class count is doubled with every feature). Java's wrapping strategy (which is a classic Decorator) works better; you can pick and choose the classes you put into the layers.

One of the important uses of Decorator is to replace extension (is) with wrapping (uses), and that's what log4j should have done. In Decorator, you have a common interface that's implemented by a core class (FileWriter):

interface Writer
{   void write( String buffer );
    //...
}

class FileWriter implements Writer
{   @Override void write( String buffer );
    {   // implementation goes here
    }
    //...
}

You could (but shouldn't) extend the implementation class

class MyBadWriter extends FileWriter        // NO! NO! NO!
{   @Override void write( String buffer );
    {   
        //...
        super.write( buffer );
    }
    //...
}

but you can achieve exactly the same effect by implementing the common interface and encapsulating the object that would otherwise be the base class:

class MyGoodWriter implements Writer
{   
    private Writer wrapped;
    public MyGoodWriter( Writer wrapped )
    {   this.wrapped = wrapped; // or create one locally
    }

    @Override void write( String buffer );
    {   //...
        wrapped.write( buffer );
    }
    //...
}

This strategy lets us pass both a stock FileWriter and a custom MyGoodWriter to any method that takes a Writer argument, just as if we had used extension. You can't, however, pass a MyGoodWriter to a method that requires a FileWriter. All design patterns have limitations, but this one isn't much of a problem in practice.

Unfortunately, org.apache.log4j.Logger does not implement any interfaces at all, so I can't leverage Decorator to clean up the code.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video