Spring中的自定义作用域

2023/05/13

1. 概述

Spring提供了两个可以在任何Spring应用程序中使用的标准bean作用域(“singleton”和“prototype”), 以及三个额外的bean作用域(“request”、“session”和“Application”)供在web应用程序中使用。

标准bean作用域不能被覆盖,并且通常认为覆盖Web作用域是一种不好的做法。 但是,你的应用程序可能需要与所提供作用域中的不同功能或额外的功能。

例如,如果你正在开发多租户系统,你可能希望为每个租户提供特定bean或一组bean的单独实例。 Spring提供了一种为诸如此类的场景创建自定义作用域的机制。

在本文中,我们将演示如何在Spring应用程序中创建、注册和使用自定义作用域

2. 创建自定义作用域类

为了创建自定义作用域,我们必须实现Scope接口。在这样做时,我们还必须确保实现是线程安全的,因为作用域可以被多个bean工厂同时使用。

2.1 管理作用域对象和回调

在实现自定义作用域时,首先要考虑的是如何存储和管理作用域对象和销毁回调。例如,这可以使用map或专用类来完成。

对于本文,我们使用同步map以线程安全的方式实现这一点。

让我们开始定义我们的自定义作用域类:

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

2.2 从作用域检索对象

要从我们的作用域中按名称检索对象,我们需要实现getObject方法。 如果名为name的对象不存在于作用域中,则该方法必须创建并返回一个新对象

在我们的实现中,我们检查名为name的对象是否在我们的map中。如果是,我们返回它,如果不是,我们使用ObjectFactory创建一个新对象, 将它添加到我们的map中,然后返回它:

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

    @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方法。此方法提供了一个回调,当名为name的对象被销毁或作用域本身被应用程序销毁时,将执行该回调:

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

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

2.4 从作用域中删除对象

接下来,让我们实现remove方法,该方法将名为name的对象从作用域中移除,同时移除其注册的销毁回调,然后返回移除的对象:

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

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

请注意,调用方负责实际执行回调并销毁删除的对象

2.5 获取会话ID

现在,让我们实现getConversationId方法。如果你的作用域支持会话ID的概念,你需要在这里返回它。否则可以返回null:

public class TenantScope implements Scope {

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

2.6 解析上下文对象

最后,让我们实现resolveContextualObject方法。如果你的作用域支持多个上下文对象,你可以将每个对象与一个key相关联, 并返回与提供的key参数对应的对象。否则可以返回null:

public class TenantScope implements Scope {

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

3. 注册自定义作用域

为了让Spring容器知道我们的作用域,需要通过ConfigurableBeanFactory实例上的registerScope方法注册它。我们来看看这个方法的定义:

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());
    }
}

现在,让我们编写一个加载BeanFactoryPostProcessor实现的Spring配置类:


@Configuration
public class TenantScopeConfig {

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

4. 使用自定义作用域

现在我们已经注册了我们的自定义作用域,我们可以将它应用到我们的任何bean, 就像我们使用任何其他使用单例以外的作用域(默认作用域)的bean一样,通过使用@Scope注解并指定我们的自定义作用域的名字。

让我们创建一个简单的TenantBean类,稍后我们将声明该bean的作用域为tenant:

public record TenantBean(String name) {

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

请注意,我们没有在此类上使用类级别的@Component和@Scope注解。

现在,让我们在配置类中定义一些tenant作用域的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、注册我们的配置类和检索我们的tenant作用域bean来验证这些实现:

class TenantScopeIntegrationTest {

    @Test
    final void whenRegisterScopeAndBeans_thenContextContainsFooAndBar() {
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
            ctx.register(TenantScopeConfig.class);
            ctx.register(TenantBeansConfig.class);
            ctx.refresh();

            TenantBean foo = ctx.getBean("foo", TenantBean.class);
            foo.sayHello();
            TenantBean bar = 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"));
        }
    }
}

控制台的输出为:

Hello from foo of type cn.tuyucheng.taketoday.customscope.TenantBean
Hello from bar of type cn.tuyucheng.taketoday.customscope.TenantBean

6. 总结

在这个教程中,我们演示了如何在Spring中定义、注册和使用自定义作用域。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章