«1. Обзор

По умолчанию Spring предоставляет две стандартные области действия компонента («singleton» и «prototype»), которые можно использовать в любом приложении Spring, а также три дополнительных области действия компонента («request», «request», «prototype»). «session» и «globalSession») для использования только в веб-приложениях.

Стандартные области действия bean-компонентов не могут быть переопределены, и обычно считается плохой практикой переопределять области видимости веб-компонентов. Однако у вас может быть приложение, требующее отличных или дополнительных возможностей от тех, которые находятся в предоставленных областях.

Например, если вы разрабатываете многопользовательскую систему, вы можете захотеть предоставить отдельный экземпляр определенного компонента или набора компонентов для каждого арендатора. Spring предоставляет механизм для создания настраиваемых областей действия для таких сценариев, как этот.

В этом кратком руководстве мы покажем, как создавать, регистрировать и использовать пользовательскую область видимости в приложении Spring.

2. Создание пользовательского класса области видимости

Чтобы создать пользовательскую область видимости, мы должны реализовать интерфейс Scope. При этом мы также должны гарантировать, что реализация является потокобезопасной, поскольку области могут использоваться несколькими фабриками компонентов одновременно.

2.1. Управление объектами с областью действия и обратными вызовами

Одна из первых вещей, которую следует учитывать при реализации пользовательского класса Scope, — это то, как вы будете хранить и управлять объектами с областью действия и обратными вызовами уничтожения. Это можно сделать, например, с помощью карты или специального класса.

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

Давайте начнем определять наш собственный класс области видимости:

public class TenantScope implements Scope {
    private Map<String, Object> scopedObjects
      = Collections.synchronizedMap(new HashMap<String, Object>());
    private Map<String, Runnable> destructionCallbacks
      = Collections.synchronizedMap(new HashMap<String, Runnable>());
...
}

2.2. Получение объекта из области видимости

Чтобы получить объект по имени из области видимости, давайте реализуем метод getObject. Как указано в JavaDoc, если именованный объект не существует в области видимости, этот метод должен создать и вернуть новый объект.

В нашей реализации мы проверяем, есть ли именованный объект на нашей карте. Если это так, мы возвращаем его, а если нет, мы используем ObjectFactory для создания нового объекта, добавляем его на нашу карту и возвращаем:

@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
    if(!scopedObjects.containsKey(name)) {
        scopedObjects.put(name, objectFactory.getObject());
    }
    return scopedObjects.get(name);
}

Из пяти методов, определенных интерфейсом Scope, только get требуется для полной реализации описанного поведения. Остальные четыре метода являются необязательными и могут вызывать исключение UnsupportedOperationException, если они не нужны или не могут поддерживать какую-либо функциональность.

2.3. Регистрация обратного вызова уничтожения

Мы также должны реализовать метод registerDestructionCallback. Этот метод обеспечивает обратный вызов, который должен выполняться при уничтожении именованного объекта или при уничтожении области видимости приложением:

@Override
public void registerDestructionCallback(String name, Runnable callback) {
    destructionCallbacks.put(name, callback);
}

2.4. Удаление объекта из области видимости

Далее давайте реализуем метод удаления, который удаляет именованный объект из области видимости, а также удаляет его зарегистрированный обратный вызов уничтожения, возвращая удаленный объект:

@Override
public Object remove(String name) {
    destructionCallbacks.remove(name);
    return scopedObjects.remove(name);
}

Обратите внимание, что ответственность за это лежит на вызывающем объекте. чтобы фактически выполнить обратный вызов и уничтожить удаленный объект.

2.5. Получение идентификатора беседы

Теперь давайте реализуем метод getConversationId. Если ваша область поддерживает концепцию идентификатора беседы, вы должны вернуть его здесь. В противном случае по соглашению возвращается null:

@Override
public String getConversationId() {
    return "tenant";
}

2.6. Разрешение контекстных объектов

Наконец, давайте реализуем метод resolveContextualObject. Если ваша область поддерживает несколько контекстных объектов, вы должны связать каждый со значением ключа и вернуть объект, соответствующий предоставленному ключевому параметру. В противном случае по соглашению возвращается значение null:

@Override
public Object resolveContextualObject(String key) {
    return null;
}

