Version: 0.2
The scoped-proxy plugin allows you to easily create proxies for scoped services.
This allows you to use your scoped services from objects in a larger scope.
To create a proxy for a scoped service, simply create a property named proxy
with a value of true
.
class CartService {
static scope = 'session'
static proxy = true
def items = []
def getItemCount() {
items.size()
}
}
There is now a unique instance of CartService
for each session in your application.
Controllers are request scoped in Grails and requests operate inside the session scope, meaning your cartService
can be used without a proxy.
class CartController {
def cartService // unique per session
def addItem = {
cartService.items << new CartItem(product: Product.get(params.id))
}
}
TagLibs on the other hand are of singleton scope and therefore exist outside of session scope. To use your cartService
you need to access it via the proxy.
class CartTagLib {
def cartServiceProxy
def itemCount = {
out << cartServiceProxy.itemCount
}
}
At execution time, calls to cartServiceProxy
are delegated to the actual session bound cartService
instance for the request.
grails install-plugin scoped-proxy
All logging occurs under the grails.plugin.scopedproxy
namespace.
Transactional services are fully supported. That is, proxies of transactional scoped services share the same transactional semantics as usual.
Scoped proxies are only relevant to integration testing.
Currently, integration tests are autowired out of a request context. This means that you must use scoped proxies of scoped services in integration tests if you want them to be autowired. It's also important to realise that each test method runs in a different request (and session) context.
class CartServiceTests extends GroovyTestCase {
def cartServiceProxy
void testAdd1() {
assert cartServiceProxy.itemCount == 0
cartServiceProxy.items << new CartItem(product: Product.get(1))
assert cartServiceProxy.itemCount == 1
}
void testAdd2() {
assert cartServiceProxy.itemCount == 0
cartServiceProxy.items << new CartItem(product: Product.get(1))
assert cartServiceProxy.itemCount == 1
}
}
The above test will pass because the actual underlying cartService
that the cartServiceProxy
delegates to in testAdd1()
and testAdd2()
are different.
This plugin adds explicit support for hot reloading of scoped services during development. This does mean however that when a session scoped bean class is reloaded, all instances of that service class are removed from all active sessions. Depending on your application, the consequences of this will be different.
This is necessary to avoid ClassCastException
s where a new proxy based on the new class encounters an old bean based on the old class.
If you are using a custom scope in your application, you may need to do some extra work to support reloading.
For scopes that are inherently session based (i.e. live inside a session lifecycle), you can (though it may not be best to) plugin into the existing session purging mechanism by registering your scope with the reloadedScopedBeanSessionPurger
bean in the application context.
import org.springframework.web.context.request.SessionScope
import org.springframework.beans.factory.InitializingBean
class CustomSessionBasedScope extends SessionScope, InitializingBean {
static String SCOPE_NAME = 'custom'
def reloadedScopedBeanSessionPurger // autowired
void afterPropertiesSet() {
// reloadedScopedBeanSessionPurger is only present in environments
// that support class reloading, hence the null check.
reloadedScopedBeanSessionPurger?.registerPurgableScope(SCOPE_NAME)
}
}
The above example illustrates how to register a custom scope with the reloadedScopedBeanSessionPurger
bean via the registerPurgableScope(String scopeName)
method. Now, whenever a bean is reloaded of our custom scope it will be removed from the session.
If your custom scope has a completely different storage mechanism, you may need to provide a ScopedBeanReloadListener
implementation that can purge beans based on old classes. It's likely that a convenient implementer of this will be your actual scope implementation (but does not need to be).
import org.springframework.beans.factory.ObjectFactory
import org.springframework.beans.factory.config.Scope
import grails.plugin.scopedproxy.reload.ScopedBeanReloadListener
class CustomScope implements Scope, ScopedBeanReloadListener {
static SCOPE_NAME = "custom"
protected storage = [:].asSynchronized()
// ScopedBeanReloadListener methods
void scopedBeanWillReload(String beanName, String scope, String proxyBeanName) {
if (scope == SCOPE_NAME) {
remove(beanName)
}
}
void scopedBeanWasReloaded(String beanName, String scope, String proxyBeanName) {
// do nothing
}
// Scope Methods
def get(String name, ObjectFactory objectFactory) {
if (!storage.containsKey(name)) {
storage[name] = objectFactory.object
}
storage[name]
}
String getConversationId() {
null
}
void registerDestructionCallback(String name, Runnable callback) {
// not implemented, but should be
}
def remove(String name) {
storage.remove(name)
}
}
All instances of ScopedBeanReloadListener
will be informed whenever any scoped bean has had it's class reloaded.
This plugin can be used to support proxying your own beans. You do this via static
methods on the grails.plugin.scopedproxy.ScopedProxyUtils
class.
// resources.groovy
import grails.plugin.scopedproxy.ScopedProxyUtils as SPU
beans {
myBean(MyBean) {
it.scope = 'session'
}
def beanBuilder = delegate
def classLoader = application.classLoader // Use Grails class loader
def beanName = 'myBean'
def proxyName = SPU.getProxyBeanName(myBean) // returns 'myBeanProxy'
def beanClass = MyBean
SPU.buildProxy(beanBuilder, classLoader, beanName, beanClass, proxyName)
}
Note: while this example shows how to use the buildProxy()
method, it would certainly be much better to use a service in this case so that you get hot reloading.
Plugin developers may wish to provide scoping of their artefacts and supporting proxying in the same manner as services.
For this example, we will use a new artefact type of Thing
which is basically the same as a Grails service.
import grails.plugin.scopedproxy.ScopedProxyUtils as SPU
class ThingGrailsPlugin {
// Usual plugin stuff
…
def artefacts = [ThingArtefactHandler]
def watchedResources = [
"file:./grails-app/things/**/*Thing.groovy",
"file:../../plugins/*/things/**/*Thing.groovy"
]
def doWithSpring = {
for (thingGrailsClass in application.thingClasses) {
def clazz = thingGrailsClass.clazz
def beanName = thingGrailsClass.propertyName
// getScope() looks for a 'scope' property, and returns 'singleton' if none found
def scope = SPU.getScope(clazz)
"$beanName"(clazz) { beanDefinition ->
beanDefinition.scope = scope
// other definition
}
if (SPU.wantsProxy(clazz)) { // class has a 'proxy' property set to true
SPU.buildProxy(delegate, application.classLoader, beanName, clazz, SPU.getProxyBeanName(beanName))
}
}
}
// Reload support
def onChange = {
if (application.isThingClass(event.source)) {
def classLoader = application.classLoader
def newClass = classLoader.loadClass(event.source.name, false) // make sure we get the new class
def grailsClass = application.getThingClass(event.source.name)
def beanName = grailsClass.propertyName
def scope = SPU.getScope(newClass)
def proxyBeanName = SPU.getProxyBeanName(beanName)
SPU.fireWillReloadIfNecessary(application, beanName, proxyBeanName)
def beans = beans {
// Redefine the bean
"$beanName"(newClass) { beanDefinition ->
beanDefinition.scope = scope
// other definition
}
if (SPU.wantsProxy(newClass)) {
// Redefine the proxy
SPU.buildProxy(delegate, classLoader, beanName, newClass, proxyBeanName)
}
}
beanDefinitions.registerBeans(event.ctx)
SPU.fireWasReloadedIfNecessary(application, beanName, proxyBeanName)
}
}
}
Checkout the ScopedProxyUtils
class for more information.
- Request scoped beans inside transactional session scoped beans
There are currently issues with this. After the request scoped bean has been reloaded, accessing it's proxy in a transactional session scoped bean will cause a ClassCastException
. The current solution is to reload the transactional session scoped class.