http://thoughtforge.net/665/creating-a-logging-aspect-with-spring-aop-and-aspectj/
The Logger
There are a few options to choose from when considering a logging framework and these have evolved over the years so it is probably best to abstract the logging framework from the aspect. For this purpose, I have implemented a simple logging interface and log level enum.
package net.thoughtforge.logger;
public interface Logger {
boolean isLogLevel(LogLevel logLevel, Class clazz);
void log(LogLevel logLevel, Class clazz, Throwable throwable, String pattern, Object... arguments);
}
package net.thoughtforge.logger;
public enum LogLevel {
DEBUG,
ERROR,
FATAL,
INFO,
TRACE,
WARN
}
In the majority of cases, I end up using the Commons Logging framework and so have included this implementation of the Logger in the source download.
You may decide that this level of abstraction is overkill and that you are willing to commit to using a specific logging framework. When making this decision, keep in mind that you will be specifying the log level throughout your code base.
The Logging Aspect
I am going to use the @Aspect approach to implement the logging aspect which is to say I will use annotations to specify the advice. I want the logging aspect to log the method name, argument values, return value and any exception thrown so I will use the @Before, @AfterThrowing and @AfterReturning annotations.
@Before(value = "@annotation(loggable)", argNames = "joinPoint, loggable")
public void before(JoinPoint joinPoint, Loggable loggable) {
Class clazz = joinPoint.getTarget().getClass();
String name = joinPoint.getSignature().getName();
if (ArrayUtils.isEmpty(joinPoint.getArgs())) {
logger.log(loggable.value(), clazz, null, BEFORE_STRING, name, constructArgumentsString(clazz, joinPoint.getArgs()));
} else {
logger.log(loggable.value(), clazz, null, BEFORE_WITH_PARAMS_STRING, name, constructArgumentsString(clazz, joinPoint.getArgs()));
}
}
The ‘Before’ advice simply logs (at the appropriate log level) the method name and the toString value of all arguments (if any). ‘Before’ advice executes when a join point is reached.
@AfterThrowing(value = "@annotation(net.thoughtforge.aspect.Loggable)", throwing = "throwable", argNames = "joinPoint, throwable")
public void afterThrowing(JoinPoint joinPoint, Throwable throwable) {
Class clazz = joinPoint.getTarget().getClass();
String name = joinPoint.getSignature().getName();
logger.log(LogLevel.ERROR, clazz, throwable, AFTER_THROWING, name, throwable.getMessage(), constructArgumentsString(clazz, joinPoint.getArgs()));
}
The ‘AfterThrowing’ advice logs the method name, exception message and the toString value of all arguments (if any). ‘AfterThrowing’ advice executes after a method exits by throwing an exception.
@AfterReturning(value = "@annotation(trace)", returning = "returnValue", argNames = "joinPoint, trace, returnValue")
public void afterReturning(JoinPoint joinPoint, Loggable loggable, Object returnValue) {
Class clazz = joinPoint.getTarget().getClass();
String name = joinPoint.getSignature().getName();
if (joinPoint.getSignature() instanceof MethodSignature) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Class returnType = signature.getReturnType();
if (returnType.getName().compareTo("void") == 0) {
logger.log(loggable.value(), clazz, null, AFTER_RETURNING_VOID, name, constructArgumentsString(clazz, returnValue));
return;
}
}
logger.log(loggable.value(), clazz, null, AFTER_RETURNING, name, constructArgumentsString(clazz, returnValue));
}
The ‘AfterReturning’ advice logs the method name and the toString value of the returned value (if any). ‘AfterReturning’ advice executes after a method exits normally.
You will notice that I log the toString value to identify objects. I routinely use the Apache Commons ToStringBuilder to create the toString value. I find this particularly useful when working with persistent entities as it allows me to clearly identify the entity.
Another possible implementation that avoids using the toString method is to use the Apache Commons ReflectionToStringBuilder within the logging aspect to create a string representation of the object being logged.
If you are writing your own toString implementations (the Commons implementation is perfectly adequate) and are implementing an object with complex properties be aware of recursive invocations that may result in a stack overflow exception.
Specifying the Pointcut
A pointcut expression is an expression that specifies where in the code the advice will be applied. With AspectJ, you can create a pointcut by specifying package, class and method attributes among other things. I find the easiest way to specify a pointcut for the logging aspect is by matching methods that have a specific annotation.
package net.thoughtforge.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import net.thoughtforge.logger.LogLevel;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {
LogLevel value();
}
As you can see, the Loggable annotation has one property that specifies the log level at which the log statement should be output. Using the annotation means that developers never need to alter the pointcut expression to add or remove methods to the pointcut. A developer only has to add the annotation to a method to have the logging aspect applied.
package net.thoughtforge.bean;
import java.util.Date;
import net.thoughtforge.aspect.Loggable;
import net.thoughtforge.logger.LogLevel;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.springframework.stereotype.Component;
@Component(value = "simpleBean")
public class SimpleBean {
private Date dateProperty;
private Integer integerProperty;
private String stringProperty;
@Loggable(value = LogLevel.TRACE)
public Date getDateProperty() {
return dateProperty;
}
@Loggable(value = LogLevel.TRACE)
public void setDateProperty(final Date dateProperty) {
this.dateProperty = dateProperty;
}
@Loggable(value = LogLevel.TRACE)
public Integer getIntegerProperty() {
return integerProperty;
}
@Loggable(value = LogLevel.TRACE)
public void setIntegerProperty(final Integer integerProperty) {
this.integerProperty = integerProperty;
}
@Loggable(value = LogLevel.TRACE)
public String getStringProperty() {
return stringProperty;
}
@Loggable(value = LogLevel.TRACE)
public void setStringProperty(final String stringProperty) {
this.stringProperty = stringProperty;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("dateProperty", dateProperty)
.append("integerProperty", integerProperty).append("stringProperty", stringProperty).toString();
}
}
The SimpleBean and SimpleBeanSubclass are for demonstration purposes. You can see that each method is annotated with the @Loggable annotation and the log level is set to TRACE. You can obviously use different log levels for different methods as required.
package net.thoughtforge.bean;
import java.math.BigDecimal;
import net.thoughtforge.aspect.Loggable;
import net.thoughtforge.logger.LogLevel;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.springframework.stereotype.Component;
@Component(value = "simpleBeanSubclass")
public class SimpleBeanSubclass extends SimpleBean {
private BigDecimal decimalProperty;
@Loggable(value = LogLevel.TRACE)
public BigDecimal getDecimalProperty() {
return decimalProperty;
}
@Loggable(value = LogLevel.TRACE)
public void setDecimalProperty(final BigDecimal decimalProperty) {
this.decimalProperty = decimalProperty;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("decimalProperty", decimalProperty).appendSuper(super.toString()).toString();
}
}
Also note the use of the ToStringBuilder to create the toString value. You may choose to use the ReflectionToStringBuilder or some other mechanism.