如何根据租户在运行时选择弹簧配置?

时间:2013-11-26 03:15:44

标签: spring grails multi-tenant

我希望能够根据用户在运行时所属的租户选择特定的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框架解决。

6 个答案:

答案 0 :(得分:2)

几年前,我们需要这样的事情,但仅适用于DataSourceViewResolvers。我们使用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,则可以添加prefixsuffix

<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,无论你的应用程序有什么意义。

您现在需要OrderSale,其中包含CommissionServiceFactory的租户特定实施。在Spring上下文加载时实例化ServiceLocator,并在应用程序启动时注入实现类。

当您想要计算租户的佣金时,您基本上从用户的登录信息中获取CommissionCalculationService,将租户ID传递给您的服务定位器,基于传递的Service Locator,服务定位器返回适当的服务实现实例。在您的调用类中,使用此实例计算租户的佣金。

要考虑的另一种模式是tenantId,甚至是tenantId

最重要的是,即使您希望干净地实现租户特定逻辑,也不要改变在上下文中加载的bean。在您的上下文中具有可以处理所有租户特定逻辑的类。依靠设计模式,根据租户ID使用上下文中的正确bean。

如果答案有点冗长,我很抱歉,我觉得需要解释为什么我认为在加载的Spring Context中更新bean不是合适的解决方案。