Пользовательский объем весной

Пользовательские возможности весной

1. обзор

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

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

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

В этом кратком руководстве мы продемонстрируемhow to create, register, and use a custom scope in a Spring application.

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

Чтобы создать настраиваемую область действия,we must implement the Scope interface. При этом мы также должныensure that the implementation is thread-safe, потому что области видимости могут использоваться несколькими фабриками компонентов одновременно.

2.1. Управление ограниченными объектами и обратными вызовами

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

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

Начнем с определения нашего настраиваемого класса области:

public class TenantScope implements Scope {
    private Map scopedObjects
      = Collections.synchronizedMap(new HashMap());
    private Map destructionCallbacks
      = Collections.synchronizedMap(new HashMap());
...
}

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

Чтобы получить объект по имени из нашей области видимости, давайте реализуем методgetObject. Как указано в JavaDoc,if the named object does not exist in the scope, this method must create and return a new object.

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

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

Из пяти методов, определенных интерфейсомScope,only the get method is required to have a full implementation описанного поведения. Остальные четыре метода являются необязательными и могут вызыватьUnsupportedOperationException, если они не нуждаются или не могут поддерживать какую-либо функцию.

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

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

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

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

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

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

Обратите внимание, чтоit is the caller’s responsibility to actually execute the callback and destroy the removed object.

2.5. Получение идентификатора разговора

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

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

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

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

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

3. Регистрация настраиваемой области

Чтобы контейнер Spring знал о вашей новой области видимости, вам нужноregister it through the registerScope method on a ConfigurableBeanFactory instance. Давайте посмотрим на определение этого метода:

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, зарегистрировав наши классыConfiguration и получив наши 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 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.example.customscope.TenantBean
Hello from bar of type org.example.customscope.TenantBean

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

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

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

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