Custom Spring scope done right

You can define your own Spring Scope. One thing to consider is that the beans that are defined in a scope must somehow be backed by a store. For the singlton scope this is the application context itself, for a session scope it is the session, for a request scope it is the request. With this setup the beans lifecycle is bound to the lifecycle of the backing store, e.g. when a session terminates it is deleted and with it all the corresponding beans.

There are other examples out there that implement a custom scope by implementing the store in the scope. This then requires that the lifecycle has to be managed from the outside. So let’s do this the right way:

  • We want a scope that matches the lifecycle of a dialog. The dialog is opened some content is displayed, some work is done and finally the dialog is closed.
  • The application code should not be polluted with logic to manage the bean life cycle of the scope.

To simplify the example let’s assume that there can only be one dialog at a time. The Scope implementation follows this example closely. The main difference is that there are two methods which define the start of the scope boundary and the end. A bean of in this scope may only be created after the start and before the end. This is necessary as the lifecycle of the Scope bean is that of a singleton and therefore the store backing the scope would have the same lifecycle. With designating the start and end the access control to the store can be limited.

So here is the Scope implementation:

public class DialogScope implements Scope {
    private Map<String, Object> objectMap = Collections.synchronizedMap(new HashMap<String, Object>());
    private volatile boolean scopeOpen = false;

    public DialogScope() {
        System.out.println(getClass().getName()+": Creating scope");
    }

    /**
     * Mark the begining of the scope, before the scope is opened no beans are registered.
     */
    public void openScope() {
        scopeOpen = true;
        System.out.println(getClass().getName()+": Opened the scope");
    }

    /**
     * Mark the end of the scope, after the scope is closed, no beans are registered.
     */
    public void closeScope() {
        scopeOpen = false;
        clear();
        System.out.println(getClass().getName()+": Closed the scope");
    }

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        if (scopeOpen && !objectMap.containsKey(name)) {
            // Only add the bean if the scope is open and the bean is not yet already registered
            objectMap.put(name, objectFactory.getObject());
        }
        return objectMap.get(name);
    }

    @Override
    public Object remove(String name) {
        return objectMap.remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
       // do nothing
    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    /**
     * Clear the cached beans in the scopes backing store
      */
    private void clear() {
        objectMap.clear();
        System.out.println(getClass().getName()+": Clear scope");
    }

    @Override
    public String getConversationId() {
        return "Dialog";
    }
}

 

And the configuration in the spring context.

 

    <bean id="dialogScope" class="ch.sahits.spring.DialogScope" />

    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
        <property name="scopes">
            <map>
                <entry key="dialog">
                    <ref bean="dialogScope" />
                </entry>
            </map>
        </property>
    </bean>

Up until now this is nothing really novel. The clever bit comes now. It’s not terribly complicated but I have not seen any example that combine these two techniques to that effect: We are using Aspect Oriented Programming to control the scope. Somewhere in the application code there will be a method opening a dialog and a method closing the dialog. These are the two points we want to connect to the start and end of the scope. I have chosen to mark these methods with custom annotations. This is not really necessary as one can also use the direct method name, however there are two potential pitfalls:

  • When refactoring the method names or the defining class this is not reflected in the aspect
  • What if there is not a single method that opens or closes the dialog?

So here are the two annotations:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DialogOpening {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DialogCloseing {
}

Now the most complex part is the definition of the aspect:

@Aspect
public class DialogAspect {
    @Autowired
    private DialogScope scope;

    @Pointcut(value="execution(public * *(..))*")
    public void anyPublicMethod() {
    }
    @Before("anyPublicMethod() && @annotation(dialogOpening)")
    public void openDialog(JoinPoint pjp, DialogOpening dialogOpening) throws Throwable {
        System.out.println(getClass().getName()+": In opening aspect:");
        for (java.lang.Object o : pjp.getArgs()) {
            System.out.println(o);
        }
        scope.openScope();
    }
    @After("anyPublicMethod() && @annotation(dialogCloseing)")
    public void openDialog(JoinPoint pjp, DialogCloseing dialogCloseing) throws Throwable {
        System.out.println(getClass().getName()+": In closing aspect:");
        for (java.lang.Object o : pjp.getArgs()) {
            System.out.println(o);
        }
        scope.closeScope();
    }
}

The aspect takes nicely care of starting the scope before the dialog is opened and ending it again on closing.

And finally there is some configuration needed in the spring context.

<aop:aspectj-autoproxy />

<bean id="DialogScopeAspect" class="ch.sahits.spring.aop.DialogAspect" />

There are some dependencies that are required here defined in the pom.xml:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.6.11</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.6.11</version>
</dependency>        <dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.1</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.1.13</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-oxm</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
</dependency>

Finally we want to see if that all worked out. We can do this with this nice little Test.

@Test
public void testScope() {
	ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml");
	WindowOpening instance = context.getBean(WindowOpening.class);

	assertNull("Dialog scoped bean Location may not be available before scope is started", context.getBean(Location.class));
	instance.openDialog("GenericDialog");
	assertNotNull("Dialog scoped bean Location must be available after scope is started", context.getBean(Location.class));
	Thread.currentThread();
	try {
	    Thread.sleep(5000);
	} catch (InterruptedException e) {
	    e.printStackTrace();
	}
	instance.closeDialog("GenericDialog");
	assertNull("Dialog scoped bean Location may not be available after scope is closed", context.getBean(Location.class));
}

As the asserts show the Location bean which is defined with the dialog scope is only present after the dialog is opened before it is closed again.

The output of the various sysouts gives us this:

ch.sahits.spring.DialogScope: Creating scope
ch.sahits.spring.aop.DialogAspect: In opening aspect:
GenericDialog
ch.sahits.spring.DialogScope: Opened the scope
ch.sahits.spring.WindowOpening: Opened a dialog: GenericDialog
Creating the location
ch.sahits.spring.WindowOpening: Close a dialog: GenericDialog
ch.sahits.spring.aop.DialogAspect: In closing aspect:
GenericDialog
ch.sahits.spring.DialogScope: Clear scope
ch.sahits.spring.DialogScope: Closed the scope

The annotation approach is largely inspired by this article.

Schreibe einen Kommentar