Spring bean custom scope

в 18:51, , рубрики: Без рубрики

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js