Loggable: a simple log4j meta-library - Part 1

If you work in a J2EE environment, you surely have (or at least will have) used the standard Apache log4j library almost once. This powerful and configurable logging library is commonly used throughout whole enterprise projects, for many reasons: debugging, testing, exception logging, tracing, application profiling, et cetera.

A common code that shows how to use a org.apache.log4j.Logger instance is the following:

package com.marzapower.test;

import org.apache.log4j.Logger;

public class MyClass {
	private static final Logger logger = Logger.getLogger(MyClass.class);

	public static void main(String... args) {
		logger.debug("Hello World!");
	}
}

As you can see, you define a private (visible only to the instances of this specific class) static (shared amongst all instances of the class) final (constant, not variable) Logger instance, and then you use it within the class. If you had another class, you should define a similar object into that class too. If you had a third, again, you'd define a Logger in that class too. And so on.

But when the number of the classes in your project grows, this approach easily becomes very frustrating. You are writing your code, and want to use a Logger, but you cannot do it directly unless you define the Logger instance for your classes. Again, if you are refactoring an existing class, that heavily uses the Logger instance for debugging purposes, and you delete all the Logger.debug() calls then ... the classic yellow exclamation mark flag appears in your Eclipse editor: the private Logger instance defined is not being used anymore, so its declaration should be removed. And so on, you probably have faced a different (but still similar) problem before.

A solution: use Java Annotations and a centralized controller

Well, I have read this interesting article written by a friend of mine, and an ex-colleague too, and have thought to myself: "Is there a way to avoid this declaration overhead, and use a Logger instance freely throughout my whole application, demanding its creation and handling to a centralized controller? Wouldn't it be useful using Java Annotations to drive such logic?".

The answer is: "Yes!". And I'll show you how to do this.

The goal

I followed this base architecture: every class in your application will call a centralized controller to retrieve a suitable org.apache.log4j.Logger instance. This instance will be created and returned depending on the parameters of the class set by a custom annotation.

The centralized controller will be my com.marzapower.loggable.Log class. The annotation will be my com.marzapower.loggable.Loggable annotation. Using these two elements in conjunction within a class of our application, we will no longer need to define a private Logger instance. The @Loggable annotation will drive the logic, and the public static method Log.get() will be our common interface to the log4j world.

(I thought it would have been nice to call the method ger() instead of get(), just to write Log.ger(), but that's not a good name for that method ...)

We could rewrite our first example this way:

package com.marzapower.test;

import com.marzapower.loggable.*;

@Loggable
public class MyNewClass {
	public static void main(String... args) {
		Log.get().debug("Hello World!");
	}
}

Isn't it more concise and clear?

The @Loggable annotation

But, how can we drive the Log controller logic in order to return the correct Logger instance for our class? We use the @Loggable annotation for this. The @Loggable annotation, if present, must be set before the class definition. The annotation is defined this way:

package com.marzapower.loggable;

import java.lang.annotation.*;

@Documented
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.TYPE)
public @interface Loggable {
	String loggerName	default "";
	Class clazz()		default Object.class;
	boolean exclude()  default false;
	LogLevel logLevel()	default LogLevel.DEBUG;

	public enum LogLevel {
		TRACE, DEBUG, INFO, WARN, ERROR, FATAL
	}
}

Let's look at the parameters:

  • loggerName: the name of a logger defined in the log4j configuration for the project. It is optional, and the default value is an empty string
  • clazz: is an instance of Class<?> and will represent the object passed to the Logger.getLogger(Class<?>) constructor. It is optional, and the default value will be Object.class
  • exclude: a boolean parameter that determines wether the class should effectively log or not, its default is false
  • level: a LogLevel instance that defines the custom log level for the class, its default is LogLevel.DEBUG

First of all, to enable logging for a specific class we have to do nothing. Just from the beginning, every single class will be capable of logging with no additional effort (= no additional declarations); you will never need the recurring "bla bla bla Logger.getLogger(bla bla bla)" overhead. In fact, if no @Loggable annotation is set, the Log.get() method will return the root logger of log4j to the caller class.

Some usage examples

If you want to enable the logging capabilities of your class, using its standard log4j Logger instance, you should use:

@Loggable
class GenericClass {
  ...
}

In this example, Log.get() will return the same object as Logger.getLogger(GenericClass.class). Also, if you would disable the logging capabilities of a class that uses Log.get() for logging, you could use the following:

@Loggable(exclude = true)
class SilentClass {
  ...
}

Declaring SilentClass this way, every call to the Log.get() method will return a silent logger (eg. with log level set to "OFF"). Now, imagine that you want to use, for your class, a Logger instance different from the standard one. In the classic log4j approach you'd do this:

class ThisClass {
  private static final Logger logger = Logger.getLogger(AnotherClass.class);
  ...
}

while now you can do it in a simpler form:

@Loggable(clazz = AnotherClass.class)
class ThatClass {
  ...
}

(Update) You can do the same, if you want to use a specific log4j named logger instead of the standard one. You just will have to change the latest example this way:

@Loggable(loggerName = "verboseLogger")
class VerboseClass {
  ...
}

Also, you could have the need of limiting all the logger calls of a class to a WARN level, or higher, but the current log4j configuration for that class has a larger INFO level. You can force this need through the @Loggable annotation. Eg.:

@Loggable(level = LogLevel.WARN)
class WarnerClass {
  ...
}

From now on, the WarnerClass will log only messages with level WARN or higher, despite of the actual log4j default configuration.

Next episode: the controller

As we have seen, the @Loggable annotation is really helpful. But, it will have no effect without its back-end class, the centralized controller Log. I will describe you this class in one of my next posts, and I hope to release a fully commented and tested library soon.

If you have questions/suggestions for this meta-library, please drop me a line. Keep in touch!

Update: the second "episode" is now available.
Also, the definition of the @Loggable annotation has been revised introducing the "loggerName" parameter

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *