Using AOP to maintain legacy Java applications

Source: Internet
Author: User
Tags aop execution include interface join reflection tostring stringbuffer
Programs to handle complex and unfamiliar Java code technology
Level: Intermediate


Abhijit Belapurkar (abhijit_belapurkar@infosys.com)
Advanced Technical Architect, Infosys Technologies Limited
March 2004

If you have ever taken over and must maintain a java-based application, this article is for you. Author Abhijit Belapurkar will show you how to use aspect-oriented programming (aspect-oriented PROGRAMMING,AOP) to gain unprecedented insights into even the most opaque legacy applications.
Software systems usually start with a limited set of well understood requirements. However, as most successful systems evolve, they take on more and more requirements, embodied in countless functional and non-functional aspects. In an enterprise environment, you can end up adding many third-party libraries and frameworks to this messy modular mix, all interacting with each other and working under the surface of the system's day-to-day work. In fact, in less than a few years, a system that initially has a simple, manageable set of requirements becomes a behemoth: unmanageable and clumsy code.

As a result, Java developers who enter this environment have a new task of day-to-day maintenance and improvement. If you are the developer, your first task is to have a deep understanding of the structure of the system. Understanding the structure will be the key to enhancing the system and diagnosing the problems that will inevitably occur. Of course, exploring any unknown system for the first time is easier said than done. In some cases, you can consult the original developer, but not in other cases. But even if you can find the original development team, some systems will be too large to be able to get to know and understand it without mechanical help.

While there are a number of tools available to help you understand complex programs (see Resources), most tools are expensive, time-consuming to learn, and limited in functionality (that is, if the tool doesn't meet your needs, you'll have no access). In this article, I will suggest an alternative approach. Aspect-oriented programming is a mature programming paradigm that can be applied to a wide range of programming scenarios, including the understanding and maintenance of legacy applications.

Note that this article assumes that you are generally familiar with AOP under AspectJ, especially the static and dynamic crosscutting techniques of AspectJ. Although I will provide a brief overview of AOP crosscutting in the next section, you should refer to the resources for more information.

General overview
Java-based AOP uses a flexible and rich expression language that you can use to decompose complex applications in almost unlimited ways. Java-based AOP syntax is similar to the Java language, and you should be able to master it easily. Once mastered, AOP is a programming technique with many applications. In addition to understanding the internal details of legacy systems, you can use AOP to refactor and enhance such systems without coercion. Although this article will fully use AspectJ, most of the techniques discussed here can be ported to other popular java-based AOP implementations, such as Aspectwerkz and JBOSSAOP (see Resources).

About crosscutting
Any application consists of multiple functional and systematic concerns (concern). Functional concerns are associated with the day-to-day use of an application, while systematic concerns are related to the overall health and maintenance of the system. For example, the functional concerns of a banking application include account maintenance and allow lending/lending operations, and its systemic concerns include security, transaction, performance, and audit logging. Even if you use the best programming methodology to develop your application, you will eventually find that its functional and systematic concerns are mixed together in the form of modules across multiple applications.

Crosscutting is an AOP technique used to ensure that independent concerns remain modular while still flexible enough to be applied at different points throughout the application. Crosscutting consists of both static and dynamic categories. Dynamic crosscutting is embodied in changing the execution behavior of an object by weaving (weave in) new behavior at a particular point of interest. Static crosscutting allows us to directly alter the structure of an object by injecting (inject in) additional methods and/or attributes.

The syntax for static crosscutting is very different from dynamic crosscutting. The following terms apply to dynamic crosscutting:


A join point is a specific execution point in a Java program, such as a method in a class.


A pointcut (pointcut) is a language-specific structure that represents or captures a particular connection point.


Notification (advice) is a piece of code (usually a crosscutting feature) to be executed when a particular pointcut is reached.


Aspect (aspect) is a structure that defines pointcuts and notifications and the mappings between them. Aspect is used by the AOP compiler to weave additional functionality into specific execution points in existing objects.

All of the code demonstrations in this article will take advantage of dynamic crosscutting. See Resources for more information on static crosscutting.

AOP under the AspectJ
To learn the examples in this article, you should be familiar with the following characteristics of AOP specific to AspectJ.


AspectJ provides a compiler/byte code AJC called the compiler that compiles AspectJ and Java language files. AJC Interweave The aspect as needed to produce a. class file that is compatible with any Java virtual machine (1.1 or later).


AspectJ supports the aspect that these aspects specify that a particular connection point should never be reached. If the AJC process determines that this is not the case, it emits a compile-time warning or error (depending on that aspect).

Application and System analysis
In the following sections, you will learn about two different application and system analysis mechanisms that use AOP. The first mechanism I call static analysis requires you to do the following things:

Check out the entire application code base in the local area from CVS (or any other code versioning system that you use).


Modify the makefile (build file) to use the AspectJ compiler (AJC).


Include an aspect (aspect) class in the appropriate location.


Perform a complete system generation process.

The second mechanism, which I call dynamic analysis, requires that you not only build systems in your local area, but also run specific use cases of the system, and collect information from Run-time reflection into the running system. In the following sections, I will discuss each mechanism in detail and use code examples to illustrate key concepts.

Static analysis
I'll look at a number of techniques for performing static analysis on legacy applications and apply them to three common maintenance scenarios, as follows:

Evaluate the impact of interface changes.
Identify dead or unreachable code.
Create loose coupling.

Let's get started here!

Evaluate the impact of interface changes
An object-oriented legacy application or system should contain a number of modules that expose a well-defined interface to the client. Suppose you accept a task that consolidates the new requirements, and the task needs to change the existing interface. Because you are unfamiliar with code, the first challenge you have is to figure out how changing this interface will affect your system's clients. For the convenience of illustrative examples here, the implicit effect here is simply to change each client so that the method invocation conforms to the changed signature. Therefore, it may be helpful to know all the clients of the interface to determine the best way to implement the new requirements and to determine whether the requirement is worthwhile for a given system. I will first use static analysis to determine the client of the interface, and then evaluate the impact of interface changes on the system.

This technique is based on a number of aspects that emit a compile-time warning when a particular connection point is found to be reached. In this case, you will write a connection point to catch calls to all methods for that particular interface. You must first extract the application code base in the local area, modify the makefile so that it uses the AspectJ compiler and include the aspect class in the appropriate location, and then run the full build file for the system. If you correctly capture a connection point, you expect to see a compile-time warning about calling to a target method in the code base.

The compile-time aspect, shown in Listing 1, detects and displays all classes that invoke the interface Com.infosys.setl.apps.AppA.InterfaceA (and its implementations) while executing (one or more) the implementation (Implementor) class itself. For example INTERFACEA, its implementation-class ClassA, and the caller class CLASSB, this aspect generates a compile-time warning about calling A.methoda () in CLASSB.

Listing 1. Classes that use Interfacea

Package Com.infosys.setl.apps.AppA;
public interface Interfacea
{
Public String MethodA ();
public int methodb ();
public void methodc ();
}

Package Com.infosys.setl.apps.AppA;
public class ClassA implements Interfacea
{
Public String MethodA ()
{
Return "Hello, world!";
}
public int MethodB ()
{
return 1;
}
public void Methodc ()
{
System.out.println ("Hello, world!");
}
}

Package Com.infosys.setl.apps.AppB;
Import com.infosys.setl.apps.appa.*;
public class ClassB
{
public static void Main (string[] args)
{
Try
{
Interfacea A =
(Interfacea) Class.forName ("Com.infosys.setl.apps.AppA.ClassA"). newinstance ();
System.out.println (A.methoda ());
}
catch (Exception e)
{
E.printstacktrace ();
}
}
}

Package com.infosys.setl.aspects;
Public Aspect Interfacecallersaspect
{
Pointcut Callermethoda (): Call (* Com.infosys.setl.apps.AppA.InterfaceA.methodA (..)) &&
!within (com.infosys.setl.apps.appa.interfacea+);
Pointcut Callermethodb (): Call (* Com.infosys.setl.apps.AppA.InterfaceA.methodB (..)) &&
!within (com.infosys.setl.apps.appa.interfacea+);
Pointcut callermethodc (): Call (* Com.infosys.setl.apps.AppA.InterfaceA.methodC (..)) &&
!within (com.infosys.setl.apps.appa.interfacea+);
Declare Warning:callermethoda (): "Call to Interfacea.methoda";
Declare Warning:callermethodb (): "Call to Interfacea.methodb";
Declare WARNING:CALLERMETHODC (): "Call to Interfacea.methodc";
}



The impact of interface changes on the implementing class can be determined by the aspect shown in Listing 2. For the example class in the aforementioned list, this aspect generates a compile-time warning for ClassA.

Listing 2. Classes that implement Interfacea

Package com.infosys.setl.aspects;
Public Aspect Interfaceimplaspect
{
Pointcut Impls (): Staticinitialization (com.infosys.setl.apps.appa.interfacea+) &&
! (Within (Com.infosys.setl.apps.AppA.InterfaceA));
Declare Warning:impls (): "Interfacea implementer";
}



Identify dead code
The system becomes fragmented as the enhancement request joins intermittently. Whenever you make changes to a part of a system or application, it is important to be aware of the impact of such changes on other parts. For example, the code in refactoring module A can cause code somewhere else (whether a method in a class or the entire class itself) to become unreachable (or dead) because the control/data stream has been updated and bypassed. Ignoring dead code may eventually result in performance problems due to the excessive number of unused code in the system.

Fortunately, the same techniques used to determine the impact of code enhancements (as shown in the previous section) can also be applied to identifying dead or unreachable code. Just as the compile-time aspect shown in Listing 1 can be used to detect all methods in the invoked interface, they can also point to methods that are not part of the interface. Any method that does not generate compile-time warnings in this interface can be considered dead and can be safely deleted.

Creating Loose coupling
Sometimes you may have to maintain a legacy application that is developed using hard-coded calls or specific components rather than interfaces. Modifying such a system, such as adding new components or replacing components, can be cumbersome and challenging. Fortunately, AOP can be used to isolate specific components from the system and replace them with generic, interface-based components. This ensures that the actual implementation can be inserted dynamically.

Consider the example class shown in Listing 3 of code. The ClassA MethodA method has a hard-coded form of logging, where calls are made directly against the System.out.

Listing 3. Class with hard-coded logging calls

Package Com.infosys.setl.apps.AppA;
public class ClassA
{
public int MethodA (int x)
{
System.out.println ("About to double!");
x*=2;
System.out.println ("Have doubled!");
return x;
}

public static void Main (String args[])
{
ClassA a = new ClassA ();
int ret = A.methoda (2);
}
}



Manually replacing an existing log call with a call to a generic logger interface is obviously cumbersome. A better choice is to find and replace all such calls using the aspects shown in Listing 4. The generic logger interface can then be provided through Loggerinterface, and the factory class Loggerfactory can be used to obtain a specific logger instance (Setlogger in Code Listing 4).

Listing 4. Replace a generic interface for hard coded calls

Package com.infosys.setl.utils;
public interface Loggerinterface
{
public void log (String logmsg, Object target);
}

Package com.infosys.setl.utils;
public class Loggerfactory
{
private static loggerfactory _instance = null;
Private Loggerfactory () {}
public static synchronized Loggerfactory getinstance ()
{
if (_instance = null)
_instance = new Loggerfactory ();
return _instance;
}
Public Loggerinterface GetLogger ()
{
Loggerinterface L = null;
Try
{
L = (loggerinterface) class.forname ("Com.infosys.setl.utils.SETLogger"). newinstance ();
}
catch (Exception e)
{
E.printstacktrace ();
}
Finally
{
return l;
}
}
}

Package com.infosys.setl.utils;
public class Setlogger implements Loggerinterface
{
public void log (String logmsg, Object target)
{
System.out.println (LOGMSG);
}
}

Package com.infosys.setl.aspects;
Import com.infosys.setl.utils.*;
Public Aspect Unplugaspect
{
void Around (String logmsg, Object source):
Call (void Java.io.PrintStream.println (String)) && within (Com.infosys.setl.apps.AppA.ClassA) &&
!within (com.infosys.setl.aspects. *) && args (logmsg) && target (source)
{
Logger.log (logmsg, source);
}
Private Loggerinterface logger = Loggerfactory.getinstance (). GetLogger ();
}



Dynamic analysis
This section will look at several dynamic analysis techniques and then apply them to familiar maintenance scenarios as well. The scenarios that will be resolved using dynamic analysis are as follows:

Generate a dynamic call graph.
Evaluate the impact of an exception on a system.
Customizing the system based on specific conditions.

Recall that dynamic AOP analysis requires that the application code base is checked out to the local zone, then the build file is modified to use the AspectJ compiler and includes the aspect class in the appropriate place, then the entire system is built, and then the use case that the system is targeting is run. Assuming that you have correctly specified a connection point of interest, you will be able to collect rich information into the running application through reflection.

Generate a dynamic call graph
A single use case traverses multiple program modules unchanged. It is often used to form a mental representation of this traversal path by running the code's execution sequence in your mind completely. Obviously, as the traversal path becomes longer, it's more difficult to remember all this information, so it's not practical for larger systems. In this section, you will learn how to generate a dynamic call graph, which is a nested trace trajectory that depicts the process by which the stack frame presses and pops up on the execution stack as the use case runs from the beginning to the end.

In Listing 5, you can see how to use a compile-time aspect to dynamically generate a call graph that reflects the execution process of a use case.

Listing 5. Generate control Flow Diagram

Package Com.infosys.abhi;
public class ClassA
{
public void MethodA (int x)
{
x*=2;
Com.infosys.abhi.ClassB B = new Com.infosys.abhi.ClassB ();
B.methodb (x);
}
}

Package Com.infosys.abhi;
public class ClassB
{
public void MethodB (int x)
{
x*=2;
Com.infosys.bela.ClassC C = new Com.infosys.bela.ClassC ();
C.METHODC (x);
}
}

Package Com.infosys.bela;
public class CLASSC
{
public void methodc (int x)
{
x*=2;
Com.infosys.bela.ClassD d = new Com.infosys.bela.ClassD ();
D.methodd (x);
}
}

Package Com.infosys.bela;
public class CLASSD
{
public void methodd (int x)
{
x*=2;
}
}

Package com.infosys.setl.aspects;
Public Aspect Logstackaspect
{
private int calldepth=0;
Pointcut cgraph ():!within (com.infosys.setl.aspects. *) && Execution (* com.infosys. *.*(..));
Object around (): Cgraph ()
{
calldepth++;
LogEntry (Thisjoin point, calldepth);
Object result = proceed ();
calldepth--;
Logexit (Thisjoin point, calldepth);
return result;
}

void LogEntry (join point JP, int calldepth)
{
StringBuffer Msgbuff = new StringBuffer ();
while (Calldepth >0)
{
Msgbuff.append ("");
calldepth--;
}
Msgbuff.append ("->"). Append (Jp.tostring ());
Log (msgbuff.tostring ());
}

void Logexit (join point JP, int calldepth)
{
StringBuffer Msgbuff = new StringBuffer ();
while (Calldepth >0)
{
Msgbuff.append ("");
calldepth--;
}
Msgbuff.append ("<-"). Append (Jp.tostring ());
Log (msgbuff.tostring ());
}

void log (String msg)
{
SYSTEM.OUT.PRINTLN (msg);
}
}

Output:
=======
->execution (void Com.infosys.abhi.ClassA.methodA (int))
->execution (void Com.infosys.abhi.ClassB.methodB (int))
->execution (void Com.infosys.bela.ClassC.methodC (int))
->execution (void Com.infosys.bela.ClassD.methodD (int))
<-execution (void Com.infosys.bela.ClassD.methodD (int))
<-execution (void Com.infosys.bela.ClassC.methodC (int))
<-execution (void Com.infosys.abhi.ClassB.methodB (int))
<-execution (void Com.infosys.abhi.ClassA.methodA (int))



