Writing Java Aspects ... with JRuby and Aquarium! 2
Aquarium V0.4.0, my AOP library for Ruby, now supports JRuby. Not only do the regular “pure Ruby” Aquarium specs run reliably under JRuby (V1.1RC2), but you can now write aspects for Java types with Aquarium!
There are some important limitations, though. Cartographers of old would mark dangerous or unknown territory on their maps with hic sunt dracones (“here be dragons”), a reference to the old practice of adorning maps with serpents around the edges.
This is true of Aqurium + Java types in JRuby, too, at least for now.
Aquarium uses Ruby’s metaprogramming API extensively and the JRuby team has done some pretty sophisticated work to integrate Java types with Ruby. Hence, it’s not too surprising there are some gotchas. Hopefully, workarounds will be possible for all of them.
The details are discussed on the JRuby page, the README on the Aquarium site, and of course the “specs” in the distribution’s jruby/spec directory. I’ll summarize them here, after discussing the pros and cons of Aquarium vs. the venerable AspectJ and showing you an example of using Aquarium for Java.
Briefly, Aquarium’s advantages over AspectJ are these:
- You can add and remove advice dynamically at runtime. You can’t remove AspectJ advice.
- You can advise JDK types easily with Aquarium. AspectJ won’t do this by default, but this is really more of a legacy licensing issue than a real technical limitation.
- You can advise individual objects, not just types.
Aquarium’s disadvantages compared to AspectJ include:
- Aquarium will be slower than using AspectJ (although this has not been studied in depth yet).
- Aquarium’s pointcut language is not as full-featured as AspectJ’s.
- There are the bugs and limitations I mentioned above in this initial V0.4.0 release, which I’ll elaborate shortly.
Here is an example of adding tracing calls to a method doIt in all classes that implement the Java interface com.foo.Work.
Aspect.new :before, :calls_to => [:doIt, :do_it], :in_types_and_descendents => Java::com.foo.Work do |jp, obj, *args|
log "Entering: #{jp.target_type.name}##{jp.method_name}: object = #{object}, args = #{args.inspect}"
end
There are two important points to notice in this example:
- You can choose to refer to the method as
do_it(Ruby style) ordoIt, but these variants are effectively treated as separate methods; advice on one will not affect invocations of the other. So, if you want to be sure to catch all invocations, use both forms. There is a bug (18326) that happens in certain conditions if you use just the Java naming convention. - If the type is an interface, you must use
:types_and_descendents(or one of the supported variants on the wordtypes...). Since interfaces don’t have method implementations, you will match no join points unless you use the_and_descendentsclause. (By default, Aquarium warns you when no join points are matched by an aspect.) However, there is a bug (18325) with this approach if Java types are subtyped in Ruby.
Limitations and Bugs
Okay, here’s the “fine print”...
In this (V0.4.0) release, there are some important limitations.
- 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 Ruby subclass of the Java type and override the method(s) you want to advise. These overrides can just call
super. Note that it will also be necessary for instances of this Ruby type to be used throughout the application, in both the Java and Ruby code. So, you’ll have to instantiate the object in your Ruby code.
Yea, this isn’t so great, but if you’re motivated… ;)
There are also a few outstanding Aquarium bugs (which could actually be JRuby bugs or quirks of the Aquarium-JRuby “interaction”; I’m not yet sure which).
- Bug #18325: If you have Ruby subclasses of Java types and you advise a Java method in the hierarchy using
:types_and_descendents => MyJavaBaseClassOrInterfaceand you call unadvise on the aspect, the advice “infrastructure” is not correctly removed from the Ruby types. Workaround: Either don’t “unadvise” such Ruby types or only advise methods in such Ruby types where the method is explicitly overridden in the Ruby class. (The spec and the Rubyforge bug report provide examples.) - Bug #18326: Normally, you can use either Java- or Ruby-style method names (e.g.,
doSomethingvs.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), Aquarium acts like the JoinPoint is advised, but the advice is never actually called. Workaround: Use the Ruby-style name in this scenario.
So, there is still some work to do, but it’s promising that you can use an aspect framework in one language with another. A primary goal of Aquarium is to make it easy to write simple aspects. My hope is that people who might find AspectJ daunting will still give Aquarium a try.
CJUG West 9/6/07: Aspect-Oriented Programming and Software Design
I’m giving a talk at the Chicago Java User’s Group West meeting this Thursday at 6:30 PM. The topic is Aspect-Oriented Programming and Software Design in Java and AspectJ. I’ll briefly describe the problems that AOP addresses and how the principles of object-oriented design influence AOP and vice versa. If you’re in the area, I hope to see you there.
Applications Should Use Several Languages 10
Yesterday, I blogged about TDD in C++ and ended with a suggestion for the dilemma of needing optimal performance some of the time and optimal productivity the rest of the time. I suggested that you should use more than one language for your applications.
If you are developing web applications, you are already doing this, of course. Your web tier probably uses several “languages”, e.g., HTML, JavaScript, JSP/ASP, CSS, Java, etc.
However, most people use only one language for the business/mid tier. I think you should consider using several; a high-productivity language environment for most of your work, with the occasional critical functionality implemented in C or C++ to optimize performance, but only after actually measuring where the bottlenecks are located.
This approach is much too rare, but it has historical precedents. One of the most successful and long-lived software projects of all time is Emacs. It consists of a core C-based runtime with most of the functionality implemented in Emacs lisp “components”. The relative ease of extending Emacs using lisp has resulted in a rich assortment of support tools for various operating systems, languages, build tools, etc. Even modern IDEs and and other graphical editors have not completely displaced Emacs.
Java has embraced the mixed language philosophy somewhat reluctantly. JNI is the official and most commonly-used API for invoking “native” code, but it is somewhat hard to use and few people actually use it. In contrast, for example, the Ruby world has always embraced this approach. Ruby has an easy to use API for invoking native C code and good alternatives exist for invoking code in other languages. As a result, many of the 3rd-party Ruby libraries (or gems) contain both Ruby and native C code. The latter is built on the fly when you install the gem. Hence, there are many high-performance Ruby applications. This is not a contradiction in terms, because the performance-critical sections run natively, even though interpreted Ruby is relatively slow.
Of course, you have to be judicious in how you use mixed-language programming. Crossing the language boundary is often somewhat heavyweight, so you should avoid doing such invocations inside tight loops, for example.
So, I think the solution to the dilemma of needing high performance sometimes and high productivity the rest of the time is to pick the right tools for each circumstance and make them interoperate. Even constrained embedded devices like cell phones would be easier to implement if most of the code were written in a language like Ruby, Python, Smalltalk, or Java and performance-critical components were written in C or C++.
If I were starting such a greenfield project, I would assume that time-to-money is the top priority and write most of my code in Ruby (my personal current favorite), using TDD of course. I would profile it constantly, as part of the nightly or continuous-integration build. When bottlenecks emerge, I would first determine if a refactoring is sufficient to fix them and if not, I would rewrite the critical sections in C. If the project were for an embedded device, I would also watch the resource usage carefully.
For my embedded device, I would test from the beginning whether or not the overhead of the interpreter/VM and the overall performance are acceptable. I would also be sure that I have adequate tool support for the inevitable remote debugging and diagnostics I’ll have to do. If I made the wrong tool choices after all, I would know early on, when it’s still relatively painless to retool.
If you’re an IT or web-site developer, you have fewer performance limitations and more options. You might decide to make the cross-language boundary a cross-process boundary, e.g., by communicating through some sort of lightweight web services. This is one way to leverage legacy C/C++ code while developing new functionality in a more productive language.
Protecting Developers from Powerful Languages 7
Microsoft’s forthcoming C# version 3 has some innovative features, as described in this blog. I give the C# team credit for pushing the boundaries of C#, in part because they have forced the Java community to follow suit. ;)
A common tension in many development shops is how far to trust the developers with languages and tools that are perceived to be “advanced”. It’s tempting to limit developers to “safe” languages and maybe not all the features of those languages. This can be misguided.
Java is usually considered safe, but Java Generics are suspect. Strong typing is safe, but dynamic typing isn’t controlled enough. Closures and continuations sound too advanced and technical to be trusted in the hands of “our team”.
To be fair, larger organizations have more at stake and caution is prudent. Regrettably, it is also true that many people in our profession are … hmm … not that well qualified.
However, I find that I’m far more productive and less likely to make mistakes using Ruby iterators with closures than writing more verbose and inelegant Java.
I used to be a strong believer in static typing, but it has become a distraction, as I have to worry more about the types of method parameters and return values, rather than just worrying about the values themselves. I realized that, on average in a typical section of code, the actual type of a variable is unimportant. The variable is just a “handle” being passed around. The name is always important, as it is a form of documentation. There are places where the type is important, of course, when the variable is read or written in some way.
Finally, static typing offers less security than at first appears. At best, it only confirms that variables of particular types are used consistently. Your unit tests also do this. However, static typing can’t confirm that the usage of the API is correct. This is analogous to testing the syntax but not the semantics of the program. In fact, only unit tests (or alternatives, like rspec ) are effective at testing both.
So, it’s prudent to be reticent about newer languages and features, but make sure the decisions you make about them are backed up by careful evaluation and don’t forget to train your team appropriately!
