Search code examples
grailsgroovymetaprogramming

Groovy method interception


In my Grails app I've installed the Quartz plugin. I want to intercept calls to every Quartz job class' execute method in order to do something before the execute method is invoked (similar to AOP before advice).

Currently, I'm trying to do this interception from the doWithDynamicMethods closure of another plugin as shown below:

def doWithDynamicMethods = { ctx ->
    // get all the job classes
    application.getArtefacts("Job").each { klass ->

        MetaClass jobMetaClass = klass.clazz.metaClass

        // intercept the methods of the job classes
        jobMetaClass.invokeMethod = { String name, Object args ->

            // do something before invoking the called method
            if (name == "execute") {
                println "this should happen before execute()"
            }

            // now call the method that was originally invoked
            def validMethod = jobMetaClass.getMetaMethod(name, args)

            if (validMethod != null) {
                validMethod.invoke(delegate, args)
            } else {
                jobMetaClass.invokeMissingMethod(delegate, name, args)
            }
        }
    }
}

So, given a job such as

class TestJob {
    static triggers = {
      simple repeatInterval: 5000l // execute job once in 5 seconds
    }

    def execute() {
        "execute called"
    }
}

It should print:

this should happen before execute()
execute called

But my attempt at method interception seems to have no effect and instead it just prints:

execute called

Perhaps the cause of the problem is this Groovy bug? Even though the Job classes don't explicitly implement the org.quartz.Job interface, I suspect that implicitly (due to some Groovy voodoo), they are instances of this interface.

If indeed this bug is the cause of my problem, is there another way that I can do "before method interception"?


Solution

  • Because all the job classes are Spring beans you can solve this problem using Spring AOP. Define an aspect such as the following (adjust the pointcut definition so that it matches only your job classes, I've assumed they are all in a package named org.example.job and have a class name that ends with Job).

    @Aspect
    class JobExecutionAspect {
    
      @Pointcut("execution(public * org.example.job.*Job.execute(..))")
      public void executeMethods() {}
    
      @Around("executeMethods()")
      def interceptJobExecuteMethod(ProceedingJoinPoint jp) {
        // do your stuff that should happen before execute() here, if you need access
        // to the job object call jp.getTarget()
    
        // now call the job's execute() method
        jp.proceed() 
      }
    }
    

    You'll need to register this aspect as a Spring bean (it doesn't matter what name you give the bean).