我希望能够根据用户在运行时所属的租户选择特定的Spring(或Grails)上下文配置。假设我使用Spring Security并在登录期间检索tenantId。 想象一下,现在我有两个租户,他们支付不同的佣金。如何在没有太多管道的情况下将特定服务注入控制器?这是两个不同的背景。所以,我应该根据租户注入不同的ExchangeService。
@Configuration
public class FooTenant{
@Bean
public ExchangeService bar() {
return new ZeroCommisionExchangeService ();
}
}
@Configuration
public class BarTenant{
@Bean
public ExchangeService bar() {
return new StandardCommisionExchangeService ();
}
}
编辑: 我知道我可以获得对Spring上下文的引用并“手动”请求服务,但我正在寻找一个更通用的解决方案,其中这个问题由IoC框架解决。
答案 0 :(得分:2)
几年前,我们需要这样的事情,但仅适用于DataSource
和ViewResolvers
。我们使用spring {TargetSource
解决方案开发了一个解决方案。 (最初我们使用了HotswappableTargetSource
,但这对我们的用例来说还不够。
我们开发的代码在多租户目录中可用here。
完全可配置且灵活。
基本上你所做的就是配置一个ContextSwappableTargetSource
并告诉它需要返回什么类型的接口/类。
<bean id="yourTentantBasedServiceId" class="biz.deinum.multitenant.aop.target.ContextSwappableTargetSource">
<constructor-arg value="ExchangeService" />
</bean>
默认是基于tenantId在ApplicationContext中查找bean(请参阅BeanFactoryTargetRegistry
)。但是,您可以指定其中的一个或多个(我们使用JndiLookupTargetRegistry
动态查找数据源,这允许在不重新启动应用程序的情况下动态添加租户)。
如果您明确配置BeanFactoryTargetRegistry
,则可以添加prefix
和suffix
。
<bean id="exchangeService" class="biz.deinum.multitenant.aop.target.ContextSwappableTargetSource">
<constructor-arg value="ExchangeService" />
<property name="targetRegistry>
<bean class="biz.deinum.multitenant.aop.target.registry.impl.BeanFactoryTargetRegistry">
<property suffix="ExchangeService"/>
</bean>
</property>
</bean>
现在对于foo,它将查找名为fooExchangeService
的bean和条barExchangeService
的bean。
tenantId存储在ThreadLocal
中,ContextHolder
包含在Filter
内。你需要找到一种填充和清除本地线程的方法(一般来说,servlet ExchangeService
就是这样做的。
在您的代码中,您现在可以简单地使用接口tenantId
,并在运行时根据{{1}}查找正确的实现。
另见http://mdeinum.wordpress.com/2007/01/05/one-application-per-client-database/
答案 1 :(得分:1)
假设您已经定义了不同的服务,您可以从上下文中获取它们的bean并使用它。在我的示例中,所有服务都实现了serviceMethod
,并根据某些标准选择适当的服务。我唯一不确定的是Multitenancy
如何影响这一点。
import org.springframework.context.ApplicationContext
class ServiceManagerController {
def serviceManager
def index() {
ApplicationContext ctx = grails.util.Holders.grailsApplication.mainContext
serviceManager = ctx.getBean(params.serviceName); //firstService or secondService
render serviceManager.serviceMethod()
}
}
FirstService
class FirstService {
def serviceMethod() {
return "first"
}
}
SecondService:
class SecondService {
def serviceMethod() {
return "second"
}
}
答案 2 :(得分:0)
在一个答案中转换我的注释,一个可能的解决方案是创建一个spring工厂bean,它接收所有他需要决定在创建实例时需要返回哪些服务。
转换为Grails:
public interface ChoosableServiceIntf {
String getName();
}
class NormalService implements ChoosableServiceIntf {
public String getName() {
return getClass().name;
}
}
class ExtendedService implements ChoosableServiceIntf {
public String getName() {
return getClass().name
}
}
class ChoosableServiceFactory {
static ChoosableServiceIntf getInstance(String decisionParam) {
if(decisionParam == 'X') {
return applicationContext.getBean('extendedService')
}
return applicationContext.getBean('normalService')
}
static ApplicationContext getApplicationContext() {
return Holders.grailsApplication.mainContext
}
}
这里我们有两个服务,ChoosableServiceFactory
负责知道女巫是正确的。
然后,您需要使用方法ApplicationContext#getBean(String, Object[])
返回正确的实例,并且由于运行时参数,还将生成工厂prototyped scope
。
测试它的控制器:
class MyController {
def grailsApplication
def index() {
ChoosableServiceIntf service = grailsApplication.mainContext.getBean('choosableServiceFactory', ["X"] as Object[])
ChoosableServiceIntf serviceNormal = grailsApplication.mainContext.getBean('choosableServiceFactory', ["N"] as Object[])
render text: "#1 - ${service.class.name} , #2 - ${serviceNormal.class.name}"
}
}
这将打印#1 - dummy.ExtendedService , #2 - dummy.NormalService
bean的声明将是:
choosableServiceFactory(ChoosableServiceFactory) { bean ->
bean.scope = 'prototype'
bean.factoryMethod = 'getInstance'
}
normalService(NormalService)
extendedService(ExtendedService)
答案 3 :(得分:0)
我使用以下代码:
public class ConfigurableProxyFactoryBean implements FactoryBean<Object>, BeanNameAware {
@Autowired
private ApplicationContextProvider applicationContextProvider;
private Class<?> proxyType;
private String beanName;
private Object object;
private Object fallbackObject;
private Object monitor = new Object();
private ConfigurableProxy proxy;
public ConfigurableProxyFactoryBean(Class<?> proxyType) {
this.proxyType = proxyType;
}
public Object getFallbackObject() {
return fallbackObject;
}
public void setFallbackObject(Object fallbackObject) {
synchronized (monitor) {
this.fallbackObject = fallbackObject;
if (proxy != null) {
proxy.setFallbackObject(fallbackObject);
}
}
}
@Override
public void setBeanName(String name) {
beanName = name;
}
@Override
public Object getObject() throws Exception {
synchronized (monitor) {
if (object == null) {
@SuppressWarnings("unchecked")
Class<Object> type = (Class<Object>)proxyType;
proxy = new ConfigurableProxy(applicationContextProvider, beanName);
proxy.setFallbackObject(fallbackObject);
object = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[] { type }, proxy);
}
return object;
}
}
@Override
public Class<?> getObjectType() {
return proxyType;
}
@Override
public boolean isSingleton() {
return true;
}
}
class ConfigurableProxy implements InvocationHandler {
public ConfigurableProxy(ApplicationContextProvider appContextProvider, String beanName) {
this.appContextProvider = appContextProvider;
this.beanName = beanName;
}
private ApplicationContextProvider appContextProvider;
private String beanName;
private Object fallbackObject;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
ApplicationContext appContext = appContextProvider.getApplicationContext();
String name = "$&&#" + beanName;
Object bean = appContext.containsBean(name) ? appContext.getBean(name) : fallbackObject;
return method.invoke(bean, args);
}
public void setFallbackObject(Object fallbackObject) {
this.fallbackObject = fallbackObject;
}
}
ApplicationContextProvider
已实施,根据当前的租户选择ApplicationContext
。
在XML配置中,它的使用方式如下:
<bean class="my.package.infrastructure.ConfigurableProxyFactoryBean" name="beanName">
<constructor-arg>
<value type="java.lang.Class">my.package.model.ServiceInterface</value>
</constructor-arg>
<property name="fallbackObject">
<bean class="my.package.service.DefaultServiceImplementation"/>
</property>
</bean>
在Tennant配置中:
<bean class="my.package.service.ServiceImplementationA" name="$&&#beanName"/>
要在您刚写的地方注入此服务:
public class MyController {
@Autowired
private ServiceInterface service;
}
你也要实施ApplicationContextProvider
,我不会与我分享。实施起来并不是很难。例如,您的实现只能在ThreadLocal
中存储上下文。您创建了自己的ServletContextListener
,每个查询都会获取当前的信号,并将其存储到您的ApplicationContextProvider
实施中。
答案 4 :(得分:0)
新租户范围和服务定位器可以提供帮助
租户范围将保证为租户创建一次服务
示例代码:
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="tenant" value="foo.TenantScope"/>
</map>
</property>
</bean>
<bean id="service" class="foo.Service" factory-bean="tenantServiceLocator" factory-method="createInstance" scope="tenant"/>
<bean id="fooService" class="FooService">
<bean id="barService" class="BarService">
<bean id="tenantServiceLocator" class="foo.TenantServiceLocator">
<property name="services">
<map>
<entry key="foo" value-ref="fooService"/>
<entry key="bar" value-ref="barService"/>
</map>
</property>
</bean>
TenantServiceLocator应该知道用户tenantId
public class TenantServiceLocator {
private Map<String, Service> services;
public String getTenantId() {
return "foo"; // get it from user in session
}
public Map<String, Service> getServices() {
return services;
}
public void setServices(Map<String, Service> services) {
this.services = services;
}
public Service createInstance(){
return services.get(tenantId);
}
}
public class FooController{
@Autowired
private Service service;
}
示例TenantScope实施
public class TenantScope implements Scope {
private static Map<String, Map<String, Object>> scopeMap = new HashMap<String, Map<String, Object>>();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = getTenantScope(getTenantId());
Object object = scope.get(name);
if(object == null){
object = objectFactory.getObject();
scope.put(name, object);
}
return object;
}
private Map<String, Object> getTenantScope(String tenantId) {
if (!scopeMap.containsKey(tenantId)) {
scopeMap.put(tenantId, new HashMap<String, Object>());
}
return scopeMap.get(tenantId);
}
private String getTenantId() {
return "foo"; // load you tenantId
}
@Override
public Object remove(String name) {
Map<String, Object> scope = getTenantScope(getTenantId());
return scope.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}
答案 5 :(得分:0)
虽然可以在运行时(HotswappableTargetSource
)交换在spring上下文中实例化的bean,但它并不适用于像你这样的用例。
请记住,您的应用程序有一个Spring Context,所有线程都使用相同的实例(在大多数情况下),这意味着当您更换bean实现时,您可以为所有应用程序的用户有效地执行此操作。为了防止出现这种情况,您将遇到另一个问题中列出的使用线程局部件确保线程安全的问题。
虽然可以继续使用这种方法并实现完成工作的实现,但它肯定是解决此问题的一种非常人为的方式。
您应该退后一步,从系统范围更广泛的设计角度来看问题。无论您使用的是Spring还是其他框架,都要打破您的模式书并查看如何解决这些问题。上面的一些答案中描述的服务定位器,工厂bean等是朝着正确方向迈出的一步。
您的用例在多租户应用程序中非常常见。您需要根据tenantId与不断变化的事物缩小可能会发生变化的事情。
例如,如问题中所述,每个租户可能会有不同的佣金金额甚至不同的佣金计算算法。一个简单的解决方案是实现一个接受CommissionCalculationService
的{{1}},以及根据要计算佣金的任何其他域对象,我想这会是{{1}或者tenantId
,无论你的应用程序有什么意义。
您现在需要Order
或Sale
,其中包含CommissionServiceFactory
的租户特定实施。在Spring上下文加载时实例化ServiceLocator
,并在应用程序启动时注入实现类。
当您想要计算租户的佣金时,您基本上从用户的登录信息中获取CommissionCalculationService
,将租户ID传递给您的服务定位器,基于传递的Service Locator
,服务定位器返回适当的服务实现实例。在您的调用类中,使用此实例计算租户的佣金。
要考虑的另一种模式是tenantId
,甚至是tenantId
。
最重要的是,即使您希望干净地实现租户特定逻辑,也不要改变在上下文中加载的bean。在您的上下文中具有可以处理所有租户特定逻辑的类。依靠设计模式,根据租户ID使用上下文中的正确bean。
如果答案有点冗长,我很抱歉,我觉得需要解释为什么我认为在加载的Spring Context中更新bean不是合适的解决方案。