diff options
author | jhugunin <jhugunin> | 2004-02-19 22:46:06 +0000 |
---|---|---|
committer | jhugunin <jhugunin> | 2004-02-19 22:46:06 +0000 |
commit | 329a415e485db06993b5992960948bc6c61dd8ee (patch) | |
tree | 1a464ce6d08ece442dbe4fb2a95324b37d439093 /docs/developer/compiler-weaver | |
parent | 6ddae42579ceb781831e08f0fcf77a4ff4ffb5c0 (diff) | |
download | aspectj-329a415e485db06993b5992960948bc6c61dd8ee.tar.gz aspectj-329a415e485db06993b5992960948bc6c61dd8ee.zip |
first pass at a guide for developers of compiler and weaver
Diffstat (limited to 'docs/developer/compiler-weaver')
-rw-r--r-- | docs/developer/compiler-weaver/advice-dec.gif | bin | 0 -> 45985 bytes | |||
-rw-r--r-- | docs/developer/compiler-weaver/ajdt-uml.gif | bin | 0 -> 82233 bytes | |||
-rw-r--r-- | docs/developer/compiler-weaver/dev-guide-diagrams.vsd | bin | 0 -> 208896 bytes | |||
-rw-r--r-- | docs/developer/compiler-weaver/dev-guide-uml.vsd | bin | 0 -> 264192 bytes | |||
-rw-r--r-- | docs/developer/compiler-weaver/index.html | 917 | ||||
-rw-r--r-- | docs/developer/compiler-weaver/overview.gif | bin | 0 -> 18250 bytes | |||
-rw-r--r-- | docs/developer/compiler-weaver/pointcut-dec.gif | bin | 0 -> 38274 bytes | |||
-rw-r--r-- | docs/developer/compiler-weaver/top-tree.gif | bin | 0 -> 40199 bytes |
8 files changed, 917 insertions, 0 deletions
diff --git a/docs/developer/compiler-weaver/advice-dec.gif b/docs/developer/compiler-weaver/advice-dec.gif Binary files differnew file mode 100644 index 000000000..43f540a4f --- /dev/null +++ b/docs/developer/compiler-weaver/advice-dec.gif diff --git a/docs/developer/compiler-weaver/ajdt-uml.gif b/docs/developer/compiler-weaver/ajdt-uml.gif Binary files differnew file mode 100644 index 000000000..3a6ff797d --- /dev/null +++ b/docs/developer/compiler-weaver/ajdt-uml.gif diff --git a/docs/developer/compiler-weaver/dev-guide-diagrams.vsd b/docs/developer/compiler-weaver/dev-guide-diagrams.vsd Binary files differnew file mode 100644 index 000000000..5636872df --- /dev/null +++ b/docs/developer/compiler-weaver/dev-guide-diagrams.vsd diff --git a/docs/developer/compiler-weaver/dev-guide-uml.vsd b/docs/developer/compiler-weaver/dev-guide-uml.vsd Binary files differnew file mode 100644 index 000000000..d3fa03dea --- /dev/null +++ b/docs/developer/compiler-weaver/dev-guide-uml.vsd diff --git a/docs/developer/compiler-weaver/index.html b/docs/developer/compiler-weaver/index.html new file mode 100644 index 000000000..7265bed47 --- /dev/null +++ b/docs/developer/compiler-weaver/index.html @@ -0,0 +1,917 @@ +<html> + +<head> +<meta http-equiv="Content-Language" content="en-us"> +<meta name="GENERATOR" content="Microsoft FrontPage 6.0"> +<meta name="ProgId" content="FrontPage.Editor.Document"> +<meta http-equiv="Content-Type" content="text/html; charset=windows-1252"> +<title>AspectJ Developer's Guide</title> +<style> +<!-- +pre { border-style: solid; border-width: 1px; margin-left: 24; padding-left: 4px; + padding-right: 4px; padding-top: 1px; padding-bottom: 1px; + background-color: #EAF3FF; margin-right:24 } +h3 { background-color: #99CCFF } +h2 { background-color: #CCCCFF } +h1 { background-color: #99CCFF } +--> +</style> +</head> + +<body> + +<h1 align="center">Guide for Developers of the AspectJ Compiler and Weaver</h1> +<p>This document is written for developers who want to understand the +implementation of AspectJ. It provides a top-down picture of the compiler +and weaver implementations. This high-level picture should make it easier +to read and understand the source code for AspectJ.</p> +<p>The AspectJ compiler/weaver (ajc) is composed of three primary modules.</p> +<ul> + <li><b>org.aspectj.ajdt.core</b> - this is the compiler front-end and + extends the eclipse Java compiler from <b>org.eclipse.jdt.core</b>. + Because of the dependencies on parts of eclipse this generates a large ~6MB jar.</li> + <li><b>weaver</b> - this provides the bytecode weaving functionality. + It has very few external dependencies to minimize the size required for + deployment of load-time weavers. Currently the build process doesn't + produce a separate jar for just the weaver, but that will have to change for + AspectJ-1.2.</li> + <li><b>runtime</b> - these are the classes that are used by generated code + at runtime and must be redistributed with any system built using AspectJ. + This module has no external dependencies and produces a tiny ~30KB jar.</li> +</ul> +<p> +<img border="0" src="overview.gif"></p> +<p>The AspectJ compiler accepts both AspectJ bytecode and source code and +produces pure Java bytecode as a result. Internally it has two stages. The +front-end (org.aspectj.ajdt.core) compiles both AspectJ and pure Java source +code into pure Java bytecode annotated with additional attributes representing +any non-java forms such as advice and pointcut declarations. The back-end of the +AspectJ compiler (weaver) implements the transformations encoded in these +attributes to produce woven class files. The back-end can be run stand-alone to +weave pre-compiled aspects into pre-compiled .jar files. In addition, the +back-end exposes a weaving API which can be used to implement ClassLoaders that +will weave advice into classes dynamically as they are loaded by the virtual +machine.</p> +<h2>Compiler front-end (org.aspectj.ajdt.core)</h2> +<p>The front-end of the AspectJ compiler is implemented as an extension of the +Java compiler from eclipse.org. The source-file portion of the AspectJ compiler +is made complicated by inter-type declarations, declare parents, declare soft, +and privileged aspects. All of these constructs require changes to the +underlying compiler to modify Java’s name-binding and static checking behavior.</p> +<p>As the compiler extends the jdt.core compiler, the package structure of this +module mimics that of the jdt.core module. The design works hard to minimize the +set of changes required to org.eclipse.jdt.core because a fun 3-way merge is +required each time we want to move to a new underlying version of this code. +The ultimate goal is to contribute all of our changes to jdt.core back into the +main development branch some day.</p> +<p>The basic structure of a compile is very simple:</p> +<ol> + <li>Perform a shallow parse on all source files</li> + <li>Pass these compilation units through AjLookupManager to do type binding + and some AspectJ augmentation</li><li>For each source file do a deep parse, + annotation/analysis, and then code generation</ol> +<h3>Top-level parse tree</h3> +<p>Let's trace the following example program through the compiler.</p> +<pre>package example.parse.tree; + +import org.aspectj.lang.*; + +public class Main { + public static void main(String[] args) { + new Main().doit(); + } + + private void doit() { + System.out.println("hello"); + } +} + +aspect A { + pointcut entries(Main o): execution(void doit()) && this(o); + before(Main o): entries(o) { + o.counter++; + System.out.println("entering: " + thisJoinPoint); + } + + private int Main.counter = 0; +}</pre> +<p>When parsed, this program will produce the following tree.</p> +<p><img border="0" src="top-tree.gif"></p> +<h3>PointcutDeclaration processing</h3> +<p>Let's look more closely at the pointcut +declaration:</p> +<pre>pointcut entries(Main o): execution(void doit()) && this(o);</pre> +<p><img border="0" src="pointcut-dec.gif"></p> +<p>The pointcut declaration is implemented as a subtype of a method declaration. +The actual pointcut is parsed by the weaver module. This parsing happens +as part of the shallow parse phase. This is because this information might +be needed to implement a declare soft.</p> +<h3>AdviceDeclaration processing</h3> +<p>Next we look at the processing for an advice declaration:</p> +<pre>before(Main o): entries(o) { + o.counter++; + System.out.println("entering: " + thisJoinPoint); +}</pre> +<p> +After parsing, the AdviceDeclaration.postParse method will be called to make this +a valid MethodDeclaration so that the standard eclipse code for analyzing a +method body can be applied to the advice. After postParse, the selector is +filled in and several additional arguments are added for the special +thisJoinPoint forms that could be used in the body.</p> +<p> +<img border="0" src="advice-dec.gif"></p> +<p> +At this point the statements field which will hold the body of the advice is +still null. This field is not filled in until the second stage of the +compiler when full parsing is done on each source file as a prelude to +generating the classfile.</p> +<h3> +Overview of the main classes in org.aspectj.ajdt.core</h3> +<p> +The main classes in this module are shown in the following diagram:</p> +<p> +<img border="0" src="ajdt-uml.gif"></p> +<h2>Weaving back-end (weaver)</h2> +<p>This provides all of the weaving functionality. It has very few +dependencies to keep the code as small as possible for deployment in load-time +weavers - only asm, bridge and util which are each very small modules with no +further dependencies. This also depends on a patched version of the bcel library from apache.org. +The patches are only to fix bcel bugs that can't be +worked around in any other way.</p> +<p>There are only four packages in this system.</p> +<ul> + <li>org.aspectj.weaver - general classes that can be used by any weaver + implementation</li> + <li>org.aspectj.weaver.patterns - patterns to represent pointcut designators + and related matching constructs</li> + <li>org.aspectj.weaver.ast - a very small library to represent simple + expressions without any bcel dependencies</li> + <li>org.aspectj.weaver.bcel - the concrete implementation of shadows and the + weaver using the bcel library from apache.org</li> +</ul> +<p class="MsoNormal">The back-end of the AspectJ compiler instruments the code +of the system by inserting calls to the precompiled advice methods. It does +this by considering that certain principled places in bytecode represent +possible join points; these are the “static shadow” of those join points. For +each such static shadow, it checks each piece of advice in the system and +determines if the advice's pointcut could match that static shadow. If it could +match, it inserts a call to the advice’s implementation method guarded by any +dynamic testing needed to ensure the match.</p> +<h2>Runtime support library (runtime)</h2> +<p>This library provides classes that are used by the generated code at runtime. +These are the only classes that must be redistributed with a system built using +AspectJ. Because these classes are redistributed this library must always +be kept as small as possible. It is also important to worry about binary +compatibility when making changes to this library. There are two packages +that are considered public and may be used by AspectJ programs.</p> +<ul> + <li>org.aspectj.lang</li> + <li>org.apectj.lang.reflect</li> +</ul> +<p>There are also several packages all under the header org.aspectj.runtime that +are considered private to the implementation and may only be used by code +generated by the AspectJ compiler.</p> +<p></p> +<h2>Mappings from AspectJ language to implementation</h2> +<table border="1" cellpadding="0" cellspacing="0" style="border-collapse: collapse" bordercolor="#111111" width="100%" height="234"> + <tr> + <td width="12%" height="19"></td> + <td width="23%" height="19">org.aspectj.ajdt.internal.compiler</td> + <td width="40%" height="19">weaver - org.aspectj.weaver.</td> + </tr> + <tr> + <td width="12%" height="19">aspect</td> + <td width="23%" height="19">ast.AspectDeclaration</td> + <td width="40%" height="19">CrosscuttingMembers</td> + </tr> + <tr> + <td width="12%" height="19">advice</td> + <td width="23%" height="19">ast.AdviceDeclaration</td> + <td width="40%" height="19">Advice + bcel.BcelShadowMunger</td> + </tr> + <tr> + <td width="12%" height="19">pointcut declaration</td> + <td width="23%" height="19">ast.PointcutDeclaration</td> + <td width="40%" height="19">ResolvedPointcutDefinition</td> + </tr> + <tr> + <td width="12%" height="19">declare error/warning</td> + <td width="23%" height="19">ast.DeclareDeclaration</td> + <td width="40%" height="19">Checker + patterns.DeclareErrorOrWarning</td> + </tr> + <tr> + <td width="12%" height="38">declare soft</td> + <td width="23%" height="38">ast.DeclareDeclaration + + problem.AjProblemReporter</td> + <td width="40%" height="38">Advice (w/ kind = Softener) + + patterns.DeclareSoft</td> + </tr> + <tr> + <td width="12%" height="38">declare parents</td> + <td width="23%" height="38">ast.DeclareDeclaration + + lookup.AjLookupEnvironment</td> + <td width="40%" height="38">patterns.DeclareParents + NewParentTypeMunger</td> + </tr> + <tr> + <td width="12%" height="18">inter-type decls</td> + <td width="23%" height="18">ast.InterType*Declaration + lookup.InterType*Binding + + lookup.AjLookupEnvironment</td> + <td width="40%" height="18">New*TypeMunger + bcel.BcelTypeMunger</td> + </tr> + <tr> + <td width="12%" height="19">if pcd</td> + <td width="23%" height="19">ast.IfPseudoToken + ast.IfMethodDeclaration</td> + <td width="40%" height="19">patterns.IfPointcut</td> + </tr> + <tr> + <td width="12%" height="17">pcd</td> + <td width="23%" height="17">ast.PointcutDesignator</td> + <td width="40%" height="17">patterns.Pointcut hierarchy</td> + </tr> +</table> +<p></p> +<h1>Tutorial: implementing a throw join point</h1> +<p>This tutorial will walk step-by-step through the process of adding a new join +point to AspectJ for the moment when an exception is thrown. In Java +source code, the shadow of this point is a throw statement. In Java bytecode, +the shadow is the athrow instruction.</p> +<p>This tutorial is recommended to anyone who wants to get a better feel for how +the implementation of AspectJ really works. Even if you're just working on +a bug fix or minor enhancement, the process of working with the AspectJ +implementation will be similar to that described below. The size of your +actual code changes will likely be smaller, but you are likely to need to be +familiar with all of the pieces of the implementation described below.</p> +<h2>Part 1: Adding the join point and corresponding pcd</h2> +<p>The first part of this tutorial will implement the main features of the throw +join point. We will create a new join point shadow corresponding to the athrow +instruction and also create a new pointcut designator (pcd) for matching it.</p> +<h3>Step 1. Synchronize with repository and run the existing test suite</h3> +<p>Do a Team->Synchronize With Repository and make sure that your tree is +completely in sync with the existing repository. Make sure to address any +differences before moving on.</p> +<p>Run the existing test suite. I currently do this in four steps:</p> +<ul> + <li>weaver/testsrc/BcWeaverModuleTests.java</li> + <li>org.aspectj.ajdt.core/testsrc/EajcModuleTests.java</li> + <li>ajde/testsrc/AjdeModuleTests.java</li> + <li>Harness on ajctests.xml -- at least under 1.4, preferably under both 1.3 and +1.4.</li> +</ul> +<p>There should be no failures when you run these tests. If there are +failures, resolve them with the AspectJ developers before moving on.</p> +<h3>Step 2. Write a proto test case</h3> +<p>a. Create a new file in tests/design/pcds/Throw.java</p> +<pre>import org.aspectj.testing.Tester; + +public class Throws { + public static void main(String[] args) { + try { + willThrow(); + Tester.checkFailed("should have thrown exception"); + } catch (RuntimeException re) { + Tester.checkEqual("expected exception", re.getMessage()); + } + } + + static void willThrow() { + throw new RuntimeException("expected exception"); + } +} + +aspect A { + before(): withincode(void willThrow()) { + System.out.println("about to execute: " + thisJoinPoint); + } +}</pre> +<p>b. Create a temporary test harness file to run just this test in myTests.xml</p> +<pre><!DOCTYPE suite SYSTEM "../tests/ajcTestSuite.dtd"> +<suite> + <ajc-test dir="design/pcds" + title="simple throw join point"> + <compile files="Throws.java" /> + <run class="Throws"/> + </ajc-test> +</suite> +</pre> +<p>c. Run this test using the harness. You should see:</p> +<pre>about to execute: execution(void Throws.willThrow()) +about to execute: call(java.lang.RuntimeException(String)) +PASS Suite.Spec(c:\aspectj\eclipse\tests) 1 tests (1 passed) 2 seconds</pre> +<h3>Step 3. Implement the new join point shadow kind</h3> +<p>Modify runtime/org.aspectj.lang/JoinPoint.java to add a name for the Throw +shadow kind.</p> +<pre>static String THROW = "throw";</pre> +<p>Modify weaver/org.aspectj.weaver/Shadow.java to add the Throw shadow kind. +This adds a static typesafe enum for the Throw Kind. The constructor uses the +name from the runtime API to ensure that these names will always match. The '12' +is used for serialization of this kind to classfiles and is part of the binary +API for aspectj. The final 'true' indicates that this joinpoint has its +arguments on the stack. This is because the throw bytecode in Java operates on a +single argument that is a Throwable which must be the top element on the stack. +This argument is removed from the stack by the bytecode. +</p> +<pre>public static final Kind Throw = new Kind(JoinPoint.THROW, 12, true); +</pre> +<p>We also modify the neverHasTarget method to include the Throw kind because in +Java there is no target for the throwing of an exception.</p> +<pre>public boolean neverHasTarget() { + return this == ConstructorCall + || this == ExceptionHandler + || this == PreInitialization + || this == StaticInitialization + || this == Throw; +} +</pre> +<p>In the read method on Shadow.Kind, add another case to read in our new +Shadow.Kind.</p> +<pre>case 12: return Throw; +</pre> +<h3>Step 4. Create this new kind of joinpoint for the throw bytecode</h3> +<p>Modify weaver/org.aspectj.weaver.bcel/BcelClassWeaver.java to recognize this +new joinpoint kind. In the method +<pre>private void match( + LazyMethodGen mg, + InstructionHandle ih, + BcelShadow enclosingShadow, + List shadowAccumulator) +{ +</pre> +<p>Add a test for this instruction, i.e.</p> +<pre>} else if (i == InstructionConstants.ATHROW) { + match(BcelShadow.makeThrow(world, mg, ih, enclosingShadow), + shadowAccumulator); +} +</pre> +<p>Then, modify BcelShadow.java to create this new kind of join point shadow:</p> +<pre>public static BcelShadow makeThrow( + BcelWorld world, + LazyMethodGen enclosingMethod, + InstructionHandle throwHandle, + BcelShadow enclosingShadow) +{ + final InstructionList body = enclosingMethod.getBody(); + TypeX throwType = TypeX.THROWABLE; //!!! not as precise as we'd like + TypeX inType = enclosingMethod.getEnclosingClass().getType(); + BcelShadow s = + new BcelShadow( + world, + Throw, + Member.makeThrowSignature(inType, throwType), + enclosingMethod, + enclosingShadow); + ShadowRange r = new ShadowRange(body); + r.associateWithShadow(s); + r.associateWithTargets( + Range.genStart(body, throwHandle), + Range.genEnd(body, throwHandle)); + retargetAllBranches(throwHandle, r.getStart()); + return s; +} </pre> +<p>Finally modify weaver/org.aspectj.weaver/Member.java to generate the needed +signature</p> +<pre>public static Member makeThrowSignature(TypeX inType, TypeX throwType) { + return new Member( + HANDLER, + inType, + Modifier.STATIC, + "throw", + "(" + throwType.getSignature() + ")V"); +}</pre> +<p>Run the proto test again and you should see:</p> +<pre>about to execute: execution(void Throws.willThrow()) +about to execute: call(java.lang.RuntimeException(String)) +about to execute: throw(catch(Throwable)) +PASS Suite.Spec(c:\aspectj\eclipse\tests) 1 tests (1 passed) 3 seconds +</pre> +<p>That last line shows the 'throw(catch(Throwable))' +join point. This is a slightly confusing string form, but it is the first sign +of our brand new join point. The reason for the weird 'catch(Throwable)' part is +that we used Member.HANDLER for the kind of the signature of this join point. +That's clearly not correct. We'll fix that at the end of the lesson as +part of the clean-up. For now, let's go on with the interesting parts.</p> +<h3>Step 5. Extend our proto-test to use a pointcut designator for matching</h3> +<p>Add a second piece of before advice to the test aspect A:</p> +<pre>before(): throw(Throwable) { + System.out.println("about to throw: " + thisJoinPoint); +}</pre> + +<p>When we run the test again we'll get a long error message from the harness. +The interesting part of the message is the following:</p> +<pre>[ 0] [error 0]: error can't find referenced pointcut at C:\aspectj\eclipse\tests\design\pcds\Throws.java:23:0 +</pre> +<p>This error is not quite what you might have expected. You might have +hoped for a syntax error saying that there is not 'throw' pointcut designator +defined. Unfortunately, this is a weakness in the syntax of AspectJ where +primitive PCDs and named PCDs have the same syntax, so the compiler can't tell +the difference between a misspelled or non-existent primitive PCD and a named +PCD reference that is missing. This also has some impact on extending the +primitive PCDs because it will break existing programs. In this case, when +we add the throw PCD we will break any existing programs that use throw as the +name for a user-defined PCD. Fortunately because throw is a Java keyword +this particular change is very safe.</p> +<h3>Step 6. Extend the PCD parser to handle this new primitive PCD</h3> +<p>Modify the +parseSinglePointcut method in weaver/org.aspectj.weaver.patterns/PatternParser.java to add one more else if clause for the throw pcd:</p> +<pre>} else if (kind.equals("throw")) { + parseIdentifier(); eat("("); + TypePattern typePat = parseTypePattern(); + eat(")"); + return new KindedPointcut(Shadow.Throw, + new SignaturePattern(Member.HANDLER, ModifiersPattern.ANY, + TypePattern.ANY, TypePattern.ANY, NamePattern.ANY, + new TypePatternList(new TypePattern[] {typePat}), + ThrowsPattern.ANY));</pre> +<p>Modify the matches method in weaver/org.aspectj.weaver.patterns/SignaturePattern.java +to add:</p> +<pre>if (kind == Member.HANDLER) { + return parameterTypes.matches(world.resolve(sig.getParameterTypes()), + TypePattern.STATIC).alwaysTrue(); +} </pre> +<p>Run the proto test again and you should see:</p> + +<pre>about to execute: execution(void Throws.willThrow()) +about to execute: call(java.lang.RuntimeException(String)) +about to execute: throw(catch(Throwable)) +about to throw: throw(catch(Throwable)) +PASS Suite.Spec(c:\aspectj\eclipse\tests) 1 tests (1 passed) 1 seconds +</pre> + +Make sure that you see the 'about to throw' printed before moving on. +This shows that the throw PCD is now successfully matching the throw join point +shadow we added earlier.<h3>Step 7. Check that we're properly providing the +single thrown argument (and clean-up the test)</h3> +<p>Now that we have a valid pcd for this advice, we can simplify our test case. +Modify our test aspect A to be the following. In addition to removing the +overly generic withincode pcd, this change also prints the actual +object that is about to be thrown:</p> +<pre>aspect A { + before(Throwable t): throw(*) && args(t) { + System.out.println("about to throw: '" + t+ "' at " + thisJoinPoint); + } +}</pre> + +<p>When we run the test again we should see the output below:</p> +<pre>about to throw: 'java.lang.RuntimeException: expected exception' at throw(catch(Throwable)) +PASS Suite.Spec(c:\aspectj\eclipse\tests) 1 tests (1 passed) 1 seconds +</pre> +<p>Congratulations! You've just +implemented the throw join point and PCD. This code isn't yet ready to be checked into any repository. It still +has some rough edges that need to be smoothed. However, you've now +added a new join point to the AspectJ language and a corresponding PCD to match +it. This is a good time to take a break before moving on to part two.</p> +<h2>Part 2: Getting the signature of this new join point right</h2> +<p>We know that throw(catch(Throwable)) is not the right thing to be printing +for the signature at this join point. What is the correct signature? +At the beginning of the tutorial, we explained that the preferred design for the +pcd was to have throw(StaticTypeOfExceptionThrown). In step 4, we set the +type of the exception thrown to be 'Throwable'. Can we set this to be more +accurate? Looking at the source code, it seems easy to identify the static +type of the exception that is thrown:</p> +<pre>throw new RuntimeException("expected exception");</pre> +<p>In the source code to a Java program there is a well-defined static type for +the exception that is thrown. This static type is used for various stages +of flow analysis to make sure that checked exceptions are always correctly +handled or declared. The ThrowStatement class in our own compiler has a +special field for exceptionType that stores the static type of the exception +thrown. Unfortunately, this static type is much harder to recover from the +corresponding bytecode. In this case we would need to do flow analysis to +figure out what the static type is for the object on the top of the stack +when the athrow instruction executes. This analysis can certainly be done. +In fact this analysis is a small part of what every JVM must do to verify the +type safety of a loaded classfile.</p> +<p>However, the current AspectJ weaver doesn't do any of this analysis. +There are many good reasons to extend it in this direction in order to optimize +the code produced by the weaver. If we were really implementing this +feature, this would be the time for a long discussion on the aspectj-dev list to +decide if this was the right time to extend the weaver with the code flow +analysis needed to support a static type for the throw join point. For the +purposes of this tutorial, we're going to assume that it isn't the right time to +do this (implementing flow analysis for bytecodes would add another 50 pages to +this tutorial). Instead we're going to change the definition of the throw +join point to state that its argument always has a static type of Throwable. +We still allow dynamic matching in args to select more specific types. In +general, good AspectJ code should use this dynamic matching anyway to correspond +to good OO designs.</p> +<h3>Step 1. Change the signature of the throw pcd</h3> +<p>Since we aren't going to recover the static type of the exception thrown, we +need to fix the parser for the throw pcd to remove this information. We'll +fix the PatternParser code that we added in step 1.6 to read as follows:</p> +<pre>} else if (kind.equals("throw")) { + parseIdentifier(); eat("("); + eat(")"); + return new KindedPointcut(Shadow.Throw, + new SignaturePattern(Member.THROW, ModifiersPattern.ANY, + TypePattern.ANY, TypePattern.ANY, NamePattern.ANY, + TypePatternList.ANY, + ThrowsPattern.ANY));</pre> +<p>Notice that this code also starts to fix the member kind to be Member.THROW +instead of the bogus Member.HANDLER that we were using before. To make +this work we have a set of things to do. First, let's create this new kind +in org.aspectj.weaver.Member. Find where the HANDLER kind is defined +there, and add a corresponding throw kind:</p> +<pre>public static final Kind THROW = new Kind("THROW", 8); +</pre> +<p>We also need to fix the serialization kind in +Member.Kind.read(DataInputStream) just above this constant list to add a case +for this new kind:</p> +<pre>case 8: return THROW; +</pre> +<p>Still in this file, we also need to fix Member.makeThrowSignature to use this +new kind:</p> +<pre>public static Member makeThrowSignature(TypeX inType, TypeX throwType) { + return new ResolvedMember( + THROW, + inType, + Modifier.STATIC, + "throw", + "(" + throwType.getSignature() + ")V"); +} +</pre> +<p>If you run the test now you'll get an error from the parser reminding us that +the throw pcd now doesn't accept a type pattern:</p> +<pre>------------ FAIL: simple throw join point() +... +C:\aspectj\eclipse\tests\design\pcds\Throws.java:19:0 Syntax error on token "*", ")" expected + +FAIL Suite.Spec(c:\aspectj\eclipse\tests) 1 tests (1 failed) 1 seconds</pre> +<p>This is an easy fix to the test case as we modify our pcd for the new syntax +in the aspect A in our Throws.java test code:</p> +<pre>before(Throwable t): throw() && args(t) {</pre> +<p> Now when we run the test case it looks like everything's fixed and we're +passing:</p> +<pre>PASS Suite.Spec(c:\aspectj\eclipse\tests) 1 tests (1 passed) 2 seconds</pre> +<h3>Part 2. Make a real test case</h3> +<p>The pass result from running our test should worry you. Unlike previous +runs, this test run doesn't show the output from our System.out.println in the +before advice. So, it's clear this advice is not running. The +problem is that even though the advice is not running, the test case is passing. +We need to make this a real test case to fix this. We'll do that by adding +code that notes when the advice runs and then checks for this event. This +code uses the Tester.event and Tester.checkEvent methods:</p> +<pre>import org.aspectj.testing.Tester; + +public class Throws { + public static void main(String[] args) { + try { + willThrow(); + Tester.checkFailed("should have thrown exception"); + } catch (RuntimeException re) { + Tester.checkEqual("expected exception", re.getMessage()); + } + Tester.checkEvents(new String[] { "before throw" }); + } + + static void willThrow() { + throw new RuntimeException("expected exception"); + } +} + +aspect A { + before(Throwable t): throw() && args(t) { + Tester.event("before throw"); + //System.out.println("about to throw: '" + t+ "' at " + thisJoinPoint); + } +}</pre> +<p>Now when we run our test case it will fail. This failure is good +because we're not matching the throw join point anymore.</p> +<pre>------------ FAIL: simple throw join point() +... +[ 1] [fail 0]: fail [ expected event "before throw" not found] + +FAIL Suite.Spec(c:\aspectj\eclipse\tests) 1 tests (1 failed) 1 seconds</pre> +<h3>Step 3. Fix signature matching again</h3> +<p>In org.aspectj.weaver.patterns.SignaturePattern.matches, we need to handle +throw signature matching the same way we handle advice signature matching. +Both of these pcds match solely on the kind of join point and use combinations +with other pcds to narrow their matches. So, find the line for kind == +Member.ADVICE and add the same line below it for Member.THROW.</p> +<pre>if (kind == Member.ADVICE) return true; +if (kind == Member.THROW) return true;</pre> +<p>This change will make our test case pass again. Run it to be sure.</p> +<p>There's an interesting tension between a good automated test and a good test +for development. Our new test case now correctly includes an automated +test to let us know when we are and are not matching the new throw join point. +However, without the println the test doesn't feel as satisfactory to me to run +during development. I often like to turn this kind of printing back on the +see what's happening. If you uncomment to System.out.println in the test +aspect A and rerun the test, you won't be very happy with the results:</p> +<pre>------------ FAIL: simple throw join point() +... +unimplemented +java.lang.RuntimeException: unimplemented + at org.aspectj.weaver.Member.getSignatureString(Member.java:596) +... + +FAIL Suite.Spec(c:\aspectj\eclipse\tests) 1 tests (1 failed) 1 seconds</pre> +<p>It looks like there's more work to do to add the new member kind for +Member.THROW. This problem only shows up when we try to print +thisJoinPoint. It's showing that we haven't updated the reflection API to +understand this new signature kind.</p> +<h3>Step 4. Extend org.aspectj.lang.reflect to understand throw signatures</h3> +<p>We need to add a couple of classes to the reflection API to implement the +throw signature. Because we decided at the beginning of this section to +not include the static type of the exception thrown in the throw signature, +these classes are extremely simple. Nevertheless, we have to build them. +Notice that when we add new source files to the system we need to include the +standard eclipse CPL license header.</p> +<pre>/* ******************************************************************* + * Copyright (c) 2004 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Common Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/cpl-v10.html + * + * Contributors: + * Jim Hugunin initial implementation + * ******************************************************************/ + +package org.aspectj.lang.reflect; +import org.aspectj.lang.Signature; + +public interface ThrowSignature extends Signature { }</pre> +<pre>/* ******************************************************************* + * Copyright (c) 2004 Contributors. + * All rights reserved. + * This program and the accompanying materials are made available + * under the terms of the Common Public License v1.0 + * which accompanies this distribution and is available at + * http://www.eclipse.org/legal/cpl-v10.html + * + * Contributors: + * Jim Hugunin initial implementation + * ******************************************************************/ + +package org.aspectj.runtime.reflect; +import org.aspectj.lang.reflect.ThrowSignature; + +class ThrowSignatureImpl extends SignatureImpl implements ThrowSignature { + + ThrowSignatureImpl(Class declaringType) { + super(0, "throw", declaringType); + } + + ThrowSignatureImpl(String stringRep) { + super(stringRep); + } + + String toString(StringMaker sm) { + return "throw"; + } +}</pre> +<p>To finish up our work in the runtime module, we need to extend +org.aspectj.runtime.reflect.Factory to add a factory method for this new +signature kind:</p> +<pre>public ThrowSignature makeThrowSig(String stringRep) { + ThrowSignatureImpl ret = new ThrowSignatureImpl(stringRep); + ret.setLookupClassLoader(lookupClassLoader); + return ret; +}</pre> +<p>We're not done yet. We still need to fix up the +org.aspectj.weaver.Member class to use these new methods and types and fix the +unimplemented exception that started us down this road in the first place. +First let's add a method to create a string for the throw signature. This +is a very simple method copied from the other create*SignatureString methods.</p> +<pre>private String getThrowSignatureString(World world) { + StringBuffer buf = new StringBuffer(); + buf.append('-'); // no modifiers + buf.append('-'); // no name + buf.append(makeString(getDeclaringType())); + buf.append('-'); + return buf.toString(); +}</pre> +<p>Now we need to modify three methods to add cases for the new Member.THROW +kind. First, Member.getSignatureMakerName add:</p> +<pre>} else if (kind == THROW) { + return "makeThrowSig"; +</pre> +<p>Next, to Member.getSignatureType add:</p> +<pre>} else if (kind == THROW) { + return "org.aspectj.lang.reflect.ThrowSignature"; +</pre> +<p>Finally, to Member.getSignatureString add:</p> +<pre>} else if (kind == THROW) { + return getThrowSignatureString(world); +</pre> +<p>With all of these changes in place we should have working code for +thisJoinPoint reflection using our new join point and signature kinds. +Rerun the test to confirm:</p> +<pre>about to throw: 'java.lang.RuntimeException: expected exception' at throw(throw) +PASS Suite.Spec(c:\aspectj\eclipse\tests) 1 tests (1 passed) 1 seconds</pre> +<h3>Step 5. Extend the test for automated coverage of reflection</h3> +<p>Modify the before advice to include at least minimal checks of the new +reflective information:</p> +<pre>before(Throwable t): throw() && args(t) { + Tester.event("before throw"); + Tester.checkEqual(thisJoinPoint.getSignature().toShortString(), "throw"); + Tester.checkEqual(t.getMessage(), "expected exception"); +}</pre> +<p> As usual, you should rerun the tests and make sure they pass.</p> +<p> With these changes to the reflection code, it looks like we have a working +version of the throw join point and there are no obvious pieces that we've +skipped. Take a break before proceeding to the final phase of tests.</p> +<h2>Part 3: More serious testing</h2> +<p>Now it's time to get a decent testing story. The test work that we will +do here is probably too little for adding a new join point to the aspectj +language; however, it should at least give you a sense of what's involved.</p> +<h3>Step 1. Run the test suite again</h3> +<p>Rerun the tests you ran at the beginning of part 1. Any failures that +occur should be resolved at this point. At the time of writing this +tutorial, I found 31 failures in the BcWeaverModuleTests. These failures +are for all of the test cases that check the exact set of shadows produces by a +given program. These test cases need to be updated based on the new join +point we're adding. These particular test cases will probably be removed +from the AspectJ test suite very soon because they've shown themselves to be +very fragile over time and they often break for changes that are not introducing +new bugs. However, you should be aware of this kind of failure because you +may find it in other unit tests.</p> +<p>You should expect to see at least one other test case fail when you run +ajcTests.xml. Here's the failure message:</p> +<pre>------------ FAIL: validate (enclosing) join point and source locations() +... +[ 1] [fail 0]: fail [ unexpected event "before AllTargetJoinPoints throw(throw)" found]</pre> +<p>Most of this message can be ignored. To find out what went wrong you +should look for messages that have "fail" in them. The last line tells you +what happened. There was an unexpected event, "before AllTargetJoinPoints throw(catch(Throwable))". +This is the signature for one of the new throw join points that we added in part +1. How could an existing test case match this new join point? The +failing test case uses 'within(TargetClass)' to collect information about ALL +join points that are lexically within a given class. Whenever we add a new +kind of join point to the language we will extend the set of points matched by +pcds like within. This means that these changes need to be very +prominently noted in the release notes for any AspectJ release. Since +we're not writing documentation in this tutorial, we will move on an fix the +test case.</p> +<h3>Step 2. Fix the failing test case</h3> +<p>Now we need to fix this failing test case. The first step is to copy +the test specification into our local myTests.xml file. The easiest way to +do this is to copy the title of the failing test from the output buffer, then +open ajcTests.xml and use find to search for this title. Then copy the xml +spec for this one test into myTests.xml. Finally, run myTests.xml to make +sure you got the failing test. You should see the same failure as before +in step 1, but you should see it a lot faster because we're only running 2 +tests.</p> +<p>To fix the test we need to find the source code. If you look at the +test specification, you can see that the source file is the new directory with +the name NegativeSourceLocation.java. Looking at the bottom of this file, +we see a large list of expected events. These are the join points that we +expect to see. If we look back up in TargetClass, we can see that the only +occurence of throw is just before the handler for catch(Error) and right after +the call to new Error. We should add our new expected event between these +two:</p> +<pre>, "before AllTargetJoinPoints call(java.lang.Error(String))" +, "before AllTargetJoinPoints throw(throw)" // added for new throw join point +, "before AllTargetJoinPoints handler(catch(Error))"</pre> +<p>Run the test suite again to see that this test now passes.</p> +<h3>Step 3. Extend test coverage to after advice</h3> +<p>There is a lot we should do now to extend test coverage for this new kind of +join point. For the purpose of this tutorial, we're just going to make +sure that the new join point kind is compatible with all 5 kinds of advice. +Let's extend our current simple Throws test to check for before and the three +kinds of after advice:</p> +<pre>import org.aspectj.testing.Tester; + +public class Throws { + public static void main(String[] args) { + try { + willThrow(true); + Tester.checkFailed("should have thrown exception"); + } catch (RuntimeException re) { + Tester.checkEqual("expected exception", re.getMessage()); + } + Tester.checkEvents(new String[] + { "before throw", "after throwing throw", "after throw" }); + } + + static void willThrow(boolean shouldThrow) { + int x; + if (shouldThrow) throw new RuntimeException("expected exception"); + else x = 42; + System.out.println("x = " + x); + } +} + +aspect A { + before(Throwable t): throw() && args(t) { + Tester.event("before throw"); + Tester.checkEqual(thisJoinPoint.getSignature().toShortString(), "throw"); + Tester.checkEqual(t.getMessage(), "expected exception"); + } + + after() returning: throw() { + Tester.checkFailed("shouldn't ever return normally from a throw"); + } + + after() throwing(RuntimeException re): throw() { + Tester.event("after throwing throw"); + Tester.checkEqual(re.getMessage(), "expected exception"); + } + + after(): throw() { + Tester.event("after throw"); + } +}</pre> +<p>Run this test to confirm that it still passes. This is a very nice +property of the orthogonality of the implementation of join points and advice. +We never had to do any implementation work to make our new join point kind work +for before and all three kinds of after advice.</p> +<h3>Step 4. Look at around advice on throw join points</h3> +<p>Let's create a new test case to see how this new join point interacts with +around advice.</p> +<pre>import org.aspectj.testing.Tester; + +public class AroundThrows { + public static void main(String[] args) { + try { + willThrow(true); + Tester.checkFailed("should have thrown exception"); + } catch (RuntimeException re) { + Tester.checkEqual("expected exception", re.getMessage()); + } + } + + static void willThrow(boolean shouldThrow) { + int x; + if (!shouldThrow) x = 42; + else throw new RuntimeException("expected exception"); + System.out.println("x = " + x); + } +} + +aspect A { + void around(): throw() { + System.out.println("about to throw something"); + proceed(); + } +}</pre> +<p>When we run this test case we get a very unpleasant result:</p> +<pre>------------ FAIL: simple throw join point with around() +... +[ 1] --- thrown +java.lang.VerifyError: (class: AroundThrows, method: willThrow signature: (Z)V) Accessing value from uninitialized register 1 +... +FAIL Suite.Spec(c:\aspectj\eclipse\tests) 3 tests (1 failed, 2 passed) 3 seconds +</pre> +<p>A VerifyError at runtime is the second worst kind of bug the AspectJ compiler +can produce. The worst is silently behaving incorrectly.</p> +<p>Unfortunately, this VerifyError is either impossible or very hard to fix. +Think about what would happen if the around advice body didn't call proceed. +In this case the local variable x would in fact be uninitialized. There is +another serious language design question here, and for a real implementation +this would once again be the time to start a discussion on the aspectj-dev +mailing list to reach consensus on the best design. For the purpose of +this tutorial we're once again going to make the language design choice that is +easiest to implement and press on.</p> +<h3>Step 5. Prohibit around advice on this new join point kind</h3> +<p>The easiest solution to implement is to prohibit around advice on throw join +points. There are already a number of these kinds of rules implemented in +the org.aspectj.weaver.Shadow.match(Shadow, World) method. We can add our +new rule at the beginning of the if(kind == AdviceKind.Around) block:</p> +<pre>} else if (kind == AdviceKind.Around) { + if (shadow.getKind() == Shadow.Throw) { + world.showMessage(IMessage.ERROR, + "around on throw not supported (possibly compiler limitation)", + getSourceLocation(), shadow.getSourceLocation()); + return false; + }</pre> +<p> Now if we rerun our test we'll see errors telling us that around +is prohibited on throw join points:</p> +<pre>------------ FAIL: simple throw join point with around() +... +[ 0] [error 0]: error at C:\aspectj\eclipse\tests\design\pcds\AroundThrows.java:22 around on throw not supported (possibly compiler limitation) +[ 0] [error 1]: error at C:\aspectj\eclipse\tests\design\pcds\AroundThrows.java:16 around on throw not supported (possibly compiler limitation) +... +FAIL Suite.Spec(c:\aspectj\eclipse\tests) 3 tests (1 failed, 2 passed) 3 seconds</pre> +<p>To finish this test case up we need to modify the specification to be looking +for these errors as the correct behavior. This will produce the following +specification:</p> +<pre><ajc-test dir="design/pcds" + title="simple throw join point with around"> + <compile files="AroundThrows.java"> + <message kind="error" line="16"/> + <message kind="error" line="22"/> + </compile> +</ajc-test></pre> +<p>Run myTests.xml one last time to see both tests passing.</p> +<h3>Step 6. Final preparations for a commit or patch</h3> +<p>You probably want to stop here for the purposes of this tutorial. We've +pointed out several language design decisions that would need to be resolved +before actually adding a throw join point to AspectJ. Some of those might +involve a large amount of additional implementation work. If this was +actually going into the tree, it would also be important to add several more +test cases exploring the space of what can be done with throw.</p> +<p>Assuming those issues were resolved and you are ready to commit this new +feature to the tree there are three steps left to follow:</p> +<ol> + <li>Move our new test specifications from myTests.xml to the end of + ajcTests.xml</li> + <li>Rerun ajcTests.xml and the unit tests to ensure everything's okay.</li> + <li>Update from the repository to get any changes from other committers + since you started work on this new feature.</li> + <li>Rerun ajcTests.xml and the unit tests to make sure nothing broke as a + result of the update.</li> + <li>Finally you can commit these changes to the AspectJ tree.</li> +</ol> +</body> + +</html>
\ No newline at end of file diff --git a/docs/developer/compiler-weaver/overview.gif b/docs/developer/compiler-weaver/overview.gif Binary files differnew file mode 100644 index 000000000..b32f11b15 --- /dev/null +++ b/docs/developer/compiler-weaver/overview.gif diff --git a/docs/developer/compiler-weaver/pointcut-dec.gif b/docs/developer/compiler-weaver/pointcut-dec.gif Binary files differnew file mode 100644 index 000000000..fed640d36 --- /dev/null +++ b/docs/developer/compiler-weaver/pointcut-dec.gif diff --git a/docs/developer/compiler-weaver/top-tree.gif b/docs/developer/compiler-weaver/top-tree.gif Binary files differnew file mode 100644 index 000000000..d8e2b26ff --- /dev/null +++ b/docs/developer/compiler-weaver/top-tree.gif |