Intercept Grails Service class method calls
I was trying to intercept method calls of a Grails Service class for a little while. Adding interceptors to Controllers is really easy and I wanted to intercept calls to one of the methods in a Service class in a similar fashion. But adding interceptors to Grails Service Classes is not as straightforward as for Controllers. After doing some research I came up with two solutions.
Problem statement: Default flush mode in Grails 2.3 is AUTO, but for some Service Classes we needed the default flush mode as COMMIT. We can change default flush mode for whole application by specifying it in grails-app/conf/DataSource.groovy
. Rather than changing it for whole application we wanted to change it for some selected Service classes only.
How to do it?
Before going for solution, lets see our sample domain model and demo service class for our example.
Domain Class:
package com.ttnd.demo class Group { String name String assignedRepId Date activationDate Date deactivationDate static mapping = { table(name: "_group") } static constraints = { deactivationDate nullable: true } @Override public String toString() { return "Group{ $name(id: $id) }" } }
Service Class:
package com.ttnd.demo import grails.transaction.Transactional import groovy.util.logging.Log4j import org.hibernate.FlushMode @Log4j @Transactional class GroupService { def grailsApplication public Group persistGroup(Group group) { FlushMode flushMode = grailsApplication.mainContext.sessionFactory.currentSession.flushMode log.debug("Flush mode for current Session: ${flushMode}") if (group.validate()) { group.save(failOnError: true) } else { log.error("Error while saving Group Object $group") } return group } }
Bootstrap some data:
import com.ttnd.demo.Group import com.ttnd.demo.GroupService class BootStrap { GroupService groupService def init = { servletContext -> Group group = new Group(name: "Admin", assignedRepId: "USR-00-12", activationDate: new Date()) groupService.persistGroup(group) } def destroy = { } }
1. First Solution – Using MetaInjection
With this approach, we have to look up the Grails services and override the invokeMethod() for the Service classes. Here before invoking the original method, we will modify the default flush mode for the current session. We will also add logging code so we can log when we enter and exit the method.
We will put our code in grails-app/conf/BootStrap.groovy
of our Grails application. After the above mentioned changes, here it is how the Bootstrap.groovy code will look like –
import com.ttnd.demo.Group import com.ttnd.demo.GroupService import org.hibernate.FlushMode class BootStrap { def grailsApplication GroupService groupService def init = { servletContext -> setupServiceInterceptor() Group group = new Group(name: "Admin", assignedRepId: "USR-00-12", activationDate: new Date()) groupService.persistGroup(group) } def destroy = { } private void setupServiceInterceptor() { grailsApplication.serviceClasses.each { serviceClass -> serviceClass.metaClass.invokeMethod = { name, args -> delegate.log.info "Invoking $name in ${delegate.class.name}" def metaMethod = delegate.metaClass.getMetaMethod(name, args) try { FlushMode flushMode = grailsApplication.mainContext.sessionFactory.currentSession.flushMode delegate.log.debug("Inside setupServiceInterceptor: Flush mode for current Session: ${flushMode}") grailsApplication.mainContext.sessionFactory.currentSession.setFlushMode(FlushMode.COMMIT) def result = metaMethod.invoke(delegate, args) delegate.log.info "Execution completed for method $name with result [$result]" return result } catch (Exception ex) { delegate.log.error "Exception occurred during execution of $name: ${ex.message}" throw ex } } } } }
When we run the application, it will print the following logs:
2015-10-30 17:39:14,482 [localhost-startStop-1] INFO demo.GroupService - Invoking persistGroup in com.ttnd.demo.GroupService$$EnhancerBySpringCGLIB$$d26dc9bf 2015-10-30 17:39:14,511 [localhost-startStop-1] DEBUG demo.GroupService - Inside setupServiceInterceptor: Flush mode for current Session: AUTO 2015-10-30 17:39:14,593 [localhost-startStop-1] DEBUG demo.GroupService - Inside GroupService.persistGroup(): Flush mode for current Session: COMMIT 2015-10-30 17:39:14,714 [localhost-startStop-1] INFO demo.GroupService - Execution completed for method persistGroup with result [Group{ Admin(id: 1) }]
2. Second Solution – Using Spring AOP
Instead of using MetaInjection we can use Spring AOP Interceptors. Don’t worry if you are not familiar with Spring AOP. For this basic example, you are not required to have an expertise in Spring AOP.
Key definitions of some AOP concepts that we are going to use here:
- Aspect: a modularization of a concern that cuts across multiple classes. Transaction management is a good example of a crosscutting concern in J2EE applications. In Spring AOP, aspects are implemented using regular classes (the schema-based approach) or regular classes annotated with the @Aspect annotation (the @AspectJ style).
- Join point: a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.
- Advice: action taken by an aspect at a particular join point. Different types of advice include “around,” “before” and “after” advice.
- Pointcut: a predicate that matches join points. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut (for example, the execution of a method with a certain name). The concept of join points as matched by pointcut expressions is central to AOP, and Spring uses the AspectJ pointcut expression language by default.
- Around advice: Advice that surrounds a join point such as a method invocation. This is the most powerful kind of advice. Around advice can perform custom behavior before and after the method invocation. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.
Create an interceptor class under src/groovy
named ServiceInterceptor.groovy. Annotate it using @Aspect annotation. Define its bean under grails-app/conf/spring/resources.groovy
.
Now after creating an AOP bean, we need to add a PointCut method. This method will identify which Service Method calls to intercept. Create a method with name executeMethods. Annotate this method with @Pointcut
annotation and define the pointcut expression here. Leave the definition body blank.
Now we need to declare an advice. Advice is associated with a pointcut expression, and runs before, after, or around method executions matched by the pointcut. Here we will use the around advice. Create another method with name interceptJobExecuteMethod and annotate it with @Around(“executeMethods()”) annotation. In @Around
annotation, we are providing the pointcut method name.
package com.ttnd.demo import groovy.util.logging.Log4j import org.aspectj.lang.ProceedingJoinPoint import org.aspectj.lang.annotation.Around import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Pointcut import org.hibernate.FlushMode @Log4j @Aspect class ServiceInterceptor { def grailsApplication /** * expression to identify which method calls to intercept */ @Pointcut("execution(public * com.ttnd.demo.GroupService.persistGroup(..))") public void executeMethods() {} @Around("executeMethods()") def interceptJobExecuteMethod(ProceedingJoinPoint joinPoint) { FlushMode flushMode = grailsApplication.mainContext.sessionFactory.currentSession.flushMode log.debug("Inside ServiceInterceptor: Flush mode for current Session: ${flushMode}") //Modify defaul flush mode for current session grailsApplication.mainContext.sessionFactory.currentSession.setFlushMode(FlushMode.COMMIT) //Proceed with method execution joinPoint.proceed() } }
grails-app/conf/spring/resources.groovy
beans = { serviceInterceptor(com.ttnd.demo.ServiceInterceptor){ grailsApplication = ref("grailsApplication") } }
When we run the application, it will print the following logs:
2015-10-30 18:07:49,814 [localhost-startStop-1] DEBUG demo.ServiceInterceptor - Inside ServiceInterceptor: Flush mode for current Session: AUTO 2015-10-30 18:07:49,947 [localhost-startStop-1] DEBUG demo.GroupService - Inside GroupService.persistGroup(): Flush mode for current Session: COMMIT
Please share your thoughts in the comment section. Will be happy to hear from you. Thanks.
the first solution doesn’t work when a service method iherited from super class using super.doMethod(), this occur a stackoverflow exception
do you have any idea how to resolve that ?
Yes