AspectJ also provides a structure called Cflowbelow, which uses a pointcut to indicate the connection point (execution point) after the control flow of each selected connection point. By using this structure, you can intercept the call graph to any depth. This is useful for situations where the control flow runs a larger loop in its entirety, causing the same call graph output to repeat over and over again (which in turn causes the size of the graph to increase, thereby reducing its applicability).

Assess the impact of an exception
Typically, you need to debug an exception that is generated in a production environment. Such exceptions can cause undesirable effects, such as dumping stack traces onto the user interface or not releasing contention resources such as shared locks.

While assessing the impact of an exception on the overall system is important, it is difficult to simulate some exceptions in the development environment. This may be determined by the nature of the exception, such as a network exception, or because the development environment is not an exact copy of the production environment. For example, an exception that occurs due to database destruction in a build environment can be difficult to simulate without knowing exactly where the damage occurred. Capturing a snapshot of a production database for transfer to a development server may also be impossible.

In addition to simulating exceptions to find out where the problem exists in your application, you want to be able to test the code fix to make sure it handles the problem correctly. This is problematic unless you can generate the exception in the patch code and observe how it is handled. In this section, you will see how to use AOP technology to simulate the process of throwing an exception because of the method that invokes an object without having to recreate exactly the run-time condition that caused the exception to actually be thrown.

Consider the example class Com.infosys.setl.apps.AppA.ClassA shown in Listing 6. Calls to the MethodA () of this class may cause a java.io.IOException to be thrown in some circumstances. What we want to know is the behavior of the caller of the MethodA () (Com.infosys.setl.apps.AppB.ClassB) (the class shown in the same manifest) when this exception is thrown. However, you do not want to consume time and resources to establish conditions that produce a specific exception.

To solve this problem, you can use the aspect genexception Pointcut Genexceptiona to interrupt the execution of MethodA (), causing a java.io.IOException to be thrown at run time. You can then test whether the application (represented as CLASSB) can handle the exception as specified, as shown in Listing 6. (You can, of course, modify the notification to "after" notification, which can be executed after the execution of MethodA (), as shown in Pointcut Genexceptionaftera. )

Note In a real-world scenario, ClassA may be a third-party library such as a JDBC driver.

Listing 6. To generate an exception from a running code

Package Com.infosys.abhi;
Import java.io.*;
public class ClassA
{
public void MethodA () throws IOException
{
System.out.println ("Hello, world!");
}
}

Package Com.infosys.bela;
public class ClassB
{
public static void Main (string[] args)
{
Try
{
Com.infosys.abhi.ClassA a = new Com.infosys.abhi.ClassA ();
A.methoda ();
Com.infosys.abhi.ClassC C = new Com.infosys.abhi.ClassC ();
System.out.println (C.METHODC ());
}
catch (Java.io.IOException e)
{
E.printstacktrace ();
}
catch (Exception e)
{
E.printstacktrace ();
}
}
}

Package Com.infosys.abhi;
public class CLASSC
{
Public String METHODC ()
{
System.out.println ("Hello, world!");
Return "Hi, world!";
}
}

Package com.infosys.setl.aspects;
Public Aspect genexception
{
Pointcut Genexceptiona ():
Execution (public void Com.infosys.abhi.ClassA.methodA () throws java.io.IOException);
Pointcut Genexceptionc ():
Call (void Java.io.PrintStream.println (String)) &&
Withincode (Public String Com.infosys.abhi.ClassC.methodC ());
Pointcut Genexceptionaftera ():
Call (public void Com.infosys.abhi.ClassA.methodA () throws java.io.IOException);

void around () throws Java.io.IOException:genExceptionA ()
{
Java.io.IOException e = new Java.io.IOException ();
Throw e;
}

After () throws Java.io.IOException:genExceptionAfterA ()
{
Java.io.IOException e = new Java.io.IOException ();
Throw e;
}

After () throws Java.lang.OutOfMemoryError:genExceptionC ()
{
Java.lang.OutOfMemoryError e = new Java.lang.OutOfMemoryError ();
Throw e;
}
}



