I want my AOP!, Part 3

Use AspectJ to modularize crosscutting concerns in real-world problems

A spect-oriented programming (AOP) -- a new programming paradigm -- allows modular implementation of crosscutting concerns. In Part 1 of this three-part series covering AOP, I defined a concern as:

A concern is a particular goal, concept, or area of interest. In technology terms, a typical software system comprises several core and system-level concerns. For example, a credit card processing system's core concern would process payments, while its system-level concerns would handle logging, transaction integrity, authentication, security, performance, and so on. Many such concerns -- known as crosscutting concerns -- tend to affect multiple implementation modules. Using current programming methodologies, crosscutting concerns span over multiple modules, resulting in systems that are harder to design, understand, implement, and evolve.

AOP works in addition to, not as a replacement for, object-oriented programming (OOP). AspectJ, a free AOP implementation for Java, helps create software systems that are easier to implement, understand, and maintain.

Read the whole "I Want My AOP" series:

In Part 1, I introduced basic AOP concepts, while in Part 2 I presented an AspectJ tutorial. Armed with your new AOP and AspectJ knowledge, in this article you'll learn how to modularize crosscutting-concern implementations with real-world examples.

Although I discuss several AspectJ examples, by no means do I imply that such implementations can only work using AspectJ. In general, AOP (and for that matter, any other programming methodology) is not about what but about how. This article's examples demonstrate that implementing software systems with AOP offers crosscutting-concern modularity advantages, including more focused code and maintenance ease.

I keep the examples intentionally simple to focus on AOP's and AspectJ's usages. For example, in the thread-pooling example, I do not implement the pool management's full functionality. However, I do concentrate on how to apply the AOP techniques. Also note, for logging I deliberately use System.out.println() instead of the logging aspect to keep the focus from the main problem to the logging concern.

This article looks at examples from several categories: resource-pool management, policy enforcement, characteristics-based behavior, and flexible access control. I wrap up the article (and the series) by presenting several other ways to use AspectJ in your work.

Note: You can download this article's source code from Resources.

Example 1: Resource-pool management modularization

One common way to optimize resource usage recycles previously created resources such as threads and database connections.

