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:

… Имплементируем наш 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 (); }

} Как мы видим — в рамках того же процесса (flow) используются те же инстансы бинов (т.е. это scope действительно не стандартный — в prototype создавались бы каждый раз новые, в singleton — одни и те же).А сам контекст берется из ThreadLocal:

public class CustomerContextThreadLocal {

private static ThreadLocal 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)); } 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)); } } }

© Habrahabr.ru