I want my AOP!, Part 3

Use AspectJ to modularize crosscutting concerns in real-world problems

1 2 3 Page 2
Page 2 of 3

Example 2: Policy-enforcement modularization

To implement policy enforcement using OOP, you must use a combination of documentation, code reviews, and so on. Since these techniques are manual, it is easy to miss policy violations.

In this section, you'll implement policy enforcement to ensure no duplicate listener objects are added to the models, and listeners do not loiter around when the view they represent becomes usable.

Swing's MVC (Model, View, Controller) pattern includes a few unenforced assumptions. First, models let you add a listener object more than once, which leads to duplicate work if an event-notification method carries an expensive operation. Second, you can easily forget to remove listeners before destroying a view. Consider a view onto a model. You add a listener to the model. The listener typically keeps a reference to a view (as a direct field, as a side effect of the listener being the view class's inner class, or as the view class itself being the listener). Now if a programmer forgets to remove its listener from the model before destroying it, the model still holds onto the listener that holds the view object. Since a garbage collector can trace such a view object to a live model object, it would not collect it. Such a view, then, would simply loiter around consuming memory. For more details and a solution, read Java Tip 79.

You can enforce a policy to avoid both situations. In this section, I present an AOP way to handle such policy enforcement. Simply compiling your sources along with these aspects will handle these policies -- no other changes are needed. Cool, right?

I also show how to use UML (Unified Modeling Language) to describe the aspect structural view. But let's first look at the overall structure. Figure 1 shows the aspect hierarchy for event management. I use an <<aspect>> stereotype to denote aspects and a pointcut qualifier to mark pointcuts.

Figure 1. The core listener management structure. Click on thumbnail to view full-size image.

Base aspect: EventListenerManagement

Since implementing both concerns requires capturing joinpoints that add listeners to models, you share the code by creating a base abstract EventListenerManagement aspect. This aspect contains an addListenerCall() pointcut that captures calls to methods adding a listener. It uses the modelAndListenerTypeMatch() pointcut to let derived aspects restrict methods captured by addListenerCall():

// EventListenerManagement.java
import java.util.*;
public abstract aspect EventListenerManagement {
    pointcut addListenerCall(Object model, EventListener listener) 
        : call(void *.add*Listener(EventListener+)) 
        && target(model) && args(listener) && modelAndListenerTypeMatch();
    abstract pointcut modelAndListenerTypeMatch();
}

Implement the uniqueness concern

At its core, the uniqueness concern, before adding any listener, checks whether that listener was previously added. If that listener is already present, the operation does not proceed; otherwise, it adds the listener.

The EventListenerUniqueness aspect implements the concern's core functionality, thus ensuring no duplicate listener objects in a model. That abstract aspect declares EventListenerManagement as its parent. It advises the addListenerCall() pointcut to check for the listener's uniqueness by looking in a list obtained by invoking getCurrentListeners(). It proceeds with adding the listener only if the list doesn't include a listener:

// EventListenerUniqueness.java
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
public abstract aspect EventListenerUniqueness 
    extends EventListenerManagement {
    void around(Object model, EventListener listener) 
        : addListenerCall(model, listener) {
        EventListener[] listeners = getCurrentListeners(model);
        if (!Utils.isInArray(listeners, listener)) {
            System.out.println("Accepting " + listener);
            proceed(model, listener);
        } else {
            System.out.println("Already listening " + listener);
        }
    }
    public abstract EventListener[] getCurrentListeners(Object model);
}

As Figure 2 shows, the concrete aspect TableModelListenerUniqueness extends EventListenerUniqueness to apply the aspect to TableModel and related classes. It provides an implementation for the modelAndListenerTypeMatch() pointcut to restrict the model type to AbstractTableModel and the listener type to TableModelListener. Another concrete aspect, ListDataListenerUniqueness, does the same for list-related classes. For ListDataListenerUniqueness's source code, see Resources.

Figure 2. Structure for implementing uniqueness concern for tables and lists. Click on thumbnail to view full-size image.
// TableModelListenerUniqueness.java
import java.util.EventListener;
import javax.swing.event.TableModelListener;
import javax.swing.table.*;
aspect TableModelListenerUniqueness extends EventListenerUniqueness {
    pointcut modelAndListenerTypeMatch() 
        : target(AbstractTableModel) && args(TableModelListener);
    public EventListener[] getCurrentListeners(Object model) {
        return ((AbstractTableModel)model)
            .getListeners(TableModelListener.class);
    }
}

Each concrete aspect, TableModelListenerUniqueness, for example, handles the listener associated with a model type. You can create a simple aspect such as this for other Swing models, as well as your models. Compiling your source code along with these aspects will ensure no duplicate listener is added.

Implement a no loitering-views concern

Next, you implement a no loitering-views concern, as Figure 3 shows. Instead of adding listeners to a model directly, you can wrap it as a referent in a WeakReference object and add it. Since the added object must be of the correct listener type, use the Decorator pattern in a class extending WeakReference and implementing the required listener interface. The decorator implements the listener interface by delegating each method to the referent object.

