Перейти к основному содержимому

Пользовательская область действия весной

· 6 мин. чтения

1. Обзор

По умолчанию Spring предоставляет две стандартные области действия компонента ( «singleton» и «prototype» ), которые можно использовать в любом приложении Spring, а также три дополнительных области действия компонента ( «request» , «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 , используется для идентификации/указания области по ее уникальному имени. Второй параметр, scope , является фактическим экземпляром пользовательской реализации Scope , которую вы хотите зарегистрировать и использовать.

Давайте создадим собственный 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.foreach.customscope.TenantBean
Hello from bar of type org.foreach.customscope.TenantBean

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

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

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

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