📜 ⬆️ ⬇️

Spring bean custom scope

I will try to give an example when the Spring custom scope is needed.

We are a B2B and SAAS company, and we run on a timer some long processes for each of the clients.
Each client has some properties (name, subscription type, etc.).
Previously, we did our prototype services with bins and passed each of them in the constructor all the necessary properties of the client and the running process (flow means a logical process, job, and not an OS process):

@Service @Scope("prototype") public class ServiceA { private Customer customer; private ReloadType reloadType; private ServiceB serviceB; @Autowired private ApplicationContext context; public ServiceA(final Customer customer, final ReloadType reloadType) { this.customer = customer; this.reloadType = reloadType; } @PostConstruct public void init(){ serviceB = (ServiceB) context.getBean("serviceB",customer, reloadType); } public void doSomethingInteresting(){ doSomthingWithCustomer(customer,reloadType); serviceB.doSomethingBoring(); } private void doSomthingWithCustomer(final Customer customer, final ReloadType reloadType) { } } 


 @Service @Scope("prototype") public class ServiceB { private Customer customer; private ReloadType reloadType; public ServiceB(final Customer customer, final ReloadType reloadType) { this.customer = customer; this.reloadType = reloadType; } public void doSomethingBoring(){ } } 

')
  //... ServiceA serviceA = (ServiceA) context.getBean("serviceA",customer, ReloadType.FullReaload); serviceA.doSomethingInteresting(); //... 


This is inconvenient - firstly, you can make a mistake in the number or type of parameters when creating a bean,
secondly a lot of boilerplate code

Therefore, we made our scope bean - “customer”.

The idea is this: I create a certain “context” - an object that stores information about what process is running (which client, which type of process is all that services need to know) and store it in ThreadLocal.
At creation of a bin of my scope I this context there injection.

In the same context, a list of already created bins is stored so that each bin is created only once per process.

When the process ends, I clear the ThreadLocal and all the beans are collected by the garbage collector.

Notice that all the beans in my scope must implement a certain interface. This is only necessary in order to inject context.

So, we declare our scope in xml:

 .. <bean id="customerScope" class="com.scope.CustomerScope"/> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="customer" value-ref="customerScope"/> </map> </property> </bean> ... 


We implement our Scope:
 public class CustomerScope implements Scope { @Override public Object get(String name, ObjectFactory<?> objectFactory) { CustomerContext context = resolve(); Object result = context.getBean(name); if (result == null) { result = objectFactory.getObject(); ICustomerScopeBean syncScopedBean = (ICustomerScopeBean) result; syncScopedBean.setContext(context); Object oldBean = context.setBean(name, result); if (oldBean != null) { result = oldBean; } } return result; } @Override public Object remove(String name) { CustomerContext context = resolve(); return context.removeBean(name); } protected CustomerContext resolve() { return CustomerContextThreadLocal.getCustomerContext(); } @Override public void registerDestructionCallback(String name, Runnable callback) { } @Override public Object resolveContextualObject(String key) { return null; } @Override public String getConversationId() { return resolve().toString(); } } 


As we can see, in the framework of the same process (flow) the same bin instances are used (i.e. this scope is not really standard - in prototype each time new would be created, in singleton the same ones would be created).
And the context itself is taken from ThreadLocal:

 public class CustomerContextThreadLocal { private static ThreadLocal<CustomerContext> customerContext = new ThreadLocal<>(); public static CustomerContext getCustomerContext() { return customerContext.get(); } public static void setSyncContext(CustomerContext context) { customerContext.set(context); } public static void clear() { customerContext.remove(); } private CustomerContextThreadLocal() { } public static void setSyncContext(Customer customer, ReloadType reloadType) { setSyncContext(new CustomerContext(customer, reloadType)); } 


It remains to create an interface for all our bins and its abstract implementation:
 public interface ICustomerScopeBean { void setContext(CustomerContext context); } public class AbstractCustomerScopeBean implements ICustomerScopeBean { protected Customer customer; protected ReloadType reloadType; @Override public void setContext(final CustomerContext context) { customer = context.getCustomer(); reloadType = context.getReloadType(); } } 


And after that, our services look much more beautiful:
 @Service @Scope("customer") public class ServiceA extends AbstractCustomerScopeBean { @Autowired private ServiceB serviceB; public void doSomethingInteresting() { doSomthingWithCustomer(customer, reloadType); serviceB.doSomethingBoring(); } private void doSomthingWithCustomer(final Customer customer, final ReloadType reloadType) { } } @Service @Scope("customer") public class ServiceB extends AbstractCustomerScopeBean { public void doSomethingBoring(){ } } //.... CustomerContextThreadLocal.setSyncContext(customer, ReloadType.FullReaload); ServiceA serviceA = context.getBean(ServiceA.class); serviceA.doSomethingInteresting(); //..... 


The question may arise - we use ThreadLocal - and what if we call asynchronous methods?
The main thing is that the entire bean tree is created synchronously, then @Autowired will work correctly.
And if one of the methods starts with @ Async, then it’s not scary, everything will work, since the bins have already been created.

It is also a good idea to write a test that checks that all bins in the scope of "customer" implement ICustomerScopeBean and vice versa:
  @ContextConfiguration(locations = {"classpath:beans.xml"}, loader = GenericXmlContextLoader.class) @RunWith(SpringJUnit4ClassRunner.class) public class CustomerBeanScopetest { @Autowired private AbstractApplicationContext context; @Test public void testScopeBeans() throws ClassNotFoundException { ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames(); for (String beanDef : beanDefinitionNames) { BeanDefinition def = beanFactory.getBeanDefinition(beanDef); String scope = def.getScope(); String beanClassName = def.getBeanClassName(); if (beanClassName == null) continue; Class<?> aClass = Class.forName(beanClassName); if (ICustomerScopeBean.class.isAssignableFrom(aClass)) assertTrue(beanClassName + " should have scope 'customer'", scope.equals("customer")); if (scope.equals("customer")) assertTrue(beanClassName + " should implement 'ICustomerScopeBean'", ICustomerScopeBean.class.isAssignableFrom(aClass)); } } } 


Source: https://habr.com/ru/post/225397/


All Articles