Пользовательская Область Spring Bean

Я попытаюсь привести пример, когда необходима пользовательская область видимости Spring. Мы являемся компанией B2B и SAAS, и у нас есть несколько длительных процессов, выполняемых по таймеру для каждого из наших клиентов.

У каждого клиента есть некоторые свойства (имя, тип подписки и т. д.).

Предварительно мы сделали наши bean-компоненты-прототипы сервисов и передали каждому из них в конструкторе все необходимые свойства клиента и запущенного процесса (flow — имеется в виду логический процесс, задание, а не процесс ОС):

  
  
  
  
  
  
  
  
   

@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(); //.



Это неудобно — во-первых, при создании бина можно ошибиться в количестве или типе параметров, во-вторых, там много шаблонного кода Поэтому мы сделали свой собственный бин области видимости — «клиент».

Идея такая: я создаю некий «контекст» — объект, хранящий информацию о том, какой процесс в данный момент запущен (какой клиент, какой тип процесса — всё, что нужно знать сервисам) и сохраняю в ThreadLocal. При создании моего компонента области действия я добавляю туда этот контекст. Список уже созданных bean-компонентов хранится в том же контексте, поэтому каждый bean-компонент создается только один раз за весь процесс.

Когда процесс завершается, я очищаю ThreadLocal и все корзины собирается сборщиком мусора.

Обратите внимание, что все bean-компоненты моей области действия должны реализовывать определенный интерфейс.

Это необходимо только для того, чтобы внедрить в них контекст. Итак, давайте объявим нашу область видимости в 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> .



Давайте реализуем нашу область видимости:

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(); } }

Как мы видим, внутри одного процесса (потока) используются одни и те же экземпляры bean-компонентов (т.е.

эта область действия действительно не стандартная — в прототипе каждый раз создавались бы новые, в синглтоне — одни и те же).

А сам контекст берется из 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)); }

Остаётся только создать интерфейс для всех наших бинов и его абстрактную реализацию:

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, то ничего страшного, все будет работать, поскольку бины уже созданы.

Также неплохо написать тест, который проверяет, что все bean-компоненты с областью действия «клиент» реализуют 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)); } } }

Теги: #java #spring framework #java

Вместе с данным постом часто просматривают: