Spring bean custom scope
Я попробую привести пример, когда бывает нужен Spring custom scope.Мы — компания B2B и SAAS, и у нас бегут по таймеру некие долгие процессы для каждого из клиентов.У каждого из клиентов есть какие то свойства (имя, тип подписки и т.д.).Раньше мы делали наши сервисы prototype бинами и передавали каждому из них в конструкторе все необходимые свойства клиента и запущенного процесса (flow — имеется ввиду логический процесс, job, а не процесс ОС):
@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 (); //… Это неудобно — во первых можно ошибиться в числе или типе параметров при создании бина, во вторых много boilerplate кода
Поэтому мы сделали свой scope бина — «customer».
Идея вот в чем: я создаю некий «контекст» — объект, хранящий информацию о том, какой процесс сейчас бежит (какой клиент, какой тип процесса — все что нужно знать сервисам) и храню его в ThreadLocal.При создании бина моего scope я этот контекст туда инжектю.
В том же контексте хранится список уже созданных бинов, чтобы каждый бин создавался только один раз на весь процесс.
Когда процесс заканчивается я очищаю ThreadLocal и все бины собираются garbage collector’ом.
Заметьте, что все бины моего scope обязаны имплементировать некий интерфейс. Это нужно только для того, чтобы им инжектить контекст.
Итак, объявляем наш scope в xml:
…
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 (); }
} Как мы видим — в рамках того же процесса (flow) используются те же инстансы бинов (т.е. это scope действительно не стандартный — в prototype создавались бы каждый раз новые, в singleton — одни и те же).А сам контекст берется из ThreadLocal:
public class CustomerContextThreadLocal {
private static 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)); } Oсталось создать интерфейс для всех наших бинов и его абстрактную реализацию:
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 (); } } И после этого наши сервисы выглядят намного красивее:
@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 (); //… Может возникнуть вопрос — мы используем ThreadLocal —, а что если мы вызываем асинхронные методы? Главное, чтобы всё дерево бинов создавалось синхронно, тогда @Autowired будет работать корректно.А если какой нибудь из методов запускается с @ Async — то не страшно, всё работать будет, так как бины уже созданы.
Неплохо также написать тест, которые проверить, что все бины со scope «customer» реализуют ICustomerScopeBean и наоборот:
@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)); } } }