Release Name: V0.5.0
Notes:
== Aquarium
Aquarium is a toolkit for Aspect-Oriented Programming (AOP) whose goals include:
* A powerful "pointcut" language for specifying where to apply aspects, comparable to the pointcut language in AspectJ for Java.
* Management of concurrent aspects (i.e., those acting on the same "join points").
* Adding and removing aspects dynamically.
* A user-friendly DSL.
* Support for advising Java types through JRuby.
=== Why Is an AOP Framework Useful in Ruby?
Ruby's metaprogramming facilities already provide some of the capabilities for which static-language AOP toolkits like AspectJ are typically used. With Ruby, you can easily add new methods and attributes to existing classes and objects. You can alias and redefine existing methods, which provides the method interception and "wrapping" needed to extend or modify existing behavior.
However, what is missing in Ruby is an expressive language for describing systemic modifications, a so-called "pointcut language". If you have simple needs for method interception and wrapping, then Aquarium will be overkill. However, if you have system-wide concerns that cross the boundaries of many objects, then an AOP tookit like Aquarium can help you implement these concerns in a more modular way.
So, if you are designing with aspects, wouldn't you like to write your code using the same "language"? Without AOP support, you have to map your aspect designs to metaprogramming idioms, which will often be slower to implement and harder to maintain. Imagine writing objects without native support for OOP!
=== Terminology
Several terms are used in the AOP community.
* Join Point - A point of execution in a program where "advice" might be invoked.
* Pointcut - (yes, one word...) A set of join points of particular interest, like a query over all join points in the system.
* Advice - The behavior invoked at a join point. There are several kinds of advice:
* Before advice - Advice invoked before the actual join point is invoked.
* After returning advice - Advice invoked after the join point executes successfully.
* After raising advice - Advice invoked only after the join point raises an exception.
* After advice - Advice invoked after the join point executes successfully or raises an exception.
* Around advice - Advice invoked instead of the join point. The around advice must choose whether or not to invoke the join point by calling a special "proceed" method. Otherwise, the join point is NOT executed.
Only around advice can prevent execution of the join point, except for the special case where before advice raises an exception.
=== Known Limitations
* You cannot advice "String", "Symbol" or instances there of, because trying to specify either one will be confused with naming a type.
* Concurrent advice on type AND advice on objects of the type can't be removed cleanly.
* The pointcut language is still limited, compared to AspectJ's. See also the comparison with AspectJ behavior next.
* The API and wrapper DSL will probably evolve until the 1.0.0 release. Backwards compatibility will be maintained between releases as much as possible. When it is necessary to break backwards compatibility, translation tools will be provided, if possible.
* There are limitations when advising Java types through JRuby. The separate RSpec suite in the "jruby" directory documentations the details on how to use Aquarium with JRuby-wrapped Java types and the limitations thereof. Here we summarize what works and what doesn't:
* Aquarium advice on a method in a Java type will only be invoked when the method is called directly from Ruby.
* To have the advice invoked when the method is called from either Java or Ruby, it is necessary to create a subclass of the Java type in Ruby and an override of the method, which can just call "super". Note that it will be necessary for instances of this Ruby type to be used throughout, not instances of a Java parent type.
* BUG #18325: If you have Ruby subclasses of Java types *and* you advise a Java method in the hierarchy using @:types_and_descendents => MyJavaBaseClassOrInterface@ *and* you call unadvise on the aspect, the advice "infrastructure" is not correctly removed from the Ruby types. Workaround: Only advise methods in Ruby subclasses of Java types where the method is explicitly overridden in the Ruby class. (The spec and the "Rubyforge bug report":http://rubyforge.org/tracker/index.php?func=detail&aid=18325&group_id=4281&atid=16494 provide examples.)
* BUG #18326: Normally, you can use either Java- or Ruby-style method names (e.g., "doSomething" vs. "do_something"), for Java types. However, if you write an aspect using the Java-style for a method name and a Ruby subclass of the Java type where the method is actually defined (i.e., the Ruby class doesn't override the method), it appears that the JoinPoint was advised, but the advice is never called. Workaround: Use the Ruby-style name in this scenario.
=== Differences With Other Ruby AOP Toolkits
There are several other AOP toolkits for Ruby that precede Aquarium. The most notable are AspectR and the aspect capabilities in the Facets toolkit. There are also Ruby 2.0 proposals to add method wrappers for "before", "after" and "wrap" behavior.
The goal of Aquarium is to provide a superset of the functionality provided by these other toolkits. Aquarium is suitable for non-trivial and large-scale aspect-oriented components in systems. Aquarium will be most valuable for systems where aspects might be added and removed dynamically at runtime and systems where nontrivial pointcut descriptions are needed, requiring a full-featured pointcut language (as discussed above...). For less demanding needs, the alternatives are lighter weight and hence may be more appropriate.
=== Differences With AspectJ Behavior
Many of AspectJ's features are not currently supported by Aquarium, but some of them are planned for future releases.
* Attribute reading and writing join points are not supported. The :attributes and :attributes_options parameters (and their synonyms) for Aspect.new are actually "syntactic sugar" for the corresponding accessor methods.
* More advanced AspectJ pointcut language features are not supported, such as the runtime pointcut designators like "if" conditionals and "cflow" (context flow) and the compile time "within" and "withincode" designators. Most of AspectJ pointcut language features are planned, however.
* While AspectJ provides "intertype declaration" capabilities (i.e., adding state and behavior to existing classes), Ruby's native metaprogramming support satisfies this need. There may be convenience hooks added in a future release, however.
* User defined advice precedence is not supported. However, advice precedence is unambiguous; the last aspects created while modules are loaded at runtime have higher precedence than earlier aspects. Ensuring a particular order is not always easy, of course.
However, Aquarium does have a few advantages over AspectJ, especially when advising Java types when running in JRuby.
* Aquarium can add and remove advice dynamically, at runtime.
* Aquarium can advise individual objects, not just classes.
* Aquarium can advise JDK classes. AspectJ can also do this, but not by default.
* Aquarium supports named advice that can be defined separately from the aspects that use the advice.
* Aquarium can advise ancestor (parent) types, not just derived (descendent) types of specified types.
Note: at the time of this writing (V0.4.0 release), there is an important limitation of Aquarium when used with java code; it appears that advice is only invoked if an advised method is called directly from Ruby code. If the advised method is called by other Java code, the advice is *not* invoked. Whether or not this limitation can be removed is under investigation.
Also, as of V0.4.0, the interaction behavior of Aquarium and AspectJ or Spring aspects has not been investigated.
=== Examples
Several complete examples are provided in the "examples" directory.
In most cases, you can either declare the appropriate classes or use the optional DSL, which adds convenience methods to classes, objects, or even Object itself. The API also supports many synonyms for things like types, objects, and methods. The best place to see the full list of synonyms is the output of "pointcut_spec.rb".
Here is an example that traces invocations of all public instance methods (included inherited ones) of the classes or modules Foo and Bar.
require 'aquarium'
Aspect.new :around, :calls_to => :all_methods, :on_types => [Foo, Bar] do |join_point, object, *args|
p "Entering: #{join_point.target_type.name}##{join_point.method_name} for object #{object}"
result = join_point.proceed
p "Leaving: #{join_point.target_type.name}##{join_point.method_name} for object #{object}"
result # block needs to return the result of the "proceed"!
end
The advice to execute at each join point is the block. The pointcut is the set of all public instance methods in Foo and Bar. (There are additional options available for specifying class methods, protected methods, excluding inherited (ancestor) methods, etc.) Here is the same example using the convenience DSL that adds aspect methods to Object (available only if you require aquarium/dsl/object_dsl', since other toolkits, like Rails, define similar methods on Object!).
require 'aquarium/dsl/object_dsl'
around :calls_to => :all_methods, :on_types => [Foo, Bar] do |join_point, object, *args|
p "Entering: #{join_point.target_type.name}##{join_point.method_name} for object #{object}"
result = join_point.proceed
p "Leaving: #{join_point.target_type.name}##{join_point.method_name} for object #{object}"
result # block needs to return the result of the "proceed"!
end
See "examples/method_tracing_example.rb" for a more detailed version of this example.
If you don't want to add these methods to Object, you can also add them individually to modules, classes, or objects:
require 'aquarium'
...
module MyModule
include Aquarium::DSL
end
class MyClass
include Aquarium::DSL
end
my_object = MyOtherClass.new
my_object.extend (Aquarium::DSL)
If you use the DSL inside a class and omit the :type(s) and :object(s) options, "self" is assumed.
class Foo
include Aquarium::DSL
...
def critical_operation *args
...
end
end
...
class Foo
around :critical_operation do |join_point, object, *args|
p "Entering: Foo#critical_operation"
result = join_point.proceed
p "Leaving: Foo#critical_operation"
result
end
end
It is important to note that aspect "instances" usually behave like class (static) variables, in terms of the lifetime of their effects. In the example shown, class Foo is permanently modified to do the print statements shown for all "critical methods", unless you save the result of calling "around" to a variable, e.g., critical_operation_logging, and you explicitly call "critical_operation_logging.unadvise" at some future time. Put another way, the effects scope just like changes made when you reopen a class or module.
A common mistake is to create an aspect in an initialize method and assign it to an attribute. This usually means that you are creating long-lived, redundant aspects every time an instance of your class is created. The aspect modifications remain in effect even when the instances themselves are garbage collected!
Here are some more succinct examples, illustrating the API (using the DSL methods) and some of the various synonyms for methods, types, etc.
You can pass in pointcuts defined elsewhere:
my_pointcut = Pointcut.new :invocations_of => /^do_/, :within_types => /Foo::Bar::/
around :pointcuts => my_pointcut do |jp, obj, *args| ... # Pass in a pointcut
around :pointcuts => [my_pointcut, ...] do |jp, obj, *args| ... # Pass in a pointcut array
As a convenience, since a JoinPoint is a Pointcut with one element, you can pass a JoinPoint object where Pointcut objects are expected:
my_join_point1 = JoinPoint.new :type => Foo::Bar, :method => do_this
my_join_point2 = JoinPoint.new :type => Foo::Bar, :method => do_that
around :pointcuts => my_join_point1 do |jp, obj, *args| ...
around :pointcuts => [my_join_point1, my_join_point2, ...] do |jp, obj, *args| ...
You can specify a single type, a type name, a type regular expression, or an array of the same. Note that :type and :types are synonymous. Use the singular form for better readability when you are specifying just one type. Other synonyms include :on_types, :within_types, and :in_types, plus the singular forms.
around :type = A, ...
around :type = "A", ...
around :types => [A, B, ...], ...
around :types => %w[A, B, ...], ...
around :types => /A::.*Helper$/, ...
around :types => [/A::.*Helper$/, /B::Foo.*/], ...
Everywhere "type" is used, you can substitute "class", "classes", "module", or "modules". Note that they are treated as synonyms; there is currently no enforcement that the values passed with ":class", for example, are actually classes, not modules.
There are also several prepositional prefixes allowed for any of the synonyms. E.g.,
around :for_types = A, ...
around :on_types = A, ...
around :in_types = A, ...
around :within_types = A, ...
Using the plural versions of the synonyms with method specifications sometimes read better:
around :calls_to => :all_methods, :on_types => [A, B, ...], ...
around :calls_to => :all_methods, :in_types => [A, B, ...], ...
around :calls_to => :all_methods, :within_types => [A, B, ...], ...
You can specify types and their descendents (subclasses or included modules) or ancestors. The same synonym prefixes for :types and :type also apply.
around :type_and_ancestors = A, ...
around :types_and_ancestors = A, ...
around :type_and_descendents = A, ...
around :types_and_descendents = A, ...
around :classes_and_descendents = A, ...
around :modules_and_descendents = A, ...
Some of the synonyms:
around :calls_to => :all_methods, :on_types_and_ancestors = A, ...
around :calls_to => :all_methods, :in_types_and_ancestors = A, ...
around :calls_to => :all_methods, :within_types_and_ancestors = A, ...
and similarly for descendents
You can specify a single object or an array of objects. As for :types, you can use :object, :objects, :on_objects, :within_object, :in_objects, and the singular forms synonymously.
a1 = A.new
a2 = A.new
around :object = a1, ...
around :objects => [a1, a2], ...
Some of the synonyms:
around :calls_to => :all_methods, :on_objects = [a1, a2], ...
around :calls_to => :all_methods, :in_objects = [a1, a2], ...
around :calls_to => :all_methods, :within_objects = [a1, a2], ...
If no types or objects are specified, the object defaults to "self". However, this default is only supported when using the DSL to create an aspect, e.g.,
class MyClass
include Aquarium::DSL
def doit; ...; end
around :method => doit, ... # Implicit :object => self, i.e., MyClass
end
You can specify a single method symbol (name), a regular expression, or an array of the same. The synonyms for :methods include :method, :calls_to, :invoking, :invocations_of, and :sending_messages_to. The special keywords :all and :all_methods mean match all methods, subject to the :method_options discussed next.
around :method = :all_methods, ...
around :method = :foo, ...
around :methods = [:foo, :bar, :baz], ...
around :methods = /^foo/, ...
around :methods = [/^foo/, /bar$/], ...
Using the synonyms:
around :calls_to = :all_methods, ...
after :invoking = :all_methods, ...
after :invocations_of = :all_methods, ...
after :sending_messages_to = :all_methods, ...
after :within_methods = :all_methods, ...
You can specify method options. By default, public instance methods only are matched. Note that :methods => :all or :all_methods with no method options matches all public instance methods, including ancestor (inherited and included module) methods. For all the method options (except for :exclude_ancestor_methods), you can append the suffix "_methods". You can also use the :restrict_methods_to synonym for :method_options.
around :methods = /foo/, :method_options => [:instance], ... # match instance methods (default)
around :methods = /foo/, :method_options => [:class], ... # match class methods
around :methods = /foo/, :method_options => [:public, :protected, :private], ...
# match public, protected, and private instance methods
around :methods = /foo/, :method_options => [:singleton], ... # match singleton methods
around :methods = /foo/, :method_options => [:exclude_ancestor_methods], ...
# ignore methods defined in ancestors, inherited classes and included modules
With synonyms:
around :calls_to = /foo/, :restricting_methods_to => [:singleton_methods], ...
You can specify attributes, which are actually convenience methods for the attribute accessors. They work very much like the :method options. Note that :all is NOT supported in this case. The available synonyms are slightly more complicated, as shown in these examples.
around :attribute = :foo, ... # defaults to methods #foo and #foo=
around :attributes = :foo, ... # the same
around :accessing = :foo, ... # the same
around :attribute = :foo, :attribute_options => [:readers]... # only matches #foo
around :reading = :foo # the same
around :attribute = :foo, :attribute_options => [:writers]... # only matches #foo=
around :writing = :foo # the same
around :attributes = [:foo, :bar, :baz], ...
around :attributes = /^foo/, ...
around :attributes = [/^foo/, /bar$/], ...
Again, it's important to remember that actually advising the attribute accesses is not done; it's the public accessor methods that are advised! This may change in a future release.
You can specify a "Pointcut" that encapsulates one or more pre-defined Pointcuts or JoinPoints.
around :pointcut = pc, ... # for pre-defined pointcut "pc"
around :pointcuts = [pc, ...], ... # for pre-defined pointcut list
around :pointcut = jp, ... # for pre-defined join point "jp"
around :pointcuts = [jp, ...], ... # for pre-defined join point list
around :pointcut = {:type => T, :method => :m}, ... # same as around :type => T, :method => :m, ..
Using the plural versions of the synonyms, with method specifications so they read better:
around :for_pointcuts => [pc1, pc2, ...], ...
around :on_pointcuts => [pc1, pc2, ...], ...
around :in_pointcuts => [pc1, pc2, ...], ...
around :within_pointcuts => [pc1, pc2, ...], ...
Since V0.4.2, you can also specify "named" pointcuts, which are searched for just like methods in types (as discussed below).
For example, if several classes in module "App" define class constant pointcuts named STATE_CHANGE, the following expression
in an around advice aspect will match all of them:
around :named_pointcuts => { :constants_matching => :STATE_CHANGE, :in_types => /App::.*/ } ...
For the type specification, which is required, any valid option for the TypeFinder class is allowed.
You can also match on class variables, using ":class_variables_matching". To match on either kind of definition, use just
":matching". If no :*matching is specified, then any class constant or variable Pointcut found will be matched.
Here are the variaus :*matching options and their synonyms:
around :named_pointcuts => { :constants_matching => :STATE_CHANGE, ... } ... # class constants only
around :named_pointcuts => { :constants_named => :STATE_CHANGE, ... } ...
around :named_pointcuts => { :constants_with_names_matching => :STATE_CHANGE, ... } ...
around :named_pointcuts => { :class_variables_matching => :STATE_CHANGE, ... } ... # class variables only
around :named_pointcuts => { :class_variables_named => :STATE_CHANGE, ... } ...
around :named_pointcuts => { :class_variables_with_names_matching => :STATE_CHANGE, ... } ...
around :named_pointcuts => { :matching => :STATE_CHANGE, ... } ... # class constants and variables
around :named_pointcuts => { :named => :STATE_CHANGE, ... } ...
around :named_pointcuts => { :with_names_matching => :STATE_CHANGE, ... } ...
The :*matching options take a name, regular expression or array of the same (you can mix names and regular expressions).
You can also use the following synonyms for :named_pointcuts:
around :named_pointcut => {...}
around :for_named_pointcut => {...}
around :on_named_pointcut => {...}
around :in_named_pointcut => {...}
around :within_named_pointcut => {...}
around :for_named_pointcuts => {...}
around :on_named_pointcuts => {...}
around :in_named_pointcuts => {...}
around :within_named_pointcuts => {...}
You can specifically exclude particular pointcuts, join points, types, objects, methods, or attributes. This is useful when you specify a list or regular expression of "items" to match and you want to exclude some of the items.
Note that there is an open bug (#15202) that appears to affect advising types, unadvising the types, then advising objects of the same types. (This is not likely to happen a lot in real applications, but it shows up when running Aquarium's specs.)
around ..., :exclude_pointcut = pc, ...
around ..., :exclude_pointcuts = [pc, ...]
around ..., :exclude_named_pointcut = {...}
around ..., :exclude_named_pointcuts = {...}
around ..., :exclude_join_point = jp, ...
around ..., :exclude_join_points = [jp, ...]
around ..., :exclude_type = t, ...
around ..., :exclude_types = [t, ...]
around ..., :exclude_type_and_ancestors = t, ...
around ..., :exclude_types_and_ancestors = [t, ...]
around ..., :exclude_type_and_descendents = t, ...
around ..., :exclude_types_and_descendents = [t, ...]
around ..., :exclude_object = o, ...
around ..., :exclude_objects = [o, ...]
around ..., :exclude_method = m, ...
around ..., :exclude_methods = [m, ...]
around ..., :exclude_attribute = a, ...
around ..., :exclude_attributes = [a, ...]
All the same synonyms for :pointcuts, :named_pointcuts, :types, :objects, and :methods apply here as well (after the "exclude_" prefix).
You can advice methods before execution:
before :types => ...
You can advice methods after returning successfully (i.e., no exceptions were raised):
after_returning :types => ...
after_returning_from :types => ... # synonym
You can advice methods after raising exceptions:
after_raising :types => ... # After any exception is thrown
after_raising_within :types => ... # synonym
after_raising => MyError, :types => ... # Only invoke advice if "MyError" is raised.
after_raising => [MyError1, MyError2], :types => ...
# Only invoke advice if "MyError1" or "MyError2" is raised.
You can advice methods after returning successfully or raising exceptions. (You can't specify
a set of exceptions in this case.):
after :types => ...
after_raising_within_or_returning_from : types => # synonym
You can advice methods both before after. This is different from around advice, where the around advice has to explicitly invoke the join point (using JoinPoint#proceed). Instead, the before-and-after methods are convenience wrappers around the creation of separate before advice and the corresponding after advice.
before_and_after :types =>, ...
before_and_after_returning :types =>, ...
before_and_after_returning_from :types =>, ... # synonym
before_and_after_raising :types =>, ...
before_and_after_raising_within :types =>, ... # synonym
before_and_after_raising_within_or_returning_from :types =>, ... # synonym
If you pass a block to Aspect.new, it will be the advice. When invoked, the advice will be passed the following three arguments,
1) the JoinPoint, which will contain a JoinPoint::Context object with useful context information,
2) the object being sent the current message, and
3) the parameters passed with the original message.
Recall that a Proc doesn't check the number of arguments (while lambdas do), so if you don't care about any of the trailing parameters, you can leave them out of the parameter list. Recall that the other difference between the two is that a return statement in a Proc returns from the method that contains it. As rule, do NOT use return statements in advices!
around :type => [...], :methods => :all do |join_point, object, *args|
advice_to_execute_before_the_jp
result = join_point.proceed # Invoke the join point, passing *args implicitly (you can override...)
advice_to_execute_after_the_jp
result # return the result of the "proceed", unless you override the value.
end
around(:type => [...], :methods => :all) {|join_point, object, *args| ...} # (...) necessary for precedence...
In the example, we show that you must be careful to return the correct value, usually the value returned by "proceed" or a value created by the block itself.
Note, prior to V0.2.0, the advice argument list was |join_point, *args|. Aquarium will look for such obsolete signatures (by looking at the arity of the proc) and raise an exception, if found. This check will be removed in a future release.
Rather than passing a block as the advice, you can pass a previously-created Proc:
around :type => [...], :methods => :all, :advice => advice
around :type => [...], :methods => :all, :advise_with => advice # synonym for advice. Note the "s"!
around :type => [...], :methods => :all, :call => advice # synonym for advice.
around :type => [...], :methods => :all, :invoke => advice # synonym for advice.
Finally, when running in JRuby, you can advise Java types! See the examples in the separate RSpec suite in the "jruby" directory and the discussion above concerning known limitations.
=== Packages
Aquarium::Aspects contains the Aspect class and supporting classes Pointcut, JoinPoint, etc.
Aquarium::Finders provides tools for locating types, objects, and methods in the runtime, using names, symbols, or regular expressions.
Aquarium::Extensions provides extensions to several Ruby core library routines.
Aquarium::Utils provides general-purpose utilities for manipulating Strings, Sets, Hashes, etc. as well as some generic types.
Aquarium::Extras provides add-ons for Aquarium, such as a Design by Contract implementation. These extras are NOT included when you require the general 'aquarium.rb' file. You have to explicitly include 'aquarium/extras' or one of the 'aquarium/extras/*' if you want to use them.
== Installation
The simplest approach is to install the gem:
gem install -y aquarium # sudo may be required on non-Windows systems
== Building the Aquarium gem
If you prefer to build the gem locally, clone from GitHub,
git clone git://github.com/deanwampler/Aquarium.git
Then do the following:
rake gem
gem install pkg/aquarium-x.y.z.gem # sudo may be required
== Running Aquarium's RSpec Specs
In order to run Aquarium's full suite of specs (rake pre_commit) you must install the following gems and tools:
* rake # Runs the build script
* rspec # Used instead of Test::Unit for TDD
* rcov # Verifies that the code is 100% covered by specs
* webgen # Generates the static HTML website
* coderay # Required by webgen
* diff-lcs # Required if you use the --diff switch
* win32console # Required by the --colour switch if you're on Windows
* meta_project # Required in order to make releases at RubyForge
* heckle # Required if you use the --heckle switch
* jruby # Required if run the separate spec suite in the "jruby" directory (v1.3+)
Once those are all installed, you should be able to run the suite with the following steps:
* git clone git://github.com/deanwampler/Aquarium.git
* cd aquarium
* rake spec
or
* rake spec_rcov # also runs rcov
Note that Aquarium itself - once built - doesn't have any dependencies outside the Ruby core and stdlib.
If you want to run the tests for the JRuby support, you must also have JRuby 1.1RC2 or later installed. To run the specs for JRuby, use the command
* rake verify_jruby
This command runs the standard Aquarium specs using JRuby instead of MRI, then runs a separate set of specs in the "jruby/spec" directory which test Aquarium with Java classes inside JRuby.
See http://aquarium.rubyforge.org for further documentation.
=== Acknowledgments
My colleagues in the AOSD community, in particular those who developed AspectJ, have been a big inspiration.
The RSpec team, in particular David Chelimsky, have really inspired my thinking about what's possible in Ruby, especially in the realm of DSLs. I also cribbed parts of the RSpec Rake process ;)
My colleagues at Object Mentor are an endless source of insight and inspiration.
Finally, a number of users have contributed valuable feedback. In particular, thanks to Brendan L., Matthew F., and Mark V.
Changes:
== Version 0.5.0
V0.5.0 adds support for Ruby 1.9.2, RSpec 2.5, Webgen (0.5.10 - for the build process), and partial support for JRuby 1.5.6.
WARNING: Ruby 1.9.1 is not supported. JRuby support is limited to "pure" Ruby code; the main RSpec specifications pass under JRuby, but not the JRuby-specific specifications in jruby/spec. Restoring full JRuby support is planned, but the timeframe is TBD.
It also has some minor API changes summarized below.
Enhancements:
22558 Support Ruby 1.9.1
27235 Support RSpec 1.2.8 (latest) and and JRuby 1.3.1 (latest)
27236 Upgrade website generation to webgen 0.5.X
The actual versions of these tools supported are more recent versions...
There are no API changes in this release, with two exceptions.
First, Most of the code changes are minor fixes to RSpec files. The one exception is an apparent change in JRuby 1.3.1 w.r.t. the access restrictions on Java methods. It now appears that protected methods are public, that is they are in the method list returned by :public_methods, but not :protected_methods. Also, private methods are not returned by any of the :*_methods.
Second, there was a deprecated option to the MethodFinder class, :options, that has been removed. Use :method_options instead.
I used the upgrade to Webgen 0.5.X as an excuse to refresh the web site's appearance. The content is mostly the same as before.
== Version 0.4.3
V0.4.3 adds a few bug fixes and enhancements, and several internal
refactorings to improve performance, etc.
Bug fixes:
[none]
Enhancements:
14165 Support Ruby 1.8.2, 1.8.5, 1.8.6, 1.9.X and 2.0 releases and snapshots
21283 Allow "_of" suffix at end of "*_and_descendents", "*_and_ancestors",
and "*_nested_types"
21924 Should be able to turn off contract-testing aspects globally in the
Design by Contract extra module
21993 By default, don't advise "system" methods that being with "_", but
provide an option to do so
Details:
14165: I closed this one because it's too broad. Aquarium currently runs with
1.8.6. I will open a new enhancement specifically for 1.9.
21283: Sometimes appending "_of" makes the specification read more smoothly,
so it's now an option.
21924: To class-level methods in DesignByContract, enable_all and disable_all,
globally turn contracts on or off. See their documentation for details.
21993: Now, by default, MethodFinder won't match any method that starts with
two underscores ("__"), so you don't have to :exclude_ancestors as much just
to avoid matching methods like "__id__" and "__send__". There is also a new
:method_options called :include_system_methods that will not suppress
matching these methods. So, I'm calling these special methods "system
methods". I don't yet provide a way to configure this list (RSpec methods
would be good additions...). The list is currently defined as an array of
regex's, MethodFinder::IGNORED_SYSTEM_METHODS. NOTE: This is effectively a
behavior change, although it's not likely to affect anyone.
== Version 0.4.2
V0.4.2 adds a few bug fixes and enhancements, greatly improved RDoc output,
and internal refactorings to improve performance.
Bug fixes:
15202 Intermittent confusion between classes and objects when invoking advice
19262 Just putting join_point argument in advice block causes mysterious
method missing error.
19321 Advice overhead is too high (ongoing improvements)
Enhancements:
13403 Support recursion into nested types without requiring "tricky" regular
expressions
13406 "Sugar" for adding methods and attributes to types
18537 Provide a search for all pointcut constants
19666 Improve the RDoc output
19119 Provide an "after_raising" type of advice that lets you handle the
exception.
#15202: I never figured out the cause of this problem, but it hasn't been seen
since late last
year, so I suspect it disappeared as a side effect of on-going refactoring and
enhancements.
#19262: If you just specified "|jp|" for an advice block, you would sometimes
get a method missing error. I never figured out exactly why, but it was
somehow related to passing the usual three arguments internally, where the
last two would be ignored in this case. Now, the code checks the arity and
only passes the join point in this case.
#19321: I removed some of the wasted object creation and initialization in
advice invocation,
improving the overhead by about 40%. However, it is still at least 10x higher
than simple method aliasing, so I want to make more improvements. (I did not
close this item.)
#13403: I added new options :types_and_nested_types and :types_and_nested that
are analogous to the similar "ancestors" and "descendents" options. The
nested option will return the specified types and any types they "enclose".
There are also corresponding "exclude" options.
#13406: I've decided not to do this, as it really isn't the "simplest thing
that could possibly work." It's easy enough for the user to define a module
of new behavior and just use "obj.extend(module)". However, when the user
needs to do this over a set of types, Aquarium's TypeFinder can be helpful,
so I added an example to the Examples code and page.
#18537: I've provided an example of the design approach where you define
pointcuts in a class, as a kind of "aspect interface" and write aspects that
specify those pointcuts. The problem has been that you had to name every
single such pointcut explicitly. There was no "finder" option, as for types,
methods, etc. Now there is a pointcut finder option with a new option
":named_pointcuts" for Aspect.new to specify a search for pointcuts in a set
of types matching a name specification. Either constants or class variables
will be matched (or both).
#19666: The rdoc for the key classes was cleaned up so it "renders" better.
Feedback is welcome.
#19119: I finished the previously-incomplete support for allowing advice to
change the raised exception, in both after and after_raising advice. A
common scenario is to wrap the thrown exception in another. For example, a
low-level, service-specific exception (like a database error) in a
higher-level, more generic application exception.
You still can't rescue the exception completely in :after_raising and :after
advice; the value for the exception in
joinpoint.context.raised_exception
when the advice returns will be raised by Aquarium. I think that permitting
:after_raising or :after advice to "eat" the exception could cause subtle
issues with scope and variable binding. It would also probably violate the
"principle of least surprise"; the advice code that rescues the exception would
not be as "obvious" to the reader as the familiar idiom of rescue clauses
that we are accustomed to using. Therefore, if you want to recover completely
from an exception, use rescue clauses in around advice.
== Version 0.4.1
V0.4.1 adds a few bug fixes, a few more user examples, internal refactoring and
some performance improvements.
Bug fixes:
19116 When an exception is thrown during advice execution, the error message
always reports the advice type is :before!
19261 after_raising DSL method provides no way to specify exceptions
Enhancements:
18705 Remove duplication and complexity in options-handling code
19320 Move the Aquarium::...::AspectDSL file to Aquarium::DSL for convenience
19399 Improve the Design by Contract example
I added a new :exceptions argument (synonym :exception) that takes a single
exception or list thereof. You can only use this argument with :after_raising.
If you specify exceptions with the latter and use the :exceptions argument,
the values will be merged.
I thought it was ugly to have to type "include
Aquarium::Aspects::DSL::AspectDSL", so I moved the code so now it's "include
Aquarium::DSL". However, for backwards compatibility, the old module still
works.
== Version 0.4.0
V0.4.0 adds specs to characterize and test advising Java classes when running
on JRuby and adds several API enhancements.
Bug fixes:
17844 JRuby - Advising types, Aquarium thinks the type is a string
17883 Workaround for JRUBY-2089
18090 JoinPoint#invoke_original_join_point only works with :around advice
Enhancements:
17834 Allow :class and :module (and variants) wherever :type is allowed
17881 Add specs that exercise advising Java types and objects using JRuby
#17844 occurred because of the way JRuby encodes Java packages into modules.
Aquarium now properly handles JRuby types.
#17883 reflected a JRuby bug, so a workaround was required.
#17834 allows the user to substitute the words "class", "classes", "module" or
"modules" anywhere the words "type" and "types" are used in the API, since some
users might naturally want to write aspects like this:
around :calls_to => :my_method, :in_class => MyClass do ...; end
However, there is no enforcement to ensure that "class" is only used for
classes and "module" is only used for modules, etc. Note: it's possible we'll
enforce this in some future release, as a way of saying things like "only advise
classes that match ...", etc. Caveat Emptor!
For #18090, a bug prevented JoinPoint#invoke_original_join_point (which allows
you to bypass all advices at the join point) from working except for :around
advice (and yes, the specs didn't cover this adequately - gasp!). Now fixed.
For #17881, I created a separate set of specs for JRuby, so it's easier to run
the "regular" Aquarium specs using MRI and the JRuby-specific specs separately
with JRuby. The new "jruby" directory contains a Rakefile, another set of specs,
and Java example code for the specs to use. The default Rakefile task re-runs
the main Aquarium spec suite using JRuby, to confirm that the suite passes
successfully, then it runs a set of different specs that load sample Java
interfaces and classes into JRuby and then advises them with Aquarium.
I found a few JRuby bugs and other behavior differences between MRI during this
release. I was able to handle them with modifications to the Aquarium code. If
you "grep" the Aquarium "lib" and "spec" directories for the word "jruby"
(ignoring case), you'll find notes about these issues and the workarounds I
implemented.
The separate JRuby spec suite shows what you can and cannot do with Java types.
As a side benefit, it also demonstrates how Java types, objects, and methods
are mapped to Ruby. There are some important limitations, however. See the
jruby.html page on the website or the README for more details.
Note: JRuby 1.1RC2 was used.
== Version 0.3.1
V0.3.1 adds numerous performance improvements, especially in the RSpec suite,
and some minor API additions.
Bug fixes:
N.A.
Enhancements:
14447 Unify internal handling of reporting errors to the user
17514 Provide an Aquarium library-wide logger with configuration parameters and
instance overrides
17515 Add an optional warning if an aspect doesn't match any join points
17516 Remove unnecessary examples that use :types_and_descendents to shorten
time to run the RSpec suite
17565 JoinPoint.new should convert a type name, symbol, or regex to the type
and only allow one type
These first two enhancements are related. There is a now an
Aquarium::Utils::DefaultLogger module with static accessors for getting and
setting the "system-wide" logger.
When instance-level overrides are necessary, the Aquarium::Utils::OptionsUtils
provides "universal" options (but currently used only by Aspect and Pointcut)
for specifying a logger (with the new :logger parameter), or alternatively,
specifying just the output stream (:logger_stream) and/or the severity
(:severity, one of the standard library's Logger::Severity-defined constants).
If either of the latter two options is specified, a separate logger instance is
created, rather than changing parameters for the global logger.
OptionsUtils also supports a :noop parameter, which classes interpret to mean
do none (or perhaps only some) of the processing. Useful for debugging.
#17515 adds a helpful warning to the system (or aspect-instance's) logger if
an aspect matches no join points. This warning will be suppressed if (i) the
severity level for the logger is above WARN or (ii) the aspect was created
with the option :ignore_no_matching_join_points => true.
#17516 fixes halved the long execution times for the whole RSpec suite by
refactoring some examples that used type finders with the :types_and_descendents
option unnecessarily. It is a very intensive computation! Note that I stubbed
out these calls using an aspect with around advise, a useful "development-time"
aspect. See Aquarium::TypeUtilsStub (in spec_example_types.rb) and how it's used
in pointcut_spec.rb. Using this technique and other optimizations, the time to
run the suite was reduced from ~5 minutes to about 1 minute.
#17565 fixes a "hole" in JoinPoint, where it doesn't confirm that a specified
type string, symbol or regex matches a class that exists and only one class. Now
it does and it stores the type, rather than the original "specification" for it.
== Version 0.3.0
V0.3.0 adds numerous improvements to the DSL, making aspect specification more
intuitive and English-like. For example, where you previously wrote, e.g.,
around :methods => :all, :types => [Foo, Bar], :advice => advice_proc
after :attribute => name, :attribute_options => [:readers], :objects => [foo, bar] ...
Now you can write the same aspects as follows:
around :calls_to => :all_methods, :within_types => [Foo, Bar], :use_advice => advice_proc
after :reading => name, :on_objects => [foo, bar] ...
Other improvements include performance and robustness enhancements and
miscellaneous internal refactoring and DRY improvements.
Bug fixes:
16267 gem not updating
Enhancements:
17154 More intuitive synonyms for specifying types, methods and attributes
For #17154, the following changes were made:
Added :all_methods as a synonym for the :all special value.
Added :reading as a synonym for :attributes => ..., :attribute_options =>
[:readers]
Added :writing and :changing as synonyms for :attributes => ...,
:attribute_options => [:writers]
Added :accessing as a synonym for :attributes => ...
Added :calls_to, :calling, :invoking, :sending_message_to as synonyms for
:methods.
Added :on_types, :in_types, :within_types and :for_types as synonyms for :types.
The same set of prefixes is supported for :type, :objects, :object, and the
various :exclude_*, :*_and_ancestors, and :*_and_descendents.
The full list of possible synonyms are shown in the spec examples. In
particular, see "pointcut_spec.rb".
== Version 0.2.0
V0.2.0 changes the parameter list used for advice blocks and adds numerous
enhancements, robustness improvements and more complete "spec'ing".
Bug fixes:
none
Enhancements:
13402 Support a subclass syntax like AspectJ's "Type+".
13984 More flexible argument list to the advise block.
14053 Remove JoinPoint#type, JoinPoint#type=, JoinPoint#object, and
JoinPoint#object=
14061 Add a control flow mechanism to skipping (sic) intermediate advice
15164 Deprecate ObjectFinder
15413 Remove ObjectFinder
15710 Eliminate redundant public methods in various "finders"
#13402 adds new invocation options to specify types and their descendents
(subclasses or modules that include the specified module(s)) and ancestors.
The latter should be used cautiously as it will include things like Kernel,
Object, and Class! I used new command options rather than the AspectJ "+"
suffix (and the proposed, but never implemented "-" suffix for ancestors),
because the "+" would be confusing with regular expressions and not in the
spirit of trying to make the pointcut language "easy to read". So, the
following are now available:
:type_and_ancestors
:types_and_ancestors
:type_and_descendents
:types_and_descendents
And the corresponding:
:exclude_type_and_ancestors
:exclude_types_and_ancestors
:exclude_type_and_descendents
:exclude_types_and_descendents
If you want both the ancestors and descendents, just use both options with
the same value.
#13984 adds the object as the second argument to the advice block parameter
list. This change reflects the fact that the object is often needed, but
calling jp.context.advised_object is a bit tedious. THIS CHANGE BREAKS
BACKWARDS COMPATIBILITY!! An exception is raised if advice has the signature
|jp, *args|.
#14061 adds a new method, JoinPoint#invoke_original_join_point, which will
invoke the original method without intermediate advice. If called within
around advice, you can write advice that vetoes all subsequent advice, yet
invokes the original method. Use this technique cautiously, however, since you
may not always know what other advices are involved and what side effects this
control-flow change might cause.
#15164 and 15413 remove ObjectFinder because it is not used and it requires
ObjectSpace, which has high overhead and won't be enabled, by default, in
JRuby (it will be optional).
#15710 removes redundant methods that were becoming a maintenance issue, in
particular, MethodFinder#find_all_by and TypeFinder#find_by_name. This is a
non-backwards-compatible API change.
Finally, note that I have not yet been able to resolve bug #15202, "Intermittent
confusion between classes and objects when invoking advice." I believe this is a
very rare occurrence and only likely to ever happen during the "torture-test" of
running the RSpec suite. Please post a comment to Tracker if you encounter it!
== Version 0.1.8
V0.1.7 did not successfully "register" at rubyforge. This releases fixes that
problem and also adds several feature enhancements and refactorings. There are
no known upgrade issues.
Bug fixes:
none
Enhancements:
13399 Add :exclusion options for methods and types.
14707 :exclude_ancestor_methods as synonym for :suppress_ancestor_methods
13399 adds new :exclude_(pointcuts|join_points|types|objects|methods|attributes)
options for Aspect.new, and Pointcut.new that make it easier to specify a list
or regular expression for various "items" and then to exclude particular
items, e.g., Aspect.new :around, :types => /nterestingType/, :exclude_types =>
UninterestingType ...
The :exclude_ancestor_methods option is now preferred over
:suppress_ancestor_methods, since the former is more consistent with the new
:exclude_* options.
== Version 0.1.7
Bug fixes:
14946 Advice fails when instrumenting methods containing special characters
15038 Spec for pointcut example variation #1
15039 Spec for pointcut example variation #2
15085 Specifying just :attributes for aspects also matches all methods, as if
:methods => :all specified
Enhancements:
13396 Unify internal handling of types vs. objects
15038 and 15039 were bugs in one of the examples (actually in the comments).
However, experimenting with them also revealed the nasty 15085 bug!
I previously handled some special characters in method names, but not all the
possible ones, hence 14946. Aquarium should now properly handle any valid Ruby
method name.
== Version 0.1.6
Bug fixes:
14353 Advising subclass method that calls super raises exception when method
executed
14356 Regexps for types must cover the whole name, which is inconsistent with
method/attribute regexps
14384 Design by Contract "extra" does not return correct value "invar" handling
13410 Fix funky navigation bar on website
14353 was kind of bad, but it's actually a Ruby bug with a good workaround. If
you advised a method that called "super", Ruby would use the wrong method name
to lookup the class in the parent. See the bug description for the details.
For 14356, type regular expressions now match on parts of names; they don't have
to match the whole name. The exception is regular expressions with module
separators "::". In this case, it seems to make more sense for the regular
expression to be interpreted as follows: If the expression is /A::B::C::D/,
then for the the outermost types, the expression behaves as /^.*A/, for the
types between two "::", the expressions behave as /^B$/ and /^C$/, and the
trailing expression behaves as /D.*$/.
14384 was an easy mistake to make with "around" advice; you have to remember to
return the result of the "join_point.proceed" call, unless you specifically
want to change the returned value! Here are two ways to do it:
do_something_before(...)
result = join_point.proceed
do_something_after(...)
return result
or
begin
do_something_before(...)
join_point.proceed
ensure
do_something_after(...)
end
The latter approach looks "asymmetrical" and it will behave differently if
"proceed" raises! However, it eliminates the temporary, if you find that
desirable.
Enhancements:
13407 Pick a better method name for JoinPoint#type, which hides the Module#type
14385 Pointcut.new should accept a :join_point => jp argument
14386 Aspect.new ..., :pointcut => should accept a join point object
14440 Add good warning message when "proceed" used for non-around advice
For 13407, new attribute methods have been added
* JoinPoint#target_type return the type that the join_point matches.
* JoinPoint#target_type= set the type that the join_point matches.
* JoinPoint#target_object return the object that the join_point matches.
* JoinPoint#target_object= set the object that the join_point matches.
The following, older methods are now deprecated and will be removed in the 0.2.0
release (#14053):
* JoinPoint#type
* JoinPoint#type=
* JoinPoint#object
* JoinPoint#object=
JoinPoint#type method is deprecated because it hides Module#type, which returns
the type of the corresponding object. For "symmetry", the other three methods
are also now deprecated and they will be removed in a future release. Until
then, all will print a warning message to STDOUT. (If you really want the type
of what could be a JoinPoint object, you should use #class anyway, as
Module#type is also deprecated!)
== Version 0.1.5
Bug fixes:
13514 Protected and private methods are made public when advised and left that
way when unadvised
13650 Loading Aquarium interferes with Rails filters
13864 Bug with negative object_id
Enhancements:
13392 Convert examples to specs.
13463 Support running in JRuby
Fixing 13650 required an API change, which is why I've tagged this release
"0.1.5" instead of something like "0.1.1" (and the changes don't seem big enough
to warrant "0.2.0"...).
Previously, requiring "aquarium.rb" in the top-level "lib" directory would
implicitly require lib/aquarium/dsl/aspect_dsl.rb, which has Object include the
AspectDSL module. This module adds methods like :before and :after to Object.
Unfortunately, those methods collide with methods of the same name that Rails
adds to Object. It was also a bit presumptuous of me to assume that everyone
wanted those methods on Object ;)
In this release, aspect_dsl.rb is still implicitly included and it still defines
the AspectDSL module. Now, however, it does not include the AspectDSL module in
Object. Instead, if you want this behavior for all types, you must require the
new lib/aquarium/aspects/dsl/object_dsl.rb explicitly.
As an alternative, if you just want the AspectDSL module included selectively in
certain types, then do the following:
class MyClass # reopen "MyClass"
# Add the methods as _class_ methods
include Aquarium::DSL
end
or, use (class|module)_eval:
require 'aquarium/dsl/aspect_dsl'
MyClass.class_eval do
# Add the methods as _class_ methods
include Aquarium::DSL
end
To add the methods as _instance_ methods on individual objects:
object = MyClass.new
object.extend(Aquarium::DSL)
Note: as discussed at
http://practicalruby.blogspot.com/2007/02/reopen-with-moduleeval.html,
using "class_eval" or "module_eval" is safer that just reopening a class if
you're not sure that "MyClass" has actually been defined yet. However, in our
particular case, it probably doesn't matter, as AspectDSL doesn't change
anything about the type, like aliasing existing methods. Still, we can't
guarantee that this won't change in the future.
== Version 0.1.0
This is the initial version.
|