In order to fully leverage the power of Groovy, it’s beneficial to have a general understanding of how it works inside. It is not necessary to know all the details, but familiarity with the overall concepts will allow you to work more confidently in Groovy and find more elegant solutions.
This section provides you with a peek inside how Groovy performs its magic. The intent is to explain some of the general concepts used under the covers, so that you can write solutions that integrate more closely with Groovy’s inner runtime workings. Groovy has numerous interception points, and choosing between them lets you leverage or override different amounts of the built-in Groovy capabilities. This gives you many options to write powerful yet elegant solutions outside the bounds of what Groovy can give you out of the box. We will describe these interception points and then provide an example of how they work in action.
The capabilities described in this section collectively form Groovy’s implementation of the Meta-Object Protocol (MOP). This is a term used for a system’s ability to change the behavior of objects and classes at runtime—to mess around with the guts of the system, to put it crudely.
Understanding the MetaClass concept:
In Groovy, everything starts with the GroovyObject interface, which, like all the other classes we’ve mentioned. It looks like this:
- public interface GroovyObject {
- public Object invokeMethod(String name, Object args);
- public Object getProperty(String property);
- public void setProperty(String property, Object newValue);
- public MetaClass getMetaClass();
- public void setMetaClass(MetaClass metaClass);
- }
Note.
GroovyObject has an association with MetaClass , which is the navel of the Groovy meta concept. It provides all the meta-information about a Groovy class, such as the list of available methods, fields, and properties. It also implements the following methods:
- Object invokeMethod(Object obj, String methodName, Object args)
- Object invokeMethod(Object obj, String methodName, Object[] args)
- Object invokeStaticMethod(Object obj, String methodName, Object[] args)
- Object invokeConstructor(Object[] args)
The MetaClass is stored in and retrieved from a central store, the MetaClassRegistry. Figure 7.2 shows the overall picture (keep this picture in mind when thinking through Groovy’s process of invoking a method).
The structure as depicted in figure 7.2 is able to deal with having one MetaClass per object, but this capability is not used in the default implementations. Current default implementations use one MetaClass per class in the MetaClassRegistry. This difference becomes important when you’re trying to define methods that are accessible only on certain instances of a class (like singleton methods in Ruby).
Note.
Method invocation and interception:
Groovy generates its Java bytecode such that each method call (after some redirections) is handled by one of the following mechanisms:
The decision is taken by an Invoker singleton that applies the logic as shown in figure 7.3. Each number in the diagram refers to the corresponding mechanism in the previous numbered list.
This is a relatively complex decision to make for every method call, and of course most of the time you don’t need to think about it. You certainly shouldn’t be mentally tracing your way through the diagram for every method call you make—after all, Groovy is meant to make things easier, not harder! However, it’s worth having the details available so that you can always work out exactly what will happen in a complicated situation. It also opens your mind to a wide range of possibilities for adding dynamic behavior to your own classes. The possibilities include the following:
The invocation logic suggests that there are multiple ways to implement intercepted, relayed, or pretended methods:
Generally speaking, overriding/implementing invokeMethod means to override the dot-methodname operator.
Method interception in action:
Suppose we have a Groovy class Whatever with methods outer and inner that call each other, and we have lost track of the intended calling sequence. We would like to get a runtime trace of method calls like:
- before method 'outer'
- before method 'inner'
- after method 'inner'
- after method 'outer'
Because this is a GroovyObject, we can override invokeMethod. To make sure we can intercept calls to our defined methods, we need to implement theGroovyInterceptable interface, which is only a marker interface and has no methods.
Inside invokeMethod , we write into a trace log before and after executing the method call. We keep an indentation level for tidy output. Trace output should go toSystem.out by default or to a given Writer , which allows easy testing. We achieve this by providing a writer property.
To make our code more coherent, we put all the tracing functionality in a superclass Traceable . Listing 7.25 shows the final solution.
- Listing 7.25 Trace implementation by overriding invokeMethod
- import org.codehaus.groovy.runtime.StringBufferWriter
- import org.codehaus.groovy.runtime.InvokerHelper
- class Traceable implements GroovyInterceptable { // Tagged superclass
- Writer writer = new PrintWriter(System.out) // Default : stdout
- private int indent = 0
- Object invokeMethod(String name, Object args){
- writer.write("\n" + ' '*indent + "before method '$name'")
- writer.flush()
- indent++
- def metaClass = InvokerHelper.getMetaClass(this)
- def result = metaClass.invokeMethod(this, name, args) // 1) Execute call
- indent--
- writer.write("\n" + ' '*indent + "after method '$name'")
- writer.flush()
- return result
- }
- }
- class Whatever extends Traceable { // Production class
- int outer(){
- return inner()
- }
- int inner(){
- return 1
- }
- }
- def log = new StringBuffer()
- def traceMe = new Whatever(writer: new StringBufferWriter(log)) // Test settings
- assert 1 == traceMe.outer() // 2) Start
- assert log.toString() == """
- before method 'outer'
- before method 'inner'
- after method 'inner'
- after method 'outer'"""
Unfortunately, this solution is limited. First, it works only on GroovyObjects , not on arbitrary Java classes. Second, it doesn’t work if the class under inspection already extends some other superclass. Recalling figure 7.3, we need a solution that replaces our MetaClass in the MetaClassRegistry with an implementation that allows tracing. There is such a class in the Groovy codebase: ProxyMetaClass.
This class serves as a decorator over an existing MetaClass and adds interceptablility to it by using an Interceptor (see groovy.lang.Interceptor in the Groovy Javadocs). Luckily, there is a TracingInterceptor that serves our purposes. Listing 7.26 shows how we can use it with the Whatever2 class.
- Listing 7.26 Intercepting method calls with ProxyMetaClass and TracingInterceptor
- package inaction.ch7
- import org.codehaus.groovy.runtime.StringBufferWriter
- class Whatever2 {
- int outer(){
- return inner()
- }
- int inner(){
- return 1
- }
- }
- def log = new StringBuffer("\n")
- def tracer = new TracingInterceptor() // Construct the Interceptor
- tracer.writer = new StringBufferWriter(log)
- def proxy = ProxyMetaClass.getInstance(Whatever2.class) // Retrieve a suitable ProxyMetaClass
- proxy.interceptor = tracer
- proxy.use { // Determine scope for using it
- assert 1 == new Whatever2().outer() // Start execution
- }
- assert log.toString() == """
- before inaction.ch7.Whatever2.ctor()
- after inaction.ch7.Whatever2.ctor()
- before inaction.ch7.Whatever2.outer()
- before inaction.ch7.Whatever2.inner()
- after inaction.ch7.Whatever2.inner()
- after inaction.ch7.Whatever2.outer()
- """
For GroovyObjects that are not invoked via the MetaClassRegistry , you can pass the object under analysis to the use method to make it work:
- proxy.use(traceMe){
- // call methods on traceMe
- }
Supplement:
* User Guide > Dynamic Groovy
沒有留言:
張貼留言