程式扎記: [ In Action ] Dynamic object orientation - Meta programming in Groovy

標籤

2014年5月5日 星期一

[ In Action ] Dynamic object orientation - Meta programming in Groovy

Preface: 
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: 
  1. public interface GroovyObject {  
  2.     public Object    invokeMethod(String name, Object args);  
  3.     public Object    getProperty(String property);  
  4.     public void        setProperty(String property, Object newValue);  
  5.     public MetaClass getMetaClass();  
  6.     public void      setMetaClass(MetaClass metaClass);  
  7. }  
All classes you program in Groovy are constructed by the GroovyClassGenerator such that they implement this interface and have a default implementation for each of these methods—unless you choose to implement it yourself. 
Note. 
If you want a usual Java class to be recognized as a Groovy class, you only have to implement the GroovyObject interface. For convenience, you can also subclass the abstract class GroovyObjectSupport , which provides default implementations.

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: 
  1. Object invokeMethod(Object obj, String methodName, Object args)  
  2. Object invokeMethod(Object obj, String methodName, Object[] args)  
  3. Object invokeStaticMethod(Object obj, String methodName, Object[] args)  
  4. Object invokeConstructor(Object[] args)  
These methods do the real work of method invocation, either through the Java Reflection API or (by default and with better performance) through a transparently createdreflector class. The default implementation of GroovyObject.invokeMethod relays any calls to its MetaClass

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. 
The MetaClass that a GroovyObject refers to and the MetaClass that is registered for the type of this GroovyObject in the MetaClassRegistry do not need to be identical. For instance, a certain object can have a special MetaClass assigned that differs from the MetaClass of all other objects of this class.

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: 
1. The class’s own invokeMethod implementation (which may further choose to relay it to some MetaClass )
2. Its own MetaClass , by calling getMetaClass().invokeMethod(…)
3. The MetaClass that is registered for its type in the MetaClassRegistry

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: 
* You can intercept method calls with cross-cutting concerns (aspects) such as logging/tracing all invocations, applying security restrictions, enforcing transaction control, and so on.
* You can relay method calls to other objects. For example, a wrapper can relay to a wrapped object all method calls that it cannot handle itself.
BY THE WAY This is what closures do. They relay method calls to their delegate.
* You can pretend to execute a method while some other logic is applied. For example, an Html class could pretend to have a method body , while the call was executed as print('body').
BY THE WAY This is what builders do. They pretend to have methods that are used to define nested product structures. This will be explained in detail in chapter 8.

The invocation logic suggests that there are multiple ways to implement interceptedrelayed, or pretended methods: 
* Implementing/overriding invokeMethod in a GroovyObject to pretend or relay method calls (all your defined methods are still called as usual).
* Implementing/overriding invokeMethod in a GroovyObject , and also implementing the GroovyInterceptable interface to additionally intercept calls to your defined methods.
* Providing an implementation of MetaClass , and calling setMetaClass on the target GroovyObjects.
* Providing an implementation of MetaClass , and registering it in the MetaClassRegistry for all target classes (Groovy and Java classes). This scenario is supported by the ProxyMetaClass.

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: 
  1. before method 'outer'  
  2.   before method 'inner'  
  3.   after  method 'inner'  
  4. after  method 'outer'  
to confirm that the outer method calls the inner method. 

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 
  1. import org.codehaus.groovy.runtime.StringBufferWriter  
  2. import org.codehaus.groovy.runtime.InvokerHelper  
  3.   
  4. class Traceable implements GroovyInterceptable {    // Tagged superclass  
  5.       
  6.     Writer writer = new PrintWriter(System.out)     // Default : stdout  
  7.     private int indent = 0  
  8.       
  9.     Object invokeMethod(String name, Object args){  
  10.         writer.write("\n" + '  '*indent + "before method '$name'")  
  11.         writer.flush()  
  12.         indent++  
  13.         def metaClass = InvokerHelper.getMetaClass(this)  
  14.         def result = metaClass.invokeMethod(this, name, args)   // 1) Execute call  
  15.         indent--  
  16.         writer.write("\n" + '  '*indent + "after  method '$name'")  
  17.         writer.flush()  
  18.         return result  
  19.     }  
  20. }  
  21.   
  22. class Whatever extends Traceable {                  // Production class  
  23.     int outer(){  
  24.         return inner()  
  25.     }  
  26.     int inner(){  
  27.         return 1  
  28.     }  
  29. }  
  30.   
  31. def log = new StringBuffer()  
  32. def traceMe = new Whatever(writer: new StringBufferWriter(log)) // Test settings  
  33. assert 1 == traceMe.outer()                         // 2) Start   
  34. assert log.toString() == """  
  35. before method 'outer'  
  36.   before method 'inner'  
  37.   after  method 'inner'  
  38. after  method 'outer'"""  
It’s crucial not to step into an endless loop when relaying the method call, which would be unavoidable when calling the method in the same way the interception method was called (thus falling into the same column in figure 7.3). Instead, we enforce falling into the leftmost column with the invokeMethod call at (3). 

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 
  1. package inaction.ch7  
  2.   
  3. import org.codehaus.groovy.runtime.StringBufferWriter  
  4.   
  5. class Whatever2 {  
  6.     int outer(){  
  7.         return inner()  
  8.     }  
  9.     int inner(){  
  10.         return 1  
  11.     }  
  12. }  
  13.   
  14. def log = new StringBuffer("\n")  
  15. def tracer = new TracingInterceptor()                   // Construct the Interceptor  
  16. tracer.writer = new StringBufferWriter(log)  
  17. def proxy = ProxyMetaClass.getInstance(Whatever2.class// Retrieve a suitable ProxyMetaClass  
  18. proxy.interceptor = tracer  
  19. proxy.use {                                             // Determine scope for using it  
  20.     assert 1 == new Whatever2().outer()                 // Start execution  
  21. }  
  22. assert log.toString() == """  
  23. before inaction.ch7.Whatever2.ctor()  
  24. after  inaction.ch7.Whatever2.ctor()  
  25. before inaction.ch7.Whatever2.outer()  
  26.   before inaction.ch7.Whatever2.inner()  
  27.   after  inaction.ch7.Whatever2.inner()  
  28. after  inaction.ch7.Whatever2.outer()  
  29. """  
Note that this solution also works with all Java classes when called from Groovy

For GroovyObjects that are not invoked via the MetaClassRegistry , you can pass the object under analysis to the use method to make it work: 
  1. proxy.use(traceMe){  
  2.     // call methods on traceMe  
  3. }  
Groovy is often perceived as a scripting language for the JVM, and it is. But making Java scriptable is not the most distinctive feature. The Meta-Object Protocol and the resulting dynamic nature elevate Groovy over other languages. 

Supplement: 
User Guide > Dynamic Groovy

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!