Aspect Oriented Programming (AOP): Using AspectJ to
implement and enforce coding standards
Introduction
Aspect-oriented programming is a relatively new concept
with the purpose of lowering complexity inherent to software
development. Essentially, AOP "captures" multi-class
interactions scattered throughout code (a.k.a. cross-cutting
concerns) and encapsulates them into a single, centralized,
class-like unit called an "aspect". For more details on
AOP, see the Resources section below.
An additional "tool" to help reduce program complexity is a
uniform coding standard. A good coding standard goes a
long way in encouraging coding best practices: it increases
code readability by promoting consistency, and, it allows
peers to focus on class design and algorithms rather than
mundane details such as whitespace formatting during code
reviews.
One of the downsides to coding standards and reviews is
delayed feedback - sometimes non-trivial changes are needed in
code that has been updated between the time the review was
scheduled and performed. Knowing that a change costs
less in the long run is a strong motivator to determine
changes as soon as possible. Another downside is that
developers are sometimes resistant to, as well as, forgetful
of the standards - especially when a deadline is hanging over
their heads. The optimal solution is to compel
developers to follow standards during compilation and unit
testing.
Aspects can enforce or implement standards two ways:
detecting static program structure at compile-time and dynamically
detecting program state at run-time.
Specifically, compile-time aspects can detect inappropriate
program structure and then emit a compiler message.
Run-time aspects, on the other hand, can detect interactive,
conditional behavior and then augment or replace the
programmer-defined behavior with the standard-defined
behavior.
Examples: Compile-Time Coding Standards
As objects become increasingly complex through their
lifetime, changing a single member variable may require
updating another variable or interacting with another class.
Coders are sometimes unaware of these obfuscated constraints
due to lack of documentation and convoluted coding.
Forcing programmers to use setter methods provides a single
point of entry to update state rather than having it scattered and/or
(incorrectly) copied throughout the class. The following pointcut and
advice enforces setter methods:
pointcut directMemberAssignment():
set(* *.*) && !withincode(* set*(..)) &&
!withincode(*.new(..));
declare error: directMemberAssignment():
"Coding standards require the use of a setter for all " +
"member variable assignments, even within the class itself.";
Another difficult situation occurs when client code has not
been insulated through a layer of indirection from low level
libraries. Changing design decisions after construction has
started creates hard-to-find bugs. Sometimes these
changes are the result of accommodating a maturing library,
other times it is the result of needing to completely replace
an inadequate library. Regardless, programmers should be
forced through a single point of entry for things such as
persistence or security so that side-effects can be minimized.
The following code from the AspectJ Programmer's Guide
demonstrates how to enforce this standard:
pointcut restrictedCall():
call(* java.sql.*.*(..)) || call(java.sql.*.new(..));
pointcut illegalSource():
within(com.foo..*) && !within(com.foo.sqlAccess.*);
declare error: restrictedCall() && illegalSource():
"java.sql package can only be accessed from com.foo.sqlAccess";
Examples: Run-Time Coding Standards
One particularly nasty type of bug to track down occurs
when programmers use the "on error condition, return null from
method" anti-pattern. The reason this is an anti-pattern
is two-fold: all calls to the method that are not wrapped by
error handling code will cause a latent NullPointerException,
and, the error handling code must be found and updated
manually everywhere it is used. The preferred technique
is to have the method throw an exception with as much context
as possible to describe the error condition. To avoid
copying exception handling code everywhere the method is
called, an aspect can be written that will consistently handle
the error. Here is a snippet which detects the
anti-pattern and outputs a message to the error console:
//The first primitive pointcut matches all calls,
//The second avoids those that have a void return type.
pointcut methodsThatReturnObjects():
call(* *.*(..)) && !call(void *.*(..));
Object around(): methodsThatReturnObjects()
{
Object lRetVal = proceed();
if(lRetVal == null)
{
System.err.println(
"Detected null return value after calling " +
thisJoinPoint.getSignature().toShortString() +
" in file " +
thisJoinPoint.getSourceLocation().getFileName() +
" at line " +
thisJoinPoint.getSourceLocation().getLine()
);
}
return lRetVal;
}
The final example coding standard is more related to
debugging convenience and auditing rather than preventing poor
programming practices. In particular, distributed
applications often run as a child process under a J2EE
application server. Based on the specific situation and
application server, the application may not have access to the
console for message display. Also, these applications
are running on multiple machines which can make debugging very
difficult. With these considerations, using a logging
package is preferred to using System.out or System.err.
This example takes the concepts presented earlier and
extends them by allowing a programmer to "opt-in" to aspect
logging and use the features of the java.util.logging package.
To achieve this, an application must implement the ILoggable
tag interface. The aspect then uses static introduction
to add methods and member variables to the implementing class
for full access to logging. The following snippet
initializes logging:
private static Logger fLogger;
//Static introduction!
private static Logger ILoggable.fLogger;
pointcut initLogger():
execution(*.new(..)) && !within(ForceLogging) &&
!within(lib.aspects..*) && !within(ILoggable);
Object around(): initLogger()
{
//If the logger is not initialized, do it here.
if(fLogger == null)
{
//Factory the logger based on the FQN of the object being
//instantiated.
System.out.println("Creating a default logger named \"" +
thisJoinPointStaticPart.getSignature().getDeclaringType().getName() +
"\"");
/* The next line invokes advice that sets ForceLogging.fLogger
* to the same value. This approach has been taken so that
* the Logger is easily used by both ForceLogging for
* intercepting System.out and System.err and for ILoggable
* default behavior.
*/
ILoggable.setLogger(
Logger.getLogger(
thisJoinPointStaticPart.getSignature().getDeclaringType().getName()));
try
{
String lUserDir = System.getProperty("user.dir");
String lPathSep = System.getProperty("file.separator");
System.out.println("File logging defaulting to <user.dir>" +
lPathSep + "<fully-qualified-class-name>.log:\n\t" +
lUserDir + lPathSep +
thisJoinPointStaticPart.getSignature().getDeclaringType().getName());
Handler lHandler =
new FileHandler(
lUserDir + lPathSep +
thisJoinPointStaticPart.getSignature().getDeclaringType().getName() +
".log");
fLogger.addHandler(lHandler);
}
catch(Exception e)
{
/* This is not the preferred way to notify a user of
* failing to create the handler because the preferred way
* is to use the handler that was supposed to be created!
* However, this is only a minor error, so swallow the
* exception so that the application can continue.
*/
e.printStackTrace();
}
}
Object o = proceed();
/* The log level is defaulted to the value set in
* ${java.home}/lib/logging.properties.
*
* The ConsoleHandler is the default Handler and is automatically
* added to the "parent" logger of all loggers. Remove it from the
* logging.properties file if you don't want this behavior.
*/
return o;
}
//Static Introduction! -- also conforms to the "setter" coding standard.
public static void ILoggable.initLogging(Logger pLogger, Handler pHandler)
{
setLogger(pLogger);
}
//Pointcut that joins on the setLogger() method
//that was statically introduced in this aspect!
pointcut setILoggableLogger(Logger pLogger):
call(void ILoggable.setLogger(Logger)) && args(pLogger);
before (Logger pLogger): setILoggableLogger(pLogger)
{
//This sets the Logger for the ForceLogging aspect.
setLogger(pLogger);
}
//Pointcut that joins on the initLogging() method
//that was statically introduced in this aspect!
pointcut initILoggableLogger(Logger pLogger, Handler pHandler):
call(void ILoggable.initLogging(Logger, Handler)) &&
args(pLogger, pHandler);
before(Logger pLogger, Handler pHandler): initILoggableLogger(pLogger, pHandler)
{
setLogger(pLogger);
fLogger.addHandler(pHandler);
}
Please see ForceLogging.java aspect to see the more
detailed Javadoc for these pointcuts and their related advice.
TestCodingStandards shows how to opt-in to logging and
demonstrates how the EnforceCodingStandards aspect
works.
Conclusion
Coding standards are a critical tool to ensure that
programmers follow coding best-practices. They do this
by guaranteeing consistency and allowing peers to focus on
more pertinent issues such as class structure, implementation,
and purpose. Aspects are a very handy technology to give
programmers more immediate, uniform and ego-less feedback than
peer code reviews.
|