Figure 3. The structure for implementing a no loitering-views concern. Click on thumbnail to view full-size image.

The EventListenerWeakening aspect implements the concern's core functionality, thus ensuring that no view loiters after its destruction. That abstract aspect also declares EventListenerManagement as its parent. It advises addListenerCall() to proceed with the listener obtained by calling the getWeakListener() method:

// EventListenerWeakening.java
import java.lang.ref.*;
import java.util.*;
import javax.swing.event.*;
public abstract aspect EventListenerWeakening 
    extends EventListenerManagement dominates EventListenerUniqueness {
    void around(Object model, EventListener listener) 
        : addListenerCall(model, listener) {
        proceed(model, getWeakListener(listener));
    }
    public abstract EventListener getWeakListener(EventListener listener);
}

The WeakEventListener decorates the EventListener in addition to extending WeakReference. The nested RemoveGarbageCollectedListeners aspect removes WeakEventListener from the model when it detects that the referent is garbage collected. Here you check the collected referent in an event notification method. If you don't wish to wait for the event to happen after the view disappears, you could use an implementation using ReferenceQueues and a reaper thread:

// EventListenerWeakening.java (contd...)
public abstract class WeakEventListener extends WeakReference 
    implements EventListener {
    public WeakEventListener(EventListener delegatee) {
        super(delegatee);
    }
    public EventListener getDelegatee() {
        return (EventListener)get();
    }
    public boolean equals(Object other) {
        if (getClass() != other.getClass()) {
            return false;
        }
        return getDelegatee() == ((WeakEventListener)other).getDelegatee();
    }
    
    public String toString() {
        return "WeakReference(" + get() + ")";
    }
    
    abstract static aspect RemoveGarbageCollectedListeners {
        pointcut eventNotification(WeakEventListener weakListener,
                                   EventObject event) 
            : execution(void WeakEventListener+.*(EventObject+))
            && this(weakListener) && args(event) 
            && lexicalScopeMatch();
        abstract pointcut lexicalScopeMatch();
        
        public abstract void removeListener(EventObject event, 
                                            EventListener listener);
        void around(WeakEventListener weakListener, EventObject event) 
            : eventNotification(weakListener, event) {
            if (weakListener.getDelegatee() != null) {
                proceed(weakListener, event);
            } else {
                System.out.println("Removing listener: " + weakListener);
                removeListener(event, weakListener);
            }
        }
    }
}

The abstract lexicalScopeMatch() pointcut ensures only methods from specific WeakListener types are captured. Without this pointcut, execution of all WeakListeners notification would be caught -- for example, table as well as lists. Since removeListener() would cast arguments to specific types, a ClassCastException would throw.

The EventListenerWeakening aspect dominates EventListenerUniqueness, which causes EventListenerWeakening's advice to addListenerCall() to be applied before that of EventListenerUniqueness. That way, you create the wrapped listener first, and the uniqueness check using WeakEventListener.equals() works correctly. Since listeners added in a model are WeakReference wrapped, comparing bare listeners would cause a comparison using equals() to not match. You could avoid the problem by modifying equals(), but that could cause the equals() method's symmetric nature to break.

The TableModelListenerWeakening aspect handles table-related listeners. It uses a specialized WeakEventListener that implements TableModelListener by delegating to the referent object. You'll find the code for the aspect that handles the list-related listener in ListModelListenerWeakening.java:

// TableModelListenerWeakening.java
import java.util.*;
import javax.swing.event.*;
import javax.swing.table.*;
public aspect TableModelListenerWeakening extends EventListenerWeakening {
    pointcut modelAndListenerTypeMatch() 
        : target(AbstractTableModel) && args(TableModelListener);
    public EventListener getWeakListener(EventListener listener) {
        System.out.println("Weakening " + listener);
        return new WeakTableModelListener((TableModelListener)listener);
    }
}
public class WeakTableModelListener extends WeakEventListener 
    implements TableModelListener {
    public WeakTableModelListener(TableModelListener delegatee) {
        super(delegatee);
    }
    
    public void tableChanged(TableModelEvent e) {
        TableModelListener listener = (TableModelListener)getDelegatee();
        listener.tableChanged(e);
    }
    
    static aspect TableRemoveGarbageCollectedListeners 
        extends WeakEventListener.RemoveGarbageCollectedListeners {
        
        pointcut lexicalScopeMatch() : within(WeakTableModelListener);
        public void removeListener(EventObject event, EventListener listener) {
            ((TableModel)event.getSource())
                .removeTableModelListener((TableModelListener)listener);
        }
    }
}

With the aforementioned aspects, you've created a crosscutting concern to avoid multiple notifications to the same model listener, as well as avoided loitering view objects. The simple Test.java tests the functionality. The aspects use the utility Utils.java class.

Related:
1 2 3 Page 2
Page 2 of 3