Я попробую привести пример, когда бывает нужен 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));
}
}
}
Автор: javax