A typical OOP approach to introducing resource pooling requires you to make a few up-front decisions. To wit, the design must specify when and how to obtain resources from the pool and how to put them back. Since profiling the system necessitates many optimizations, designing up-front resource pooling proves difficult. However, introducing such support afterwards requires you to modify many source files. (See Part 1's "Architecture's Dilemma" section for more details.) Further, the conventional approach lacks agility; it is not easy to turn on, off, or tune such optimizations.

Let's see how AOP and AspectJ help implement resource-pool management in a modularized fashion.

First, let's implement a simple TCP/IP service for converting requested strings to uppercase. The server creates a new thread each time a new connection request arrives. Once a thread completes serving a connection, it terminates naturally. The implementation below is simple Java without any AspectJ constructs:

// UppercaseServer.java
import java.io.*;
import java.net.*;
public class UppercaseServer {
    public static void main(String[] args) throws Exception {
      if (args.length != 1) {
          System.out.println("Usage: java UppercaseServer <portNum>");
          System.exit(1);
      }
      int portNum = Integer.parseInt(args[0]);
      ServerSocket serverSocket = new ServerSocket(portNum);
        
        while(true) {
            Socket requestSocket = serverSocket.accept();
            Thread serverThread 
                = new Thread(new UppercaseWorker(requestSocket));
            serverThread.start();
        }
    }
}
class UppercaseWorker implements Runnable {
    private Socket _requestSocket;
    public UppercaseWorker(Socket requestSocket) throws IOException {
        System.out.println("Creating new worker");
        _requestSocket = requestSocket;
    }
    public void run() {
        BufferedReader requestReader = null;
        Writer responseWriter = null;
        try {
            requestReader
                = new BufferedReader(
                      new InputStreamReader(_requestSocket.getInputStream()));
            responseWriter
                = new OutputStreamWriter(_requestSocket.getOutputStream());
            while(true) {
                String requestString = requestReader.readLine();
                if (requestString == null) {
                    break;
                }
                System.out.println("Got request: " + requestString);
                responseWriter.write(requestString.toUpperCase() + "\n");
                responseWriter.flush();
            }
        } catch(IOException ex) {
        } finally {
            try {
                if (responseWriter != null) {
                    responseWriter.close();
                }
                if (requestReader != null) {
                    requestReader.close();
                }
                _requestSocket.close();
            } catch (IOException ex2) {
            }
        }
        System.out.println("Ending the session");
    }
}

Next, let's see how AspectJ can add a thread-pooling crosscutting concern. First, write a simple ThreadPool class that acts as a stack for available threads. The get() method extracts a thread from the stack, whereas the put() method pushes one in. The put() method also contains the DelegatingThread class that delegates the run() method to the _delegatee worker object:

// ThreadPool.java
import java.util.*;
public class ThreadPool {
    List _waitingThread = new Vector();
    public void put(DelegatingThread thread) {
        System.out.println("Putting back: " + thread);
        _waitingThread.add(thread);
    }
    public DelegatingThread get() {
        if (_waitingThread.size() != 0) {
            DelegatingThread availableThread 
                = (DelegatingThread)_waitingThread.remove(0);
            System.out.println("Providing for work: " + availableThread);
            return availableThread;
        }
        return null;
    }
    static class DelegatingThread extends Thread {
        private Runnable _delegatee;
        
        public void setDelegatee(Runnable delegatee) {
            _delegatee = delegatee;
        }
        
        public void run() {
            _delegatee.run();
        }
    }
}

You now possess the classes needed to add thread pooling. Next, write an aspect that uses the ThreadPool class to add thread pooling to the server:

// ThreadPooling.java
public aspect ThreadPooling {
    ThreadPool pool = new ThreadPool();
    //=====================================================================
    // Thread creation
    //=====================================================================
    pointcut threadCreation(Runnable runnable) 
        : call(Thread.new(Runnable)) && args(runnable);
    Thread around(Runnable runnable) : threadCreation(runnable) {
        ThreadPool.DelegatingThread availableThread = pool.get();
        if (availableThread == null) {
            availableThread = new ThreadPool.DelegatingThread();
        }
        availableThread.setDelegatee(runnable);
        return availableThread;
    }
    //=====================================================================
    // Session   
    //=====================================================================
    pointcut session(ThreadPool.DelegatingThread thread)
        : execution(void ThreadPool.DelegatingThread.run()) && this(thread);
    void around(ThreadPool.DelegatingThread thread) : session(thread) {
        while(true) {
            proceed(thread);
            pool.put(thread);
            synchronized(thread) {
                try {
                    thread.wait();
                } catch(InterruptedException ex) {
                }
            }
        }
    }
    //=====================================================================
    // Thread start    
    //=====================================================================
    pointcut threadStart(ThreadPool.DelegatingThread thread) 
        : call(void Thread.start()) && target(thread);
    void around(Thread thread) : threadStart(thread) {
        if (thread.isAlive()) {
            // wake it up
            synchronized(thread) {
                thread.notifyAll();
            }
        } else {
            proceed(thread);
        }
    }
}

Let's examine the implementation in detail:

  • Pointcut threadCreation() captures joinpoints, thus creating a new thread object taking a Runnable object as the argument.
  • Advise the threadCreation() pointcut to first check the thread pool for available threads. If no thread is available, create a new one. In either case, set the delegatee to the Runnable object passed in and return that object instead. Note, you do not call proceed() in this advice, so therefore you never execute the captured operation.
  • Pointcut session() captures the run() method's execution of any ThreadPool.DelegatingThread objects.
  • By putting session() inside a while(true) loop, you advise session() to never finish the servicing. That ensures a thread, once created, never dies. Once a request is processed, you put the thread back into thread pool and put the thread into waiting state.
  • Pointcut threadStart() captures a call to the Thread.start() method. It uses isAlive() to check if the thread previously started. That would happen for a thread obtained from a pool and now in a waiting state. Wake up the thread by notifying it. If the thread had not started yet, as for a freshly created thread, proceed with starting the thread.

Note that you made no changes to UppercaseServer.java. The code for a simple test client is in UppercaseClient.java.

You can use the same technique for pooling database-connection objects. Simply capture joinpoints that create new connections and advise them to use one from a connection pool instead, if an appropriate connection is available. You also need to capture joinpoints that close connection objects and advise them to instead put those objects back in the resource pool.

1 2 3 Page 1
Page 1 of 3