...

четверг, 5 июня 2014 г.

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:



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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий