Grails services are Spring managed singleton beans by default. Singleton is one of the five different scopes (singleton, prototype, request, session, and globalSession) that Spring framework offers for managed beans. Prior to Spring 2.5, there were only 2 standard scopes: singleton and prototype. Spring 2.5 added 3 additional scopes: request, session and globalSession for use in web-based applications. Grails adds two more scopes to the mix: flow and flash.
Sometimes, you might run into a situation that none of these scopes meet your requirements. For instance, when you have a multi-tenant or multi-client application, what you may need is a client scope, a separate bean instance for each client. Recently, I ran into this situation. Addressing an existing performance issue drove me into this situation, making an use case for a custom scope.
We have a Grails 2.5.4 multi-client application with clientId included in URL mappings for end-points like: /clients/$clientId/resource. There has been a performance issue with one of the end-points backed by a service. The service has a heavy-weight method which builds and caches client data once for each client with an expiration time set to expire cache few hours once built. It takes few minutes to build this data due to it's nature and some data rule complexities. Once built and cached, it rebuilds data from the cache really fast. One of the performance improvements identified upfront was to limit concurrent calls to that method. Obviously, there is no point in allowing concurrent builds for the same client. The solution to put in place was to allow one-and-only-one concurrent call for any given client, but allow concurrent calls for different clients one per each client. Making the method synchronized is an easy way to limit concurrent calls, but it only solves half of the problem. As service is a singleton bean and synchronized method uses this as the object lock. With that, concurrent calls get executed serially, across all clients. But, we need to allow concurrent executions for different clients, but not for the same client. It is still possible to achieve this with synchronized method, but only when there is one service instance per client. This opened the need for a custom scope. Since Spring 2.0, the concept of scoping beans is made extensible and is the way to add custom scope.
Spring maintains a cache of all scoped beans in it's container. Every scope except prototype has it's own in-memory cache of bean instances which is initialized & populated when the application gets started and maintained by spring container. All spring managed beans either get instantiated or get proxies created during the application startup. Obviously, prototype scoped beans don't need any kind of cache as they get created and injected into all beans that are auto-wired with Singleton beans. A Grails service being singleton bean is also stateless and hence it's methods can be executed by multiple concurrent threads.
Spring has a good documentation of all these details and the API is well-documented as well. For creating a custom scope, all we need to do is: 1) Create a custom Scope object by implementing org.springframework.beans.factory.config.Scope interface and 2) Register custom scope with Spring container. 3) Scope required beans at this custom scope. Sounds simple, in a Grails application it should even be simpler. Lets get through step-by-step implementation details of adding a new custom scope: Client scope to a Grails 2 application.
Environment: Grails 2.5.4, Spring 4.1.8, Java 8 on MacOS High Sierra 10.13.5
src/groovy/com/giri/grails/scope/ClientScope.groovy
Sometimes, you might run into a situation that none of these scopes meet your requirements. For instance, when you have a multi-tenant or multi-client application, what you may need is a client scope, a separate bean instance for each client. Recently, I ran into this situation. Addressing an existing performance issue drove me into this situation, making an use case for a custom scope.
We have a Grails 2.5.4 multi-client application with clientId included in URL mappings for end-points like: /clients/$clientId/resource. There has been a performance issue with one of the end-points backed by a service. The service has a heavy-weight method which builds and caches client data once for each client with an expiration time set to expire cache few hours once built. It takes few minutes to build this data due to it's nature and some data rule complexities. Once built and cached, it rebuilds data from the cache really fast. One of the performance improvements identified upfront was to limit concurrent calls to that method. Obviously, there is no point in allowing concurrent builds for the same client. The solution to put in place was to allow one-and-only-one concurrent call for any given client, but allow concurrent calls for different clients one per each client. Making the method synchronized is an easy way to limit concurrent calls, but it only solves half of the problem. As service is a singleton bean and synchronized method uses this as the object lock. With that, concurrent calls get executed serially, across all clients. But, we need to allow concurrent executions for different clients, but not for the same client. It is still possible to achieve this with synchronized method, but only when there is one service instance per client. This opened the need for a custom scope. Since Spring 2.0, the concept of scoping beans is made extensible and is the way to add custom scope.
Spring maintains a cache of all scoped beans in it's container. Every scope except prototype has it's own in-memory cache of bean instances which is initialized & populated when the application gets started and maintained by spring container. All spring managed beans either get instantiated or get proxies created during the application startup. Obviously, prototype scoped beans don't need any kind of cache as they get created and injected into all beans that are auto-wired with Singleton beans. A Grails service being singleton bean is also stateless and hence it's methods can be executed by multiple concurrent threads.
Spring has a good documentation of all these details and the API is well-documented as well. For creating a custom scope, all we need to do is: 1) Create a custom Scope object by implementing org.springframework.beans.factory.config.Scope interface and 2) Register custom scope with Spring container. 3) Scope required beans at this custom scope. Sounds simple, in a Grails application it should even be simpler. Lets get through step-by-step implementation details of adding a new custom scope: Client scope to a Grails 2 application.
Environment: Grails 2.5.4, Spring 4.1.8, Java 8 on MacOS High Sierra 10.13.5
Step-1 Implement custom Scope
This is straight forward. Just implement the interface and provide implementation for all essential methods. Remember, the implementation should also maintain it's own cache for this custom scoped beans. Also, make sure that any needed scoped context (in this case, it is clientId) is available and accessible in this implementation when a reference to the bean scoped at this custom scope is needed.src/groovy/com/giri/grails/scope/ClientScope.groovy
package com.giri.grails.scope
import grails.plugin.springsecurity.SpringSecurityService
import grails.util.Holders
import groovy.util.logging.Log4j
import org.springframework.beans.factory.ObjectFactory
import org.springframework.beans.factory.config.Scope
/**
* Custom scope bean for client-scoped services registered in
resources.groovy
.
*
* All services(Spring beans) that need client-scope should define static property of scope like:
* static scope = ClientScope.SCOPE_NAME
*
* @see resources.groovy
*
* @author gpottepalem
* Created on July 15, 2018
*/
@Log4j
class ClientScope implements Scope {
static final String SCOPE_NAME = 'clientScope'
/**
* Client scoped bean store.
* A synchronized multi-thread-safe map of various beans scoped with {@link ClientScope#SCOPE_NAME}
* Spring framework depends on this store for maintaining beans defined with this custom client-scope.
* e.g. Two clients with one common service and two different services
* [ client-1 : [
* 'ClientService' : clientServiceObjectRef,
* 'OtherService' : otherServiceObjectRef
* ],
* client-2 : [
* 'ClientService' : clientServiceObjectRef,
* 'SomeOtherService' : someOtherServiceObjectRef
* ]
* ]
*/
private Map<Integer, Map<String, Object<?> clientScopedBeansMap = [:].asSynchronized()
/**
* Helper method, returns client-scoped beans for a given client.
* @param clientId the client id
* @return A map of client-scoped beans for the given client.
*/
private Map<String Object> getClientScopedBeans(Integer clientId) {
if(!clientScopedBeansMap[clientId]) {
clientScopedBeansMap[clientId] = [:].asSynchronized()
log.debug "No client scoped bean found for client:$clientId, just created new map"
}
return clientScopedBeansMap[clientId]
}
/**
* Helper method, returns clientId taking it from authenticated user.
* @return clientId of the current user logged in
*/
private Integer getClientId() {
(Holders.grailsApplication.mainContext.getBean('springSecurityService') as SpringSecurityService).authentication?.clientId
}
@Override
Object get(String name, ObjectFactory<?> objectFactory) {
synchronized (this) {
Integer clientId = getClientId()
Map<String Object> clientScopedBeans = getClientScopedBeans(clientId)
if (!clientScopedBeans[name]) {
clientScopedBeans[name] = objectFactory.object
log.debug "Added new instance: ${clientScopedBeans[name]} for bean: $name for client:$clientId to the bean store"
}
return clientScopedBeans[name]
}
}
@Override
Object remove(String name) {
Map<String Object> scopedBeanMap = getClientScopedBeans(getClientId())
return scopedBeanMap.remove(name)
}
@Override
void registerDestructionCallback(String s, Runnable runnable) {
// nothing to register
}
@Override
Object resolveContextualObject(String s) {
return null
}
@Override
String getConversationId() {
return SCOPE_NAME
}
}
Step-2 Register custom scope
Register custom scope in:grails-app/conf/spring/resources.groovy
import com.giri.grails.scope.ClientScope
import org.springframework.beans.factory.config.CustomScopeConfigurer
beans = {
...
// Custom scope: per-client
clientScope(ClientScope)
// register all custom scopes
customScopeConfigurer(CustomScopeConfigurer) {
scopes = [(ClientScope.SCOPE_NAME) : ref('clientScope')].asImmutable()
}
...
}
Step-3 Scope a client-specific service with custom scope
Say, we have a service ClientService that we need to scope at clientScope. Just define a static scope property set with this custom scope like:grails-app/services/com/giri/ClientService.groovy
package com.giri
import com.giri.grails.scope.ClientScope
class ClientService {
static scope = ClientScope.SCOPE_NAME
def clientDataBuilderService //DI
//delegates to clientDataBuilderService
synchronized Map buildClientData(String clientId) {
clientDataBuilderService.buildData(clientId)
}
...
}
Step-4 Custom-scoped service - Dependency Injection
Grails supports Spring Dependency Injection by convention. A property name that matches the class name of a Spring managed bean gets injected automatically. Unlike Spring applications, you don't need @Autowired annotation on a property or a setter method. But for custom scoped beans, the actual custom scoped bean might get instantiated lazily when the client context (in this case, clientId) is available in the application (either taken from the request, session, security authentication etc.). This context is not known at the start of the application. So, without creating proxies, dependency injection may not be possible. This requires the scoped bean to be programmatically resolved, unlike auto-wired by Grails convention. The following is a way to get a handle to the scoped bean instance. The assumption here is, that the context needed for creating a scoped bean for specific client (clientId) is available in the security context and all end-points are secured.grails-app/controllers/com/giri/ClientController.groovy
package com.giri
import grails.converters.JSON
import grails.util.Holders
class ClientController {
...
def index(String clientId) {
ClientService clientService = Holders.grailsApplication.mainContext.getBean('clientService', ClientService.class)
clientService.buildClientData(clientId) as JSON
}
...
}
TIP: Unit Testing
Without scoped bean dependency injected by following Grails naming convention for DI and by referring it using Grails ApplicationContext puts us into a limitation in unit tests. The actual scoped bean instance is needed which can otherwise be mocked if it was injected. Typically, Grails doesn't load bean definitions as the complete ApplicationContext is not needed in unit-tests. The following are two options that Grails offers to get away with this and have bean definitions loaded and ApplicationContext available for unit-tests:Option-1
test/unit/com/giri/ClientControllerSpec.groovy
package com.giri
import grails.test.mixin.TestFor
import spock.lang.Specification
@TestFor(ClientController)
class ClientControllerSpec extends Specification {
static loadExternalBeans = true //loads beans defined in resources.groovy and beans are available in applicationConext
ClientService clientService
def setup() {
//controller.clientService = Mock(ClientService) //doesn't work
clientService = applicationContext.getBean('clientService')
clientService.clientDataBuilderService = Mock(ClientDataBuilderService)
}
void "test index"() {
when:
controller.index('client-1')
then:
(1.._) * clientService.clientDataBuilderService.buildData('client-1') >> ['abcd' : 1234]
and:
response.json == [
"abcd": 1234
]
}
}
Option-2
package com.giri
import grails.test.mixin.TestFor
import spock.lang.Specification
@TestFor(ClientController)
class ClientControllerSpec extends Specification {
ClientService clientService
def setup() {
//controller.clientService = Mock(ClientService) //doesn't work
//define bean to get into applicationContext as it is not injected to mock it out
defineBeans {
clientService(ClientService)
}
clientService = applicationContext.getBean('clientService')
clientService.clientDataBuilderService = Mock(ClientDataBuilderService)
}
void "test index"() {
when:
controller.index('client-1')
then:
(1.._) * clientService.clientDataBuilderService.buildData('client-1') >> ['abcd' : 1234]
and:
response.json == [
"abcd": 1234
]
}
}