如何基于租户在运行时选择spring配置?
我希望能够根据用户在运行时所属的租户选择特定的Spring(或Grails)上下文配置。假设我使用Spring安全性,并在登录期间检索tenantId。 想象一下,现在我有两个租户,他们支付不同的佣金。如何在没有太多管道的情况下将特定服务注入控制器?这里有两种不同的背景。所以,我应该根据租户注入不同的ExchangeService如何基于租户在运行时选择spring配置?,spring,grails,multi-tenant,Spring,Grails,Multi Tenant,我希望能够根据用户在运行时所属的租户选择特定的Spring(或Grails)上下文配置。假设我使用Spring安全性,并在登录期间检索tenantId。 想象一下,现在我有两个租户,他们支付不同的佣金。如何在没有太多管道的情况下将特定服务注入控制器?这里有两种不同的背景。所以,我应该根据租户注入不同的ExchangeService @Configuration public class FooTenant{ @Bean public ExchangeService bar() { retur
@Configuration
public class FooTenant{
@Bean
public ExchangeService bar() {
return new ZeroCommisionExchangeService ();
}
}
@Configuration
public class BarTenant{
@Bean
public ExchangeService bar() {
return new StandardCommisionExchangeService ();
}
}
编辑:
我知道我可以获得对Spring上下文的引用并“手动”请求服务,但我正在寻找一种更通用的解决方案,在这种解决方案中,这个问题可以通过IoC框架来解决。假设您已经定义了不同的服务,您可以从上下文中获取它们的bean并使用它。在我的示例中,所有服务都实现了
serviceMethod
,并根据一些标准选择合适的服务。我唯一不确定的是,多租户
会如何影响这一点
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()
}
}
第一次服务
class FirstService {
def serviceMethod() {
return "first"
}
}
第二服务:
class SecondService {
def serviceMethod() {
return "second"
}
}
在回答中转换我的评论,一个可能的解决方案是创建一个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[])
返回正确的实例,并且由于运行时参数的原因,还将使工厂成为原型范围
要测试它的控制器:
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)
我使用以下代码:
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;
}
}
在坦能配置中:
<bean class="my.package.service.ServiceImplementationA" name="$&&#beanName"/>
您还将实现ApplicationContextProvider
,我将不与您分享我的。实施起来并不难。例如,您的实现可以只在ThreadLocal
中存储上下文。您可以创建自己的ServletContextListener
,它为每个查询获取当前的tennant,并将其存储到您的ApplicationContextProvider
实现中。几年前,我们需要这样的东西,但只适用于数据源
和视图解析程序
。我们使用spring'TargetSource
解决方案开发了一个解决方案。(最初我们使用的是热插拔TargetSource
,但这不适合我们的用例
我们开发的代码可在多租户目录中找到
它是完全可配置和灵活的
基本上,您要做的是配置一个ContextSwappableTargetSource
,并告诉它需要返回什么类型的接口/类
<bean id="yourTentantBasedServiceId" class="biz.deinum.multitenant.aop.target.ContextSwappableTargetSource">
<constructor-arg value="ExchangeService" />
</bean>
现在对于foo,它将查找名为fooExchangeService
的bean,对于barbarExchangeService
tenantId存储在一个ThreadLocal
中,它被包装在ContextHolder
中。您需要找到一种方法来填充和清除这个线程本地(通常servlet过滤器会执行此操作)
在您的代码中,您现在只需使用接口ExchangeService
,并且在运行时基于tenantId
将查找正确的实现
另请参见新租户范围和servicelocator可以提供帮助
租户范围将保证为租户创建一次以上的服务
示例代码:
<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;
}
公共类租户服务定位器{
私人地图服务;
公共字符串getTenantId(){
返回“foo”;//在会话中从用户处获取
}
公共地图服务(){
返回服务;
}
公共void设置服务(地图服务){
这是服务=服务;
}
公共服务createInstance(){
返回服务。获取(租户ID);
}
}
公共类FooController{
@自动连线
私人服务;
}
一个示例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;
}
}
公共类租户作用域实现作用域{
私有静态映射scopeMap=newhashmap();
@凌驾
公共对象获取(字符串名称,ObjectFactory ObjectFactory){
Map scope=gettenatscope(getTenantId());
Object=scope.get(name);
if(object==null){
object=objectFactory.getObject();
范围.放置(名称、对象);
}
返回对象;
}
私有映射getTenantScope(字符串tenantId){
如果(!scopeMap.containsKey(tenantId)){
put(tenantId,newhashmap());
}
返回scopeMap.get(tenantId);
}
私有字符串getTenantId(){
返回“foo”;//加载您的租户
}
@凌驾
公共对象删除(字符串名称){
Map scope=gettenatscope(getTenantId());
返回范围。删除(名称);
}
@凌驾
public void registerDestructionCallback(字符串名称,可运行回调){
}
@凌驾
公共对象ResolveContextaObject(字符串键){
返回null;
}
@凌驾
公共字符串getConversationId(){
返回null;
}
}
虽然可以在运行时交换spring上下文中实例化的bean(HotswappableTargetSource
),但它并不适用于像您这样的用例
请记住,应用程序只有一个Spring上下文,所有线程都使用相同的实例(在大多数情况下),这意味着当您交换bean实现时,您实际上是在为应用程序的所有用户执行此操作。为了防止这种情况,您遇到了确保线程安全、使用线程局部变量的问题,如另一个答案中所列
虽然这是可能的
<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>
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;
}
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;
}
}