Grails 3 Interceptors

Grails 3 Interceptors

By Jeff Scott Brown, OCI Principal Software Engineer

September 2015

Introduction

Grails 3 is a major step forward in the evolution of the framework. One noteable feature of Grails 3 is Grails interceptors (as a replacement to filters).  In previous versions, Grails uses filters much like servlet filters (though better integrated into the Grails runtime and its conventions).  Filters are simply a way to implement logic, which might relate to any number of controllers, and a powerful and flexible tool to address many such concerns.

Grails 3 replaces filters with interceptors which offer a number of benefits over filters, including support for static compilation, and more flexible configurability.

Check out Jeff Brown's Grails Interceptors Quickcast!

Defining an Interceptor

Like most Grails artifacts, interceptors are simple Groovy classes. They follow certain conventions that allow the framework to first identify and then augment them with a rich set of behavior relevant to frequently addressed tasks.

Interceptors contain code that may be applied to requests before and/or after invoking controller actions, without having to embed that logic into all of the controllers to which the logic may relate. Because interceptors are often associated with controllers, the convention is to define them under the grails-app/controllers/ directory. Interceptor class names should end with the word Interceptor.

// grails-app/controllers/demo/FirstInterceptor.groovy
package demo
 
class FirstInterceptor {
    // ...
}

The interceptor class does not need to extend any special base class, nor does it need to be marked with any special annotations in order for the framework to recognize the class as an interceptor. The location of the source file and the name of the class are sufficient to register the class as an interceptor.

The create-interceptor command may be used to define an interceptor.

 $ grails create-interceptor demo.First
| Created grails-app/controllers/demo/FirstInterceptor.groovy
| Created src/test/groovy/demo/FirstInterceptorSpec.groovy
// grails-app/controllers/demo/FirstInterceptor.groovy
package demo
 
class FirstInterceptor {
 
    boolean before() { true }
 
    boolean after() { true }
 
    void afterView() {
        // no-op
    }
 
}
// src/test/groovy/demo/FirstInterceptorSpec.groovy
package demo
 
 
import grails.test.mixin.TestFor
import spock.lang.Specification
 
@TestFor(FirstInterceptor)
class FirstInterceptorSpec extends Specification {
 
    void "Test first interceptor matching"() {
        when:"A request matches the interceptor"
            withRequest(controller:"first")
 
        then:"The interceptor does match"
            interceptor.doesMatch()
    }
}

The Interceptor Trait

Grails 3 takes advantage of Groovy's powerful and flexible support by automatically making all interceptors implement the grails.artefact.Interceptor trait. This means the methods and properties defined by the given trait - and all of the methods and properties defined by traits, which the given trait extends - are available inside of an interceptor, providing easy access to attributes like grailsApplication, params, request, session, etc. The trait also provides access to methods like redirect and render.

Matching Requests

A convention is imposed that will automatically configure interceptors to match all requests to their corresponding controller. The implicit mapping from an interceptor to a controller is based on the artifact names. For example, by default, the PersonInterceptor will match all requests to the PersonController.

Interceptors often need to deviate from the convention and to define the requests in which they participate using the match or matchAll method.

// grails-app/controllers/demo/FirstInterceptor.groovy
package demo
 
class FirstInterceptor {
 
    public FirstInterceptor() {
        // match all requests to the
        // reporting controller...
        match controller: 'reporting'
 
        // match a request to the create action
        // in the person controller
        match controller: 'person', action: 'create'
 
        // match all requests to the accounting
        // or payroll controller
        match controller: ~/(accounting|payroll)/
    }
 
    // ...
}

