| title: Grails® 3 Interceptors | |
| date: March 1, 2015 | |
| description: Grails 3 interceptors are a great way to insert logic into the request handling process. | |
| author: Jeff Scott Brown | |
| image: 2015-03-01.jpg | |
| CSS: [%url]/stylesheets/prism.css | |
| JAVASCRIPT: [%url]/javascripts/prism.js | |
| --- | |
| # [%title] | |
| [%author] | |
| [%date] | |
| ## Introduction | |
| Grails<sup>®</sup> 3 is a major step forward in the evolution of the framework and re-evaluates many aspects of the framework that have evolved over the years. One area of the framework that was re-evaluated is that related to Grails filters. | |
| Grails filters are a lot like servlet filters but are more simple and are better integrated into the Grails runtime and its conventions. Grails filters were a way to implement logic that might relate to any number of controllers and were a powerful and flexible way to address many of those concerns. Grails 3 introduces the notion of interceptors as a better way to address many of the types of concerns that previously would have been addressed with filters. | |
| Like filters, interceptors contain code which may be applied to requests before and/or after invoking controller actions without having to embed that logic into all of the controllers that the logic may relate to. Interceptors offer a number of benefits over filters including support for static compilation and more flexible configurability. | |
| ## Defining An Interceptor | |
| Like most Grails artifacts, interceptors are normal Groovy classes that follow certain conventions which allow the framework to identify them and augment them with a rich set of behavior that is relevant to tasks that interceptors frequently address. | |
| 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`. | |
| ```groovy | |
| // grails-app/controllers/demo/FirstInterceptor.groovy | |
| package demo | |
| class FirstInterceptor { | |
| // ... | |
| } | |
| ``` | |
| The interceptor class does noes not need to extend any special base class and does not 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 | |
| ``` | |
| ```groovy | |
| // grails-app/controllers/demo/FirstInterceptor.groovy | |
| package demo | |
| class FirstInterceptor { | |
| boolean before() { true } | |
| boolean after() { true } | |
| void afterView() { | |
| // no-op | |
| } | |
| } | |
| ``` | |
| ```groovy | |
| // 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 for traits in many places. All interceptors are automatically made to implement the [grails.artefact.Interceptor](https://grails.org/doc/3.0.x/api/grails/artefact/Interceptor.html) trait so all of the methods and properties defined by that trait and all of the methods and properties defined by traits which that trait extends are all available inside of an interceptor. This provides easy access to attributes like `grailsApplication`, `params`, `request`, `session` and others. The trait also provides access to methods like `redirect` and `render`. | |
| ## Matching Requests | |
| A convention is imposed which 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 define which requests they should participate in using the `match`or `matchAll` method. | |
| ```groovy | |
| // 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 except for `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](https://docs.spring.io/spring/docs/4.1.7.RELEASE/javadoc-api/org/springframework/util/AntPathMatcher.html). | |
| An interecptor may match all requests except requests that satisfy some condition. | |
| ```groovy | |
| // 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 intereceptor each return an instance of [grails.interceptors.Matcher](https://grails.github.io/grails-doc/3.0.x/api/grails/interceptors/Matcher.html). 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 that an interceptor is applied to. Keep in mind that whatever logic is contained in an interceptor is being engaged for all of the requests that the interceptor matches. | |
| ## Interceptor Methods | |
| There are 3 separate methods that an interceptor may define 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 does not need to 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 intereceptor and processing should not continue on as per usual. For example, a `before` interceptor may recognize that a request to a controller action is invalid for some reason, issued a `redirect` to handle the situation and then return `false` so that the originally requested action will not be engaged. | |
| ```groovy | |
| 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 it 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. Intereceptors are sorted based on their order from lowest to highest value and executed in that order 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 kind of like a stack and are popped off of the stack while processing the `after` and `afterView` methods so those are executed in the opposite order that the `before` methods are executed. | |
| For convenience there are properties named `HIGHEST_PRECEDENCE` and `LOWEST_PRECEDENCE` which may be referenced when initializing the `order` property. | |
| ```groovy | |
| package demo | |
| class FirstInterceptor { | |
| int order = HIGHEST_PRECEDENCE + 100 | |
| // ... | |
| } | |
| ``` | |
| ```groovy | |
| package demo | |
| class SecondInterceptor { | |
| int order = HIGHEST_PRECEDENCE + 200 | |
| // ... | |
| } | |
| ``` | |
| ## Static Compilation | |
| Unlike Grails 2 filters, Grails 3 intercetors are compatible with Groovy's static compilation and as such may be marked with the `@CompileStatic` annotation. Static compilation may be of particular importance in an interceptor because code in an interceptor potentially could be executed as part of handling every request into the application so performance may be particularly important there. Referencing methods introduced by the `Interceptor` trait like `redirect` and `render` and accessing propeties like `grailsApplication` are all compatible with `@CompileStatic`. | |
| ## Logging | |
| Like most Grails artifacts interceptors have a `log` property. | |
| ```groovy | |
| 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`. For the interceptor shown above that would be `grails.app.controllers.demo.SimpleAuthInterceptor` so the logging could be configured in `grails-app/conf/logback.groovy` as shown below. | |
| ```groovy | |
| // ... | |
| logger 'grails.app.controllers.demo.SimpleAuthInterceptor', | |
| DEBUG, ['STDOUT'], false | |
| ``` | |
| Note that the logger name prefix is `grails.app.controllers`, not `grails.app.interceptors`. This is because by default interceptors are defined under the `grails-app/controllers/` directory. | |
| The framework offers flexibility around this such that if you would like to separate your interceptors from your controllers you can do that 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 autowired by name. This means that just like other Grails artifacts, an interceptor may define a property with a name that matches a bean name and that property will be initialized using standard Spring dependency injection. | |
| In addition to participating in dependency injection, because interceptors are Spring beans they participate in all of the standard Spring bean management handling. For example, if an interceptor wanted access to the application's configuration, the intereceptor class could implement the [grails.core.support.GrailsConfigurationAware](https://grails.github.io/grails-doc/3.0.x/api/grails/core/support/GrailsConfigurationAware.html) interface. | |
| ```groovy | |
| 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`. | |
| ```yml | |
| --- | |
| demo: | |
| interceptor: | |
| first: | |
| action: save | |
| controller: person | |
| ``` | |
| ## Testing Interceptors | |
| Interceptors may be tested on their own as first class artifacts. | |
| ```groovy | |
| // grails-app/controllers/demo/FirstInterceptor.groovy | |
| package demo | |
| class FirstInterceptor { | |
| public FirstInterceptor() { | |
| match controller: 'demo', action: 'index' | |
| } | |
| boolean before() { | |
| params.firstInterceptorRan = 'yes' | |
| true | |
| } | |
| } | |
| ``` | |
| ```groovy | |
| // grails-app/controllers/demo/DemoController.groovy | |
| package demo | |
| class DemoController { | |
| def index() { | |
| render "firstInterceptorRan is ${params.firstInterceptorRan}" | |
| } | |
| def create() { | |
| render "firstInterceptorRan is ${params.firstInterceptorRan}" | |
| } | |
| } | |
| ``` | |
| ```groovy | |
| // 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. | |
| ```groovy// 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 configuration burden required to take advantage of really powerful functionality provided by the framework. Support for marking interceptors with `@CompileStatic` means that the performance cost associated with interceptor logic can be minimized. The fact that interceptors are Spring beans provides a lot of flexibility for keeping interceptors simple yet powerful. |