Spring bean custom scope

от автора

Я попробую привести пример, когда бывает нужен Spring custom scope.

Мы — компания B2B и SAAS, и у нас бегут по таймеру некие долгие процессы для каждого из клиентов.
У каждого из клиентов есть какие то свойства (имя, тип подписки и т.д.).
Раньше мы делали наши сервисы prototype бинами и передавали каждому из них в конструкторе все необходимые свойства клиента и запущенного процесса (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();  //... 

Это неудобно — во первых можно ошибиться в числе или типе параметров при создании бина,
во вторых много boilerplate кода

Поэтому мы сделали свой scope бина — «customer».

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

В том же контексте хранится список уже созданных бинов, чтобы каждый бин создавался только один раз на весь процесс.

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

Заметьте, что все бины моего scope обязаны имплементировать некий интерфейс. Это нужно только для того, чтобы им инжектить контекст.

Итак, объявляем наш scope в 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> ... 

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

ссылка на оригинал статьи http://habrahabr.ru/post/225397/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *