Я попытаюсь привести пример, когда необходима пользовательская область видимости 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
-
Звикки, Фриц
19 Oct, 24 -
Права Человека
19 Oct, 24 -
Корпоративный Троллинг. Первая Часть
19 Oct, 24 -
Тесная Связь: Amd Gpu И Directx 11
19 Oct, 24