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:
[code lang=”groovy”]
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) }"
}
}
[/code]
Service Class:
[code lang=”groovy”]
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
}
}
[/code]
Bootstrap some data:
[code lang=”groovy”]
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 = {
}
}
[/code]
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 –
[code lang=”groovy”]
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
}
}
}
}
}
[/code]
When we run the application, it will print the following logs:
[code lang=”groovy”]
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) }]
[/code]
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.
[code lang=”groovy”]
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()
}
}
[/code]
grails-app/conf/spring/resources.groovy
[code lang=”groovy”]
beans = {
serviceInterceptor(com.ttnd.demo.ServiceInterceptor){
grailsApplication = ref("grailsApplication")
}
}
[/code]
When we run the application, it will print the following logs:
[code lang=”groovy”]
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
[/code]
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