The new Certification Class of Learn Spring Security is out:

>> CHECK OUT THE COURSE

1. Overview

Out of the box, Spring provides two standard bean scopes (“singleton” and “prototype”) that can be used in any Spring application, plus three additional bean scopes (“request”, “session”, and “globalSession”) for use only in web-aware applications.

The standard bean scopes cannot be overridden, and it’s generally considered a bad practice to override the web-aware scopes. However, you may have an application requiring different or additional capabilities from those found in the provided scopes.

For example, if you are developing a multi-tenant system, you may want to provide a separate instance of a particular bean or set of beans for each tenant. Spring provides a mechanism for creating custom scopes for scenarios such as this.

In this quick tutorial, we will demonstrate how to create, register, and use a custom scope in a Spring application.

2. Creating a Custom Scope Class

In order to create a custom scope, we must implement the Scope interface. In doing so, we must also ensure that the implementation is thread-safe because scopes can be used by multiple bean factories at the same time.

2.1. Managing the Scoped Objects and Callbacks

One of the first things to consider when implementing a custom Scope class is how you will store and manage the scoped objects and destruction callbacks. This could be done using a map or a dedicated class, for example.

For this article, we’ll do this in a thread-safe manner using synchronized maps.

Let’s begin to define our custom scope class:

public class TenantScope implements Scope {
    private Map<String, Object> scopedObjects
      = Collections.synchronizedMap(new HashMap<String, Object>());
    private Map<String, Runnable> destructionCallbacks
      = Collections.synchronizedMap(new HashMap<String, Runnable>());
...
}

2.2. Retrieving an Object from Scope

To retrieve an object by name from our scope, let’s implement the getObject method. As the JavaDoc states, if the named object does not exist in the scope, this method must create and return a new object.

In our implementation, we check to see if the named object is in our map. If it is, we return it, and if not, we use the ObjectFactory to create a new object, add it to our map, and return it:

@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
    if(!scopedObjects.containsKey(name)) {
        scopedObjects.put(name, objectFactory.getObject());
    }
    return scopedObjects.get(name);
}

Of the five methods defined by the Scope interface, only the get method is required to have a full implementation of the described behavior. The other four methods are optional and may throw UnsupportedOperationException if they don’t need to or can’t support a functionality.

2.3. Registering a Destruction Callback

We must also implement the registerDestructionCallback method. This method provides a callback that is to be executed when the named object is destroyed or if the scope itself is destroyed by the application:

@Override
public void registerDestructionCallback(String name, Runnable callback) {
    destructionCallbacks.put(name, callback);
}

2.4. Removing an Object from Scope

Next, let’s implement the remove method, which removes the named object from the scope and also removes its registered destruction callback, returning the removed object:

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

Note that it is the caller’s responsibility to actually execute the callback and destroy the removed object.

2.5. Getting the Conversation ID

Now, let’s implement the getConversationId method. If your scope supports the concept of a conversation ID, you would return it here. Otherwise, the convention is to return null:

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

2.6. Resolving Contextual Objects

Finally, let’s implement the resolveContextualObject method. If your scope supports multiple contextual objects, you would associate each with a key value, and you would return the object corresponding to the provided key parameter. Otherwise, the convention is to return null:

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

3. Registering the Custom Scope

To make the Spring container aware of your new scope, you need to register it through the registerScope method on a ConfigurableBeanFactory instance. Let’s take a look at this method’s definition:

void registerScope(String scopeName, Scope scope);

The first parameter, scopeName, is used to identify/specify a scope by its unique name. The second parameter, scope, is an actual instance of the custom Scope implementation that you wish to register and use.

Let’s create a custom BeanFactoryPostProcessor and register our custom scope using a ConfigurableListableBeanFactory:

public class TenantBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
        factory.registerScope("tenant", new TenantScope());
    }
}

Now, let’s write a Spring configuration class that loads our BeanFactoryPostProcessor implementation:

@Configuration
public class TenantScopeConfig {

    @Bean
    public static BeanFactoryPostProcessor beanFactoryPostProcessor() {
        return new TenantBeanFactoryPostProcessor();
    }
}

4. Using the Custom Scope

Now that we have registered our custom scope, we can apply it to any of our beans just as we would with any other bean that uses a scope other than singleton (the default scope) — by using the @Scope annotation and specifying our custom scope by name.

Let’s create a simple TenantBean class — we’ll declare tenant-scoped beans of this type in a moment:

public class TenantBean {
    
    private final String name;
    
    public TenantBean(String name) {
        this.name = name;
    }

    public void sayHello() {
        System.out.println(
          String.format("Hello from %s of type %s",
          this.name, 
          this.getClass().getName()));
    }
}

Note that we did not use the class-level @Component and @Scope annotations on this class.

Now, let’s define some tenant-scoped beans in a configuration class:

@Configuration
public class TenantBeansConfig {

    @Scope(scopeName = "tenant")
    @Bean
    public TenantBean foo() {
        return new TenantBean("foo");
    }
    
    @Scope(scopeName = "tenant")
    @Bean
    public TenantBean bar() {
        return new TenantBean("bar");
    }
}

5. Testing the Custom Scope

Let’s write a test to exercise our custom scope configuration by loading up an ApplicationContext, registering our Configuration classes, and retrieving our tenant-scoped beans:

@Test
public final void whenRegisterScopeAndBeans_thenContextContainsFooAndBar() {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    try{
        ctx.register(TenantScopeConfig.class);
        ctx.register(TenantBeansConfig.class);
        ctx.refresh();
        
        TenantBean foo = (TenantBean) ctx.getBean("foo", TenantBean.class);
        foo.sayHello();
        TenantBean bar = (TenantBean) ctx.getBean("bar", TenantBean.class);
        bar.sayHello();
        Map<String, TenantBean> foos = ctx.getBeansOfType(TenantBean.class);
        
        assertThat(foo, not(equalTo(bar)));
        assertThat(foos.size(), equalTo(2));
        assertTrue(foos.containsValue(foo));
        assertTrue(foos.containsValue(bar));

        BeanDefinition fooDefinition = ctx.getBeanDefinition("foo");
        BeanDefinition barDefinition = ctx.getBeanDefinition("bar");
        
        assertThat(fooDefinition.getScope(), equalTo("tenant"));
        assertThat(barDefinition.getScope(), equalTo("tenant"));
    }
    finally {
        ctx.close();
    }
}

And the output from our test is:

Hello from foo of type org.baeldung.customscope.TenantBean
Hello from bar of type org.baeldung.customscope.TenantBean

6. Conclusion

In this quick tutorial, we showed how to define, register, and use a custom scope in Spring.

You can read more about custom scopes in the Spring Framework Reference. You can also take a look at Spring’s implementations of various Scope classes in the Spring Framework repository on GitHub.

As usual, you can find the code samples used in this article over on the GitHub project.

Go deeper into Spring Security with the course:

>> LEARN SPRING SECURITY