Skip to content
Philip Ford edited this page Sep 14, 2018 · 7 revisions

Introduction

Aspect-Oriented Programming (AOP) allows us to modularize code that may otherwise remain entangled across a number of methods and classes. For example, we may find ourselves doing the same security check around a number of different units of work, instantiating some variables before other units and logging some information after yet others have completed or thrown an exception. AOP allows us to write all this functionality once and have it applied to our methods at the appropriate point (before, after, around or when an exception is thrown), where once we may have violated the DRY principle and written it multiple times.

Typically, in Java, we would use one of two mechanisms. Either we would use a framework to modify the byte-code of our own classes to directly insert this functionality, or we would dynamically create a proxy for our own classes (either using JDK Dynamic Proxies or CGlib). In Groovy, as we shall see, we need neither technique.

AOP in Groovy

Groovy, a JVM dynamic language with a Java-like syntax, sports impressively powerful features that make mimicking AOP a breeze.

Groovy's Metaobject-Protocol (MOP) is a mechanism by which application developers can modify the implementation and behavior of their language. Groovy's MOP is particularly powerful. All dynamic method calls get routed through invokeMethod and property access through getProperty and setProperty. Thus, we have a single point of contact for modifying the core behavior of the Objects we create. By overriding invokeMethod, getProperty and setProperty we can intercept every single call to our Objects without the need for a proxy or byte-code manipulation.

Example

I used the example below in a unit test. I had a custom FileDecorator class that wrapped a File object. I wanted to avoid writes to files during the unit test. The code below overrides the behavior of all instances of the original class, and intercepts calls to the write(), getText(), and renameTo() methods, redirecting them to a datastore (a map) instead of to the File object and the file system.

class Proxy extends DelegatingMetaClass {

  def data = [:]

  Proxy(final Class cls) {
      	super(cls)
      	initialize()
  }

  public Object invokeMethod(Object obj, String method, Object[] args) {
      	String cls = obj.class.simpleName
      	println "before: $cls.$method, args:$args -->"
      	def val = null

        // Creating a map of instance data, with the absolute path of
        // the wrapped File as the key.
  	    if (!data.get(obj.cmp.getAbsolutePath())){
  	       data.put(obj.cmp.getAbsolutePath(), [
  	          text:obj.text
  	       ])
  	    }

        // Advising only the write, getText, and renameTo methods
      	if (method == 'write'){
			data.put(obj.cmp.getAbsolutePath(), [text: args[0]])
			return obj
      	} else if (method == 'getText'){
			def text = data[obj.cmp.getAbsolutePath()].text
			println "[getText]\n$text"
			return text
      	} else if (method == 'renameTo'){
      	    def text = data[obj.cmp.getAbsolutePath()].text
      	    obj.cmp = new File(args[0])
      	    data.put(obj.cmp.getAbsolutePath(),  [ text: text ])
      	    return data[obj.cmp.getAbsolutePath()]
      	} else if (method == 'readLines'){
      		return data[obj.cmp.getAbsolutePath()].text.split('\n')
      	} else {
	      	try {
	          	val = super.invokeMethod(obj, method, args)
	      	} catch(Exception e) {
	          	println "after: $cls.$method, has thrown:$e <--"
	          	throw e
	      	}
      	}
      	println "after: $cls.$method, return value:$val <--"
      	return val;
  	}

  	def static setClass(Class cls) {
      	cls.metaClass = new Proxy(cls)
  }
}

References

Clone this wiki locally