Monday, August 6, 2007

Using AspectJ to detect violations of the Swing single thread rule.

As I pointed out in my previous entry, all swing components must be initialized and accessed through the Event Dispatch Thread (EDT) only. While a visual consequence of not doing so might never be observed, when they do they can have quite dramatic consequences for the user's confidence in your system. Therefore, some effort should be put into avoiding this problem by educating developers and providing a design that accommodates this constraint. However, experience tells that no matter how much effort you put into it up front, there will be a few glitches. Therefore, you may want to add new tool that can detect such defects during test, before you end up chipping your product out to the users. This is one instance where AspectJ becomes handy.

AspectJ is developed, as part of the eclipse project, to provide Java with a new extension to support aspect oriented programming. AspectJ is an interesting tool regardless of any knowledge or usage of aspect oriented programming. One of its most interesting capabilities is to inject new functionality into your binary code at runtime (as of Java 1.5), meaning that you do not to pollute your production code base with debugging functionality. The principle of AspectJ is fairly simple but allows for very powerful combinations, you write so called aspects, that define some functionality, an advice, that will be triggered when a predefined cutpoint is reached. A gross oversimplification, but somewhat helpful to get the idea, is to think of aspects as an advanced form of interceptors. In this entry, I'll show a very narrow application of this, to catch violations of the Swing single thread rule.

The aspect presented below contains two cutpoint definitions. The first called swingMethods() defines any call to a method on a class of the javax.swing package or on any class extending a class of the javax.swing package. The second cutpoint, safeMethods(), defines any call to a method called matching the expressions add*Listener or remove*Listener on a class of the javax.swing package or any class extending one. Also any call to setText() on a JComponent is included. What these methods have in common is that they are defined as thread safe by the swing framework. We have one advice defined, called checkCallingThread(), which logs a message to standard error as well as prints the stack trace. Using the @Before annotation with the two previously defined cutpoints, we define the advice to be executed before any call to a swing method except if it is part of the safe methods.

import java.awt.EventQueue;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;


@Aspect
public class EDTCheck {

@Pointcut("call (* javax.swing..*+.*(..)) || " +
"call (javax.swing..*+.new(..))")
public void swingMethods() {}

@Pointcut("call (* javax.swing..*+.add*Listener(..)) || " +
"call (* javax.swing..*+.remove*Listener(..)) || " +
"call (void javax.swing.JComponent+.setText(java.lang.String))")
public void safeMethods() {}

@Before("swingMethods() && !safeMethods()")
public void checkCallingThread(JoinPoint.StaticPart thisJoinPointStatic) {
if(!EventQueue.isDispatchThread()) {
System.out.println(
"Swing single thread rule violation: "
+ thisJoinPointStatic);
Thread.dumpStack();
}
}

}


The aspect is compiled like any other Java source file, and you and up with a regular class file. Now to running the aspect on some test code. The code presented below contains some legal calls to swing code and some illegal ones. The calls on the JLabel are illegal except for the addKeyListener() call. The calls to the JTextField are all legal because they take place on the EDT. All calls to the CustomJComponent are illegal. Now one last thing is needed to run our test, the aop.xml file to configure the aspectJ weaver.

package test;

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.KeyAdapter;
import javax.swing.JComponent;
import javax.swing.JLabel;

import javax.swing.JTextField;

public class Test {

public static void main(String[] args) {
JLabel jLabel = new JLabel();
jLabel.getText();

CustomJComponent customJComponent =
new CustomJComponent();
customJComponent.changeColor(Color.BLACK);

EventQueue.invokeLater(new Runnable() {
public void run() {
JTextField jTextField = new JTextField();
jTextField.getPreferredSize();
}
});

jLabel.addKeyListener(new KeyAdapter(){});
}

public static class CustomJComponent extends JComponent {
public void changeColor(Color color) {
}
}

}


The aop.xml presented below configures the aspectJ weaver to use our aspect to be weaved into any class in the test package. It is usually a good idea to restrict the weaving of aspects to the code set that you want to test, because weaving can slow things down significantly and there really isn't any point in weaving classes you know have nothing to do with Swing.

<aspectj>
<aspects>
<aspect name="EDTCheck"/>
</aspects>
<weaver options="-verbose">
<include within="test.*" />
</weaver>
</aspectj>


The aop.xml needs to be placed in a META-INF directory in your classpath. So to run this you simply execute the following command (assuming your classpath is set up correctly):

java -javaagent:c:/Temp/aspectj/lib/aspectjweaver.jar test.Test


The result looks something like this:

Swing single thread rule violation: call(javax.swing.JLabel())
java.lang.Exception
at EDTCheck.checkCallingThread(EDTCheck.java:26)
at test.Test.main(Test.java)
Swing single thread rule violation: call(String javax.swing.JLabel.getText())
java.lang.Exception
at EDTCheck.checkCallingThread(EDTCheck.java:26)
at test.Test.main(Test.java:15)
Swing single thread rule violation: call(test.Test.CustomJComponent())
java.lang.Exception
at EDTCheck.checkCallingThread(EDTCheck.java:26)
at test.Test.main(Test.java:16)
Swing single thread rule violation: call(void test.Test.CustomJComponent.changeColor(Color))
java.lang.Exception
at EDTCheck.checkCallingThread(EDTCheck.java:26)
at test.Test.main(Test.java:20)


We can see that all illegal calls have been captured and a useful stack trace points out precisely in the code where the problem occurs. This can be a good addition to your test toolkit, and can be used whenever the test team is running their tests, or by yourself when you have a suspicion of some threading problems in your Swing code. However, remember that this is not a static analysis tool and the code will actually have to be executed before any error can be detected. Therefore, if you do not thoroughly go through your entire UI during test, a lot of errors will go undetected.

This is only one of the many applications of AspectJ. I would recommend that you go visit the AspectJ website for more information and possible applications. This aspect can be successfully used as part of the development process for a Swing application to catch potential bugs as early as possible, which is the best time to catch them after all.

2 comments:

Anonymous said...

Hi,

do you think it is possible to develop an annotational aspect that could perform all long tasks on a swingworker thread and return again on the EDT ?

so any place there is a service call you could do some thing like

@doOutsideEdt
Object serviceCall(){

}


doOutsideEdt would be an aspect that does an 'around' advice on the service method call taking it off the edt, putting it on a swingworker and then returning the result back on the edt

Anders Prisak said...

Hi,

It would not be possible with the SwingWorker itself because it is asynchronous and the call you are showing me is synchronous (it returns a value). You might be able to pull it off with the Worker from foxtrot (http://foxtrot.sourceforge.net/docs/worker.php). It's an interesting idea, if you get any result let me know. I might try it out myself if I find the time.