Skip to content

keithrz/grails-scoped-proxy

 
 

Repository files navigation

Scoped Proxy Plugin

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.

Example

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.

Installation

grails install-plugin scoped-proxy

Logging

All logging occurs under the grails.plugin.scopedproxy namespace.

Transactions

Transactional services are fully supported. That is, proxies of transactional scoped services share the same transactional semantics as usual.

Testing

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.

Hot Reloading

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 ClassCastExceptions where a new proxy based on the new class encounters an old bean based on the old class.

Supporting Reloading With Custom Scopes

If you are using a custom scope in your application, you may need to do some extra work to support reloading.

Session Based Scopes

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.

Non Session Based Scopes

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.

Proxying Custom Beans

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.

Proxying Custom Artefacts

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.

Known Issues

  • 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.

About

Makes using proxies for scoped Grails service easy and painless.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Groovy 100.0%