Custom Scope in Spring
In this article, we will discuss Custom Scope in Spring. We will show how to create and use a Custom scope in a Spring application.
Introduction
Spring provides several beans scopes. Out of all these scopes, Singleton and Prototype are available for any Spring application. Spring also provides few other Bean scopes (e.g. Request, Session etc.) which are only available for a specific context (Request scope is only available in the web context)? For most of the applications, out of the box, bean scopes will meet our need, however large enterprise applications may require a scope with additional capabilities or features which are not available in these bean scopes.
Let’s take an example of a multi-tenant system where we want to provide bean instance based on the tenant system requesting this instance. To handle all such requirement, Spring provides a way to create a custom scope.
[pullquote align=”normal”]Redefining existing Spring bean scope is not a good practice and not advisable. We cannot override singleton and prototype scopes. [/pullquote]
1. Creating Spring Custom Scope
We need to implement scope interface to create our custom scope in Spring. This interface declares 4 methods.
- get the object from the scope.
- remove the object from the scope.
- destroy the object.
1.1 Get an Object from Scope
To get an object from the underlying scope, we need to implement getObject method defined in the scope interface.As per the method contract, if the given object is not found in the underlying spring storage mechanism, we must create and return a new object.
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object scopedObject = tenantScope.get(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
tenantScope.put(name, scopedObject);
}
return scopedObject;
}
Let’s cover few important points for the above code
- We checked if there is an object in the tenant scope for the name.
- If no object found, we created a new object using objectFacotry and placed this object in the tenant scope map for the future use and returned this object.
[pullquote align=”normal”]get method is the only method required to have a full implementation. All other method implementations are optional. Please refer to Spring doc for more detail [/pullquote]
1.2 Register Destruction Callback
the registerdestructioncallback method will be executed when we destroy/ remove an object from the scope. This method is optional. We may need to implement this method if we are planning to define remove method.
@Override
public void registerDestructionCallback(String name, Runnable runnable) {
destructionCallbacks.put(name, runnable);
}
1.3 Removing Object from Scope
To remove object based on the name, we need to implement remove method defined in the Scope interface.
While removing custom scope object, we need to keep in mind following points
- In a case named object is removed, the method should return removed object or null if no object with a name found.
- The caller is responsible to execute the callback for the object and remove it.
- This is an optional method. We can return “UnsupportedOperationException” if there is no plan to support this method.
@Override
public Object remove(String name) {
Object scopedObject = tenantScope.get(name);
if (scopedObject != null) {
tenantScope.remove(name);
return scopedObject;
}
else {
return null;
}
}
1.4 Conversation ID
This is an optional implementation. It depends on the underlying storage mechanism. We can return null if this is not supported.
@Override
public String getConversationId() {
return "tenant";
}
1.5 Contextual Object Resolution
This method is useful if our scope supports multiple contextual objects. This is typically implemented by assigning it with a key and value and object assigned with the key will be returned. A common use case if RequestScope in Spring.
@Override
public Object resolveContextualObject(String s) {
return null;
}
Here is our complete TenantScope class
public class TenantScope implements Scope {
private Map<String, Object> tenantScope = Collections.synchronizedMap(new HashMap<>());
private Map<String, Runnable> destructionCallbacks = Collections.synchronizedMap(new HashMap<String, Runnable>());
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object scopedObject = tenantScope.get(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
tenantScope.put(name, scopedObject);
}
return scopedObject;
}
@Override
public Object remove(String name) {
Object scopedObject = tenantScope.get(name);
if (scopedObject != null) {
tenantScope.remove(name);
return scopedObject;
}
else {
return null;
}
}
@Override
public void registerDestructionCallback(String name, Runnable runnable) {
destructionCallbacks.put(name, runnable);
}
@Override
public Object resolveContextualObject(String s) {
return null;
}
@Override
public String getConversationId() {
return "tenant";
}
}
2. Registering Custom Scope
After we created our custom scope, we need to let Spring container know about our custom scope.To register custom scope with Spring; we need to use registerScope method declared on the ConfigurableBeanFacgtory interface.
void registerScope(java.lang.String scopeName, Scope scope)
Let’s see how to register our custom scope with Spring container
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
factory.registerScope("tenant",new TenantScope());
}
}
While registering custom scope with Spring container, we need to know of following important points
- The first argument to register method is the unique name with the scope (in our example it is the tenant).
- We need to pass actual instance which we need to associate with a name as the second argument for this method.
As the last step, we need Spring configuration to load our custom BeanFactoryPostProcessor.
@Configuration
public class CustomScopeConfig {
public static BeanFactoryPostProcessor beanFactoryPostProcessor(){
return new CustomBeanFactoryPostProcessor();
}
}
3. Using Custom Scope
We created custom scope in Spring, register this custom scope with Spring container. We can use our custom scope by using @Scope annotation and passing custom scope name, alternatively, we can also create a custom annotation to use our custom scope. Let’s create a simple Bean class for our testing:
public class GreetingService {
public void greeting(){
System.out.println("Greetings from custom scope");
}
}
3.1 Use Custom Scope by @Scope Annotation
To use a new custom scope, we can declare our new bean by using @Scope annotation and passing custom scope name
@Configuration
public class CustomScopeConfig {
@Bean
public static BeanFactoryPostProcessor beanFactoryPostProcessor(){
return new CustomBeanFactoryPostProcessor();
}
}
3.2 Use Custom Annotation
import org.springframework.context.annotation.Scope;
@Scope("tenant")
public @interface TenantScoped {
}
//declaration
@Configuration
public class CustomBeanConfig {
@TenantScoped
@Bean
public GreetingService greetingService(){
return new GreetingService();
}
}
Summary
In this post, we covered Creating Custom Scope in Spring. We learned how to define and use the custom scope in our application.
Hi Umesh,
It was really useful. I earlier not had an idea to use a custom scope with multi-tenancy.
Hello Manoj,
I am happy that it was helpful you.
Thanks
Umesh