Dr. Dobb's Journal August 2003
Object-oriented programming (OOP) has gained success as a means of handling software complexity through hierarchical structures of carefully designed object types (classes). Each class encapsulates a set of reusable business functions and reuses functions provided by its base class or classes. Reuse of object-oriented code eliminates much of the bug-prone code repeated throughout complex software projects.
However, an object can only reuse code along its inheritance path. OOP languages such as Java and C# only support single inheritance. In Java applications, the inheritance hierarchy must be developed around the core business functions to allow maximum reuse of complex business logic code. Secondary business- and development-process concerns that crosscut through the inheritance treesecurity, logging, resource management, error management, and other design constraints, properties, or behaviors that affect the whole systemhave to be repeated in every affected object. The class diagram in the "Runtime View" of Figure 1 illustrates how crosscutting concerns are tangled within a main class structure. As software projects grow more complex, it is hard to keep repetitive code segments up-to-date.
While research has developed ways to address this crosscutting-code-reuse problem, we still need backward-compatible ways to protect huge investments in existing software and developer training. Many important enterprise Java technologies such as the Enterprise JavaBeans (EJB) and the Java Data Objects (JDO), are developed to deal with crosscutting problems in Java-based complex enterprise systems. Currently, there are four approaches to this problem, each based on OOP systems.
In the traditional object-oriented world, you can mix-in different branches of inheritance trees through multiple inheritance. While OOP languages such as Smalltalk and C++ support multiple inheritance, the technique is hard to use and requires significant up-front design efforts. OOP languages such as Java and C#, on the other hand, reduce complexity (and potential abuse) by eliminating multiple inheritance altogether.
In addition, multiple inheritance does not solve all crosscutting mix-in problems. When mixing several concerns, the type system often finds ambiguous or even unsolvable cases. For example, the problem of "inheritance anomaly" proves that mixing synchronization concerns and other concerns using inheritance is simply undoable. In short, multiple inheritance has proven too complex to be useful.
Aspect-oriented programming (AOP) addresses crosscutting complexity and associated design challenges. AOP provides segments of reusable code called "aspects" that are modularized in centralized AOP components. Aspects can be maintained from a single point and are woven into an object's code at compile time. Aspects provide modularity for otherwise unrelated classes across an application. AOP adds additional dimensions of code reuse on top of OOP class hierarchies. This lets you design classes that focus on core business logic, then mix-in other concerns later without duplicated code. AOP is not a replacement for OOP, but a natural extension that adds more power to it. Figure 1 illustrates AOP development. Tools exist to make AOP available to popular OOP languages. For instance, AspectJ (http://www.eclipse.org/aspectj/), originally developed by Xerox PARC but recently transferred to the open-source Eclipse Technology Project, is the leading Java effort using this approach.
AspectJ defines language constructs that describe how aspects are woven into the existing class structure. Basic AOP conceptual elements defined in AspectJ include:
AspectJ provides a compiler that identifies the joinpoints in applications and inserts pointcut and advice code directly into Java class files. AspectJ can also act as a precompiler and mix concerns into Java source-code files. In either case, AspectJ generates fully Java 2 compatible classes.
Although generic AOP languages can be powerful, they have steep learning curves. Another major limitation of AspectJ is the inflexible compile-time aspect weaving. Finally, the AspectJ approach to AOP has been patented by Xerox, leaving the future of AOP uncertain (see U.S. Patent 6,467,086, "Aspect-oriented programming," granted to G.J. Kiczales, et al., from the Xerox Corporation on October 15, 2002).
A third way to address crosscutting-code-reuse problems is to insert aspect code directly into class bytecodes at run time. Bytecode manipulation is normally done by a special framework, such as Bob Lee's jAdvise framework (http://crazybob.org/downloads .htm) or a special execution container, such as Java Aspect Components (JAC, http://jac.aopsys.com/), developed by Renaud Pawlak. Both tools are released under open-source licenses. This approach lets you mix-in crosscutting concerns at any time to any joinpoint in the execution flow. Compared with the compile-time AOP approach, dynamic aspect weaving is simpler to use and far more flexible. For example, we can change the AOP behavior through run-time configuration files or a management interface without rebuilding the application.
However, neither jAdvise nor JAC are currently standards, and both require you to learn underlying concepts and APIs. Debugging dynamically instrumented bytecode with nonstandard tools is difficult. It is not always possible to use third-party frameworks or containers in custom applications. Also, programming techniques to implement dynamic bytecode aspect weavers are still young. At this writing, for instance, jAdvise is still in early development and only supports method invocation joinpoints.
The fourth approach involves design patterns. While AOP still requires new languages or third-party frameworks, design patterns are mainstream. In terms of crosscutting concerns, the most important design pattern is the Interceptor pattern. In Pattern-Oriented Software Architecture (John Wiley & Sons, 2000), Douglas Schmidt defined the Interceptor pattern as a technique that "allows services to be added transparently to a framework and triggered automatically when certain events occur." The Interceptor pattern is easy to use, native to Java, and supports aspect weaving at run time. JBoss and XDoclet lead developer Rickard Vberg (http://roller.anthonyeden.com/page/rickard/) promotes dynamic aspect weaving based on the Interceptor approach. An advantage of this approach is that it only relies on standard Java language features and does not modify bytecode. (Bytecode enhancers have been philosophically opposed by many programmers because of their complexity and potential for abuse.)
Although interceptors provide the shortest migration path to add crosscutting concerns to existing software, there are limitations when compared with the pure AOP approach. Not all AOP features can be easily implemented using run-time interceptors. For instance, only method invocation joinpoints are directly supported by Java dynamic-proxy-based interceptors. However, method calls are the most popular joinpoints because they preserve the encapsulation of the objects in a safe manner. As Renaud Pawlak revealed in his Ph.D. thesis, unlike other joinpoints, method-call aspect insertions are composable.
For most projects, we believe method interception based on common design patterns are adequate alternatives to AOP compilers or bytecode enhancers. To illustrate, we examine how the Java language's reflection and dynamic-proxy features can be used to implement Decorator and Interceptor patterns. Using an example of smart JavaBeans, we demonstrate how to use the Interceptor pattern to implement some of the most important AOP features and outline the limitations.
Simple JavaBeans are easy to write and maintain. Java application servers can access JavaBeans conveniently from JavaServer Pages (JSP), using bean tags and/or script variables. (For information on how JavaBeans work in server-side Java, see the accompanying text box entitled "How JavaBeans Work in JavaServer Pages.")
But things get complicated when you need finer controls over bean behavior. For example, you might need to log the bean actions and/or authenticate users when certain business functions are accessed. Those concerns fall outside the bean business logic and are normally not implemented by individual JavaBeans providers. They crosscut many different bean classes. Crosscutting concerns such as logging and security are typical in enterprise Java applications. In fact, a major design goal for the EJB technology is to provide software "containers" that manage those concerns for business objects. While JBoss is experimenting with interceptor-based AOP implementations, our approach of adding a transparent layer on top of simple JavaBeans provides a lightweight alternative to full EJB containers.
JavaBean EchoBean (Listing One) takes in user input strings from a JSP front end and sets them to property message through a setter method. It can echo the message property back when the JSP page requests it through the getter method. EchoBean also has a method to report its copyright information. Our simple crosscutting concern is to log the timestamp and arguments for each bean method call.
Before and after each joinpoint, you need to process the policy and decide what aspect code to execute. This is done through a policy-handler object, which is responsible for parsing the policy configuration file, identifying pointcuts, collecting context information, and carrying out the advice. The policy handler can be reset at run time by other components in the application if the policy changes.
Listing Two puts all policy-handling code into the Policy class. A Policy object is constructed from the XML policy configure file. Method execute() scans the policy tree object to identify pointcuts and decides the appropriate advice action based on the bean information and run-time arguments. In our log action example, it logs the timestamp, bean class name, method name, and method arguments for each bean method access. The example code is easily expandable to handle more complex cases such as authentication.
Listing Three is the XML policy file. A policy element can contain multiple bean elements, which specify the class names of JavaBeans that need to be logged. A bean element can contain multiple method elements, which specify the names of the bean methods to be logged (joinpoints) and whether the logging should be done before or after the method call.
JavaBean properties are accessed from the <jsp:getProperty> and <jsp:setProperty> tags, which call the bean access methods on the JSP page's behalf. The simplest idea is just to decorate the two tag handlers and add the policy handler before and after they dispatch getter/setter method calls to the bean instances. The complete source code (available electronically; see "Resource Center," page 5) provides an example implementation of this approach. Our tag handlers are generic since they use reflection to map arbitrary properties to bean getter/setter methods.
However, this Decorator pattern approach has a serious flaw: It does not check possible bean method calls that do not go through get/setProperty tags. The cause is that the decorators are only partly transparent through the bean tags. What you really need is a generic approach to transparently mix-in aspects to any object or method. The tool that comes to our rescue is the Java Dynamic Proxy.
The dynamic-proxy API was introduced as part of the Java reflection system in JDK 1.3. Dynamic proxies can implement any number of interfaces and redirect all method calls to an invocation handler that can act on the method call or dispatch it to another object for handling. Dynamic proxies delegate method calls to other object instances at run time. To applications, dynamic-proxy objects can be cast to any interface type implemented. That lets you use dynamic proxies to mix aspects into object methods while still preserving the object type (interface). Type transparency is important for large applications, where OOP operations such as type polymorphism and dynamic type casting are frequently used.
Class BeanProxy (Listing Four) is a generic proxy for any bean object. A new bean and its proxy instances are created by passing the bean class name and Policy object to static method BeanProxy.create(). When applications call any method that appears in the interfaces the proxy implements, the JVM run time passes the call to the proxy object's invoke() method with the appropriate method name and run-time arguments. We place the policy handler inside our own implementation of the proxy invoke() method to mix-in needed crosscutting code. The method invoke() is a single point for all possible method-call joinpoints. The dynamic proxy can guarantee to call policy handlers before and after all joinpoints.
In general, to refactor classes to use dynamic-proxy interceptors, you only need to alter the object creation code because the dynamic proxy is transparent to all other code in the application. You could use a customized bean instantiation tag to create beans and then wrap them with dynamic proxies in your policy control beans example JSP page. All the following get/setProperty tags and script variables access the proxies as if they were ordinary beans.
Dynamic proxies can be used to weave crosscutting concerns into simple JavaBeans. Since dynamic proxies work with interfaces to preserve types, we have to create an interface for each bean class. In our example, interface EchoBeanInt declares methods in bean class EchoBean. In the JSP application, you can replace the default bean instantiation tag <jsp:useBean> with a new dynamic-proxy-aware <ddj:useBeanProxy> custom tag. The new <ddj:useBeanProxy> tag handler needs to create transparent proxies together with bean objects through the BeanProxy.create() method. The tag handler then stores the proxy into the PageContext with the appropriate scope and exports it to a script variable. When the bean instance is used again in later JSP pages, the <ddj:useBeanProxy> tag or direct script code accessing PageContext or HttpSession attributes fetches the same proxy. Listing Five is an implementation of the new <ddj:useBeanProxy> tag handler class UseBeanProxy.
Compared to AOP approaches such as AspectJ, jAdvise, and JAC, dynamic-proxy-based interceptors do have shortcomings:
Still, we have found these limitations acceptable in small-scale projects.
You often have to balance the relative merits of different technologies and choose the one best suited for particular development needs. We believe that the dynamic-proxy solution provides a convenient method for integrating aspects into many existing applications.
Thanks to Renaud Pawlak, Ted Neward, Robert Lee, and Rickard Vberg.
DDJ
package Beans;
public class EchoBean implements EchoBeanIface {
private String _message;
public EchoBean () {
_message = "not initialized";
}
public String getMessage() {
return _message;
}
public void setMessage(String message) {
_message = message;
}
public String copyrightNotice() {
return "Copyright Michael Yuan and Norman Richards\n";
}
}
package Policy;
import java.io.*;
import java.util.*;
import org.w3c.dom.*;
import javax.xml.parsers.*;
// Dynamic policy handler.
public class Policy {
Document policy = null;
// Parse policy config file and build a DOM tree.
public Policy (String path) throws Exception {
File policyFile = new File( path );
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
policy = docBuilder.parse(policyFile);
return;
}
public void execute(String className, String methodName, String position,
String action, Object [] args) throws Exception {
if ( policy == null ) {
throw new Exception("policy error!");
}
Element bean = null, method = null;
// Get all "bean" elements
NodeList beans = policy.getElementsByTagName("bean");
// Iterate and find matches for input parameter "className".
for (int i=0; i<beans.getLength(); i++) {
Element e = (Element) beans.item(i);
if ( className.equals(e.getAttribute("className")) ) {
bean = e;
break;
}
}
if ( bean == null ) {
return;
}
// Get all "method" elements
NodeList methods = bean.getElementsByTagName("method");
// Iterate & find matches for input parameters "methodName" & "position".
for (int i=0; i<methods.getLength(); i++) {
Element e = (Element) methods.item(i);
if ( methodName.equals(e.getAttribute("name")) &&
position.equals(e.getAttribute("position")) ) {
method = e;
break;
}
}
if ( method == null ) {
return;
}
// Act according to "action" attribute.
if ( action.equals(method.getAttribute("action")) ) {
String output = (new Date()).toString() + " called " +
className + "." + methodName + " arguments ";
if ( args == null ) {
output = output + "NULL";
} else {
for (int i=0; i<args.length; i++) {
output = output + args[i].toString();
}
}
System.out.println(output);
}
return;
}
}
Listing Three
<policy>
<bean className="Beans.EchoBean">
<method name="setMessage" action="log" position="before"/>
<method name="getMessage" action="log" position="after"/>
<method name="copyrightNotice" action="log" position="after"/>
</bean>
</policy>
package CustomTags.DynamicProxy;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import Policy.*;
// Transparent proxy around any JavaBean to handle policies.
public class BeanProxy implements InvocationHandler {
Object[] _targets;
String _beanClassName;
Policy _policy;
public BeanProxy(Object[] targets, String beanClassName, Policy policy) {
_targets = targets;
_beanClassName = beanClassName;
_policy = policy;
}
// New JavaBean and proxy instance.
public static Object create (String beanClassName,
Policy policy) throws Exception {
Class beanClass = Class.forName( beanClassName );
Class beanIfaceClass = Class.forName( beanClassName + "Iface" );
Object beanObj = beanClass.newInstance();
Class [] ifaces = {beanIfaceClass};
Object [] targets = {beanObj};
return Proxy.newProxyInstance(BeanProxy.class.getClassLoader(), ifaces,
new BeanProxy(targets, beanClassName, policy));
}
// All bean methods are invoked through this method.
public Object invoke(Object proxy, Method method, Object[] args)
throws Exception {
Object result;
_policy.execute(_beanClassName, method.getName(), "before", "log", args);
result = method.invoke(_targets[0], args);
_policy.execute(_beanClassName, method.getName(), "after", "log", args);
return result;
}
}
package CustomTags.DynamicProxy;
import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;
import CustomTags.*;
import Policy.*;
// Custom tag handler to initialize a JavaBean and its dynamic proxy.
public class UseBeanProxy extends TagSupport {
String _beanName;
String _beanClassName;
int _scope;
public void setId(String id) {
_beanName = id;
}
public void setBeanClass(String beanClassName) {
_beanClassName = beanClassName;
}
public void setScope(String scope) {
try {
_scope = Utils.getScope( scope );
} catch (Exception e) {
System.out.print(e.getMessage());
}
}
public int doStartTag () {
try {
// If the named bean instance is already available, do nothing.
if ( pageContext.getAttribute(_beanName, _scope) != null ) {
return SKIP_BODY;
}
// Get policy object.
Policy policy = (Policy) pageContext.getAttribute("Policy",
PageContext.APPLICATION_SCOPE);
// If this is the first time accessing policy object in this
// application, create one and store in PageContext.
if ( policy == null ) {
String path = pageContext.getServletContext().getRealPath("/");
policy = new Policy(path + "/WEB-INF/conf/PolicyConf.xml");
pageContext.setAttribute("Policy", policy,
PageContext.APPLICATION_SCOPE);
}
// Bean instance wrapped in a dynamic proxy.
Object obj = BeanProxy.create( _beanClassName, policy );
// Put the proxy instance into PageContext.
pageContext.setAttribute(_beanName, obj, _scope);
} catch (Exception e) {
System.out.print(e.getMessage());
}
return SKIP_BODY;
}
}