The named arguments supported are namespace, controller, action, method and uri. All of them (with the exception of uri) accept either a String or a regex expression. (The uri argument supports a String path that is compatible with Spring's org.springframework.util.AntPathMatcher.)

An interceptor may match all requests, except requests that satisfy some condition.

// grails-app/controllers/demo/FirstInterceptor.groovy
package demo
 
class FirstInterceptor {
 
    public FirstInterceptor() {
        // match all requests except requests
        // to the auth controller
        matchAll().excludes(controller: 'auth')
    }
 
    // ...
}

The match and matchAll methods in an interceptor both return an instance of grails.interceptors.Matcher. Most of the methods in Matcher also return the Matcher, which allows for method chaining as shown above with matchAll().excludes(controller: 'auth').

Care should be taken to narrow the group of requests to which an interceptor is applied, as whatever logic is contained in an interceptor will be applied to all of the requests that the interceptor matches.

Interceptor Methods

There are 3 separate methods that an interceptor may define in order to participate in different parts of the request processing lifecycle.

  • The before method is executed before the controller action is invoked.
  • The after method is invoked after the controller action returns, and before the view is rendered.
  • The afterView method is invoked after the view is rendered.

An interceptor may provide any combination of these 3 call back methods and/or may not provide all 3.

The before and after methods have a boolean return type. The methods should return true to indicate that control should continue as usual without interruption. The methods should return false to indicate that the interceptor has decided that the request has been handled by the interceptor, and processing should not continue as per usual. For example, a before interceptor may recognize that a request to a controller action is invalid for some reason, issue a redirect to handle the situation, and then return false so that the originally requested action will not be engaged.

package demo
 
class SimpleAuthInterceptor {
 
    public SimpleAuthInterceptor() {
        match controller: 'person'
    }
 
    boolean before() {
        // if the user has not been authenticated,
        // redirect to authenticate the user...
        if(!session.userHasBeenAuthenticated) {
            redirect controller: 'auth', action: 'login'
            return false
        }
        true
    }
}

Interceptor Ordering

Any number of interceptors may match a request and, in some circumstances, may be useful to affect the order in which the interceptors are invoked. To support this, the framework will recognize an int property named order defined in any interceptor.

The value of the order property may be any number. Interceptors are sorted and executed based on order, from lowest to highest value, so an interceptor with an order of 100 will execute before an interceptor with an order of 200. This determines the order in which the before interceptor methods are invoked. The interceptors are configured like a stack; they are "popped off the stack" while processing the after and afterView methods causing them to be executed in an order opposite that of the before methods.

Below is a reference that may be used in initializing the order property. Notice the properties are listed from HIGHEST_PRECEDENCE and LOWEST_PRECEDENCE.

package demo
 
class FirstInterceptor {
 
    int order = HIGHEST_PRECEDENCE + 100
 
    // ...
 
}
package demo
 
class SecondInterceptor {
 
    int order = HIGHEST_PRECEDENCE + 200
 
    // ...
 
}

Static Compilation

Unlike Grails 2 filters, Grails 3 interceptors are compatible with Groovy's static compilation and may, therefore, be marked with the @CompileStatic annotation. Static compilation (and performance) is of particular importance in an interceptor. Because an interceptor's code may be executed as part of handling every request, referencing methods introduced by the Interceptor trait (like redirect and render) and accessing properties (like grailsApplication) are all compatible with @CompileStatic.

Logging

Like most Grails artifacts, interceptors have a log property.

package demo
 
class SimpleAuthInterceptor {
 
    public SimpleAuthInterceptor() {
        match controller: 'person'
    }
 
    boolean before() {
        // if the user has not been authenticated,
        // redirect to authenticate the user...
        if(!session.userHasBeenAuthenticated) {
            // log a message
            log.debug 'Redirecting to login page'
            redirect controller: 'auth', action: 'login'
            return false
        }
        true
    }
}

The default logger name for an interceptor is grails.app.controllers. The logger name for the interceptor shown above would be grails.app.controllers.demo.SimpleAuthInterceptor, so the logging could be configured in grails-app/conf/logback.groovy as shown below.

// ...
logger 'grails.app.controllers.demo.SimpleAuthInterceptor',
       DEBUG, ['STDOUT'], false

Note that the logger name prefix is grails.app.controllers<interceptor class name>, not grails.app.interceptors. This is because, by default, interceptors are defined under the grails-app/controllers/ directory. The framework does, however, offer flexibility around this. If you would like to separate your interceptors from your controllers, you can do so by moving the interceptors to a directory like grails-app/interceptors/. If the interceptor defined above were moved, from grails-app/controllers/demo/SimpleAuthInterceptor.groovy to grails-app/interceptors/demo/SimpleAuthInterceptor.groovy, the logger name would be grails.app.interceptors.demo.SimpleAuthInterceptor.

Interceptors are Spring Beans

All interceptors are configured as beans in the Spring application context and are configured to be auto-wired by name. This means that an interceptor may define a property with a name that matches a bean name, allowing that property to be initialized using standard Spring dependency injection.

In addition to participating in dependency injection, interceptors also participate in standard Spring bean management handling. For example, if an interceptor wanted access to the application's configuration, the interceptor class could implement the grails.core.support.GrailsConfigurationAware interface.

package demo
 
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
 
class FirstInterceptor implements GrailsConfigurationAware {
 
    boolean before() {
        // ...
    }
 
    @Override
    void setConfiguration(Config co) {
        // configure the interceptor matching dynamically
        // based on what is in application.yml
        match co.'demo.interceptor.first'
    }
}

With that in place, the request matching could be dynamically configured in grails-app/conf/application.yml.

---
demo:
    interceptor:
        first:
            action: save
            controller: person

Testing Interceptors

Interceptors may be tested on their own as first class artifacts.

// grails-app/controllers/demo/FirstInterceptor.groovy
package demo
 
class FirstInterceptor {
 
    public FirstInterceptor() {
        match controller: 'demo', action: 'index'
    }
 
    boolean before() {
        params.firstInterceptorRan = 'yes'
        true
    }
}
// grails-app/controllers/demo/DemoController.groovy
package demo
 
class DemoController {
 
    def index() {
        render "firstInterceptorRan is ${params.firstInterceptorRan}"
    }
 
    def create() {
        render "firstInterceptorRan is ${params.firstInterceptorRan}"
    }
}
// src/test/groovy/demo/DemoInterceptorSpec.groovy
package demo
 
 
import grails.test.mixin.TestFor
import spock.lang.Specification
 
@TestFor(DemoInterceptor)
class DemoInterceptorSpec extends Specification {
 
    void "Test first interceptor matching"() {
        when:"A request is made to the index action"
        withRequest(controller:"demo", action: 'index')
 
        then:"The interceptor does match"
        interceptor.doesMatch()
    }
 
    void "Test first interceptor not matching"() {
        when:"A request is made to the create action"
        withRequest(controller:"demo", action: 'create')
 
        then:"The interceptor does not match"
        !interceptor.doesMatch()
    }
}

The effects introduced by an interceptor may be tested in a functional test.

// src/integration-test/groovy/demo/DemoControllerFunctionalSpec.groovy
package demo
 
import geb.spock.GebSpec
import grails.test.mixin.integration.Integration
 
@Integration
class DemoControllerFunctionalSpec extends GebSpec {
 
    void "test the index action"() {
        when:
        go '/demo/index'
 
        then:
        $().text() == 'firstInterceptorRan is yes'
    }
 
    void "test the create action"() {
        when:
        go '/demo/create'
 
        then:
        $().text() == 'firstInterceptorRan is null'
    }
}

Conclusion

Grails 3 interceptors are a great way to insert logic into the request handling process. Like all Grails artifacts, interceptors take advantage of convention over configuration and sensible defaults to maximize flexibility and minimize the configuration burden that is required to take advantage of the really powerful functionality provided by the framework. Support for marking interceptors with @CompileStatic means that the performance cost associated with interceptor logic may be minimized. The fact that interceptors are Spring beans provides great flexibility for keeping interceptors simple yet powerful.

Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.


secret