3. Регистрация пользовательской области видимости

Чтобы контейнер Spring знал о вашей новой области действия, вам необходимо зарегистрировать ее с помощью метода registerScope в экземпляре ConfigurableBeanFactory. Давайте взглянем на определение этого метода:

void registerScope(String scopeName, Scope scope);

Первый параметр, scopeName, используется для идентификации/указания области действия по ее уникальному имени. Второй параметр, область действия, является фактическим экземпляром пользовательской реализации области действия, которую вы хотите зарегистрировать и использовать.

«Давайте создадим собственный BeanFactoryPostProcessor и зарегистрируем нашу пользовательскую область с помощью ConfigurableListableBeanFactory:

public class TenantBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
        factory.registerScope("tenant", new TenantScope());
    }
}

Теперь давайте напишем класс конфигурации Spring, который загружает нашу реализацию BeanFactoryPostProcessor:

@Configuration
public class TenantScopeConfig {

    @Bean
    public static BeanFactoryPostProcessor beanFactoryPostProcessor() {
        return new TenantBeanFactoryPostProcessor();
    }
}

4. Использование пользовательской области

Теперь что мы зарегистрировали нашу пользовательскую область, мы можем применить ее к любому из наших bean-компонентов так же, как и к любому другому bean-компоненту, который использует область действия, отличную от singleton (область действия по умолчанию) — с помощью аннотации @Scope и указания нашего пользовательского область действия по имени.

Давайте создадим простой класс TenantBean — через мгновение мы объявим bean-компоненты этого типа в области клиента:

public class TenantBean {
    
    private final String name;
    
    public TenantBean(String name) {
        this.name = name;
    }

    public void sayHello() {
        System.out.println(
          String.format("Hello from %s of type %s",
          this.name, 
          this.getClass().getName()));
    }
}

Обратите внимание, что мы не использовали аннотации @Component и @Scope на уровне класса для этот класс.

Теперь давайте определим некоторые bean-компоненты с областью действия арендатора в классе конфигурации:

@Configuration
public class TenantBeansConfig {

    @Scope(scopeName = "tenant")
    @Bean
    public TenantBean foo() {
        return new TenantBean("foo");
    }
    
    @Scope(scopeName = "tenant")
    @Bean
    public TenantBean bar() {
        return new TenantBean("bar");
    }
}

5. Тестирование пользовательской области

Давайте напишем тест для проверки нашей конфигурации пользовательской области, загрузив ApplicationContext, зарегистрировав наш Классы конфигурации и извлечение наших bean-компонентов в области клиента:

@Test
public final void whenRegisterScopeAndBeans_thenContextContainsFooAndBar() {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    try{
        ctx.register(TenantScopeConfig.class);
        ctx.register(TenantBeansConfig.class);
        ctx.refresh();
        
        TenantBean foo = (TenantBean) ctx.getBean("foo", TenantBean.class);
        foo.sayHello();
        TenantBean bar = (TenantBean) ctx.getBean("bar", TenantBean.class);
        bar.sayHello();
        Map<String, TenantBean> foos = ctx.getBeansOfType(TenantBean.class);
        
        assertThat(foo, not(equalTo(bar)));
        assertThat(foos.size(), equalTo(2));
        assertTrue(foos.containsValue(foo));
        assertTrue(foos.containsValue(bar));

        BeanDefinition fooDefinition = ctx.getBeanDefinition("foo");
        BeanDefinition barDefinition = ctx.getBeanDefinition("bar");
        
        assertThat(fooDefinition.getScope(), equalTo("tenant"));
        assertThat(barDefinition.getScope(), equalTo("tenant"));
    }
    finally {
        ctx.close();
    }
}

И результат нашего теста:

Hello from foo of type org.baeldung.customscope.TenantBean
Hello from bar of type org.baeldung.customscope.TenantBean

6. Заключение

В этом кратком руководстве мы показали, как определить, зарегистрировать, и используйте пользовательскую область в Spring.

Подробнее о пользовательских областях можно прочитать в Справочнике по Spring Framework. Вы также можете ознакомиться с реализациями Spring различных классов Scope в репозитории Spring Framework на GitHub.

Как обычно, вы можете найти примеры кода, использованные в этой статье, в проекте GitHub.