In a similar scenario, you might encounter an application that throws unchecked exceptions (such as subclasses of java.lang.RuntimeException such as NullPointerException) or an error (such as OutOfMemoryError Java.lang.Error of the subclass). Both types of exceptions are difficult to simulate in the development environment. Instead, you can use the Pointcut genexceptionc along with the corresponding after notification (as shown above) to cause the code to run a outofmemory error, and then Invoke System.out.println () in CLASSC's METHODC ().

Customizing systems based on specific conditions
Typically, AOP is ideal in a focus environment that cuts across multiple system modules. However, AOP can also be useful for focusing on specific parts of the system. For example, you might want to make some very specific changes to your system, such as keeping your customers happy or reflecting a change in your local regulations. Some changes may even be temporary (that is, they must be removed from the system after a specific period of time).

In such cases, you may find that for these reasons, branching the code to make related enhancements directly in your code may not be possible:

You may end up having to manage multiple code branches with different versions in CVS. This is a management issue that increases the likelihood of error. Even assuming that all the enhancements to the request are beneficial features that have a positive impact on the overall product orientation, introducing these features first will give the development team a chance to try, without having to submit the changes to CVS.


If a temporary feature must be removed from the system, if the attribute is introduced directly into the code, the required regression test will be resource-intensive and error-prone. On the other hand, if the attribute is introduced through AOP, the deletion process simply compiles the system after excluding the aspect from the makefile (build file).

Fortunately, it is easy to weave this custom feature through the aspect. The weaving process can be done at compile time (using AspectJ) or at load time (using Aspectwerkz). In the design context, it is important to remember that an AOP implementation may have inherent limitations on environment disclosure (for example, AspectJ does not allow local variables to expose methods to the notification code by executing the environment). In general, however, using aspects to customize the system will bring a cleaner implementation that separates the quarantined concerns from the baseline version of the system code.

Conclusion
In this article, you learned how to use aspect-oriented programming to understand and non-mandatory maintain large, complex legacy systems. This paper addresses many common maintenance scenarios with the power of two analytical techniques and dynamic crosscutting, emphasizing error diagnosis and non-mandatory enhancement of existing code. If you are responsible for the day-to-day maintenance of a legacy application, all the techniques shown here should prove invaluable.

Resources

You can download AspectJ from the AspectJ Project home page.


Get an in-depth introduction to AOP and AspectJ through "using Aspect programming to improve modularity" (developerworks,2002 year January).


Learn more about the most well-known java-based AOP styles through AspectJ and mock object testing flexibility (developerworks,2002 year May).


"AOP solves tightly coupled challenges" (developerworks,2004 February) is a practical introduction to static crosscutting techniques.


Aspectwerkz is a lightweight, open source, java-based AOP implementation.


JBOSSAOP is another competitor in the Java-based AOP world.


Rigi is an interactive visualization tool (developed by a researcher at the University of Victoria, British Columbia, Prov., Canada) designed to help you better understand and document your software.


The Klocwork InSight can be used to extract an accurate graphical view of software design directly from existing source code (c, C + +, and Java code) to fully understand the structure and design of the application.


Hundreds of articles on various aspects of Java programming can be found from the DeveloperWorks Java technology Zone.


Visit Developer Bookstore for a detailed list of technical books, including hundreds of Java-related books.


Also refer to the Java Technology Area Tutorial Homepage for a complete list of free Java-related tutorials from DeveloperWorks.



About the author
Abhijit Belapurkar obtained a bachelor's degree in computer science from the Indian Institute of Technology in Delhi, India. He has nearly 10 years of experience in architecture and security for distributed applications, and has over 5 years of experience building N-tier applications using the Java platform. He is currently a senior technical architect in Infosys Technologies Limited in Bangalore, India.



Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.