当我们在做项目时,特别是ToB的项目,会发生一种场景,即大的业务流程是一样的,但是在某个节点,不同的租户有不同的业务需求。
这就需要我们针对不同的租户将代码路由到不同的实现上面,从而执行正确的业务逻辑。
如下图所示,我们现在有个业务逻辑,需要依次执行A、B、C、D四段代码逻辑。
但是B和D节点,不同的租户有不同的业务逻辑,需要单独去实现,这时就需要我们能通过租户标识动态的路由到自己的实现上面。
我们上节图中的B节点为例。
首先,我们得创建一个X租户和所有租户的父类,比如我们这里就叫AbstractBService
。
然后,我们创建AbstractBService
的两个子实现,分别为BServiceOfB
和BServiceOfAll
。
最后,我们用一个注解来区分不同租户的实现,我这里是用的自定义的一个注解@TenantSelector
。
我们主要是在程序启动时,扫描该注解,然后将拥有同一个父类的划分为一组。
通过动态代理创建一个bean,我们项目使用的就是这个动态代理创建的bean,这个bean会拥有所有实现类的引用。
当执行具体的方法时,该动态代理就会通过上下文的租户信息去获取对应的实现类的bean,然后执行该实现类的bean的方法。
这样,我们代码运行的时候,就可以根据不同的租户路由到不同的实现上面。具体实现逻辑,看下面的代码实现。
首先,我们需要实现一个spring-boot-starter
,这样,我们的项目就可以导入该starter,直接使用租户路由的注解,从而实现代码路由。
1.引入我们需要的jar包
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.eddiegroupId>
<artifactId>tenant-spring-boot-starterartifactId>
<version>1.0.0version>
<properties>
<spring-boot.version>2.1.15.RELEASEspring-boot.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-autoconfigureartifactId>
dependency>
<dependency>
<groupId>cglibgroupId>
<artifactId>cglibartifactId>
<version>3.2.2version>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
project>
2.路由时使用的注解
这里,我们会给一个注解的默认值all
,表示当某租户没有自己的实现时,就执行通用的逻辑。
import java.lang.annotation.*;
/**
* @author Eddie
*/
@Documented
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TenantSelector {
String[] value() default "all";
}
3.存储租户信息的上下文
当我们解析完用户的登录信息后,就可以将该用户的租户信息放到该线程上下文中。
public class TenantContext {
private static final ThreadLocal<String> TENANT_CONTEXT = new InheritableThreadLocal<String>() {
@Override
protected String initialValue() {
return "all";
}
};
public static String get() {
return TENANT_CONTEXT.get();
}
public static void set(String mart) {
TENANT_CONTEXT.set(mart);
}
public static void remove() {
TENANT_CONTEXT.remove();
}
}
这里我使用的是InheritableThreadLocal
,可以将租户信息继承给子线程,方便我们在创建子线程并行执行某些代码时,子线程也能有租户信息,执行租户特定的实现。
4.动态代理的拦截器
这里,我们需要用到动态代理来创建一个bean,放到spring ioc容器中。
通过下面的代码,我们就可以发现,当程序进行路由时,就是通过线程中的租户信息来选择bean进行真正地方法调用的。
import com.eddie.context.TenantContext;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author Eddie
*/
public class TenantSelectorMethodInterceptor implements MethodInterceptor {
private final Map<String, Object> beanMap;
public TenantSelectorMethodInterceptor() {
beanMap = new ConcurrentHashMap<>();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
String mart = TenantContext.get();
Object object = beanMap.getOrDefault(mart, beanMap.get("all"));
return method.invoke(object, args);
}
public void addBean(String[] marts, Object bean) {
for (String mart : marts) {
beanMap.put(mart, bean);
}
}
}
5.FactoryBean的代码
我们需要通过 FactoryBean的方式,将动态代理的bean 放到spring ioc 容器中进行管理。
import com.eddie.annotation.TenantSelector;
import com.eddie.cglib.TenantSelectorMethodInterceptor;
import net.sf.cglib.proxy.Enhancer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* @author Eddie
*/
public class TenantSelectorFactoryBean<T> implements FactoryBean<T>, ApplicationContextAware {
private final Class<T> superclass;
private final TenantSelectorMethodInterceptor methodInterceptor;
private ApplicationContext ac;
List<Class<? extends T>> subclasses;
public TenantSelectorFactoryBean(Class<T> superclass, List<Class<? extends T>> subclasses) {
this.superclass = superclass;
this.methodInterceptor = new TenantSelectorMethodInterceptor();
this.subclasses = subclasses;
}
@Override
public T getObject() throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(superclass);
enhancer.setCallback(methodInterceptor);
return (T) enhancer.create();
}
@Override
public Class<?> getObjectType() {
return superclass;
}
@PostConstruct
public void buildMatBean() {
for (Class<? extends T> subclass : subclasses) {
TenantSelector martSelector = subclass.getAnnotation(TenantSelector.class);
methodInterceptor.addBean(martSelector.value(), ac.getBean(subclass));
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.ac = applicationContext;
}
}
6.最后
通过mavn install
的方式,将该项目推送到本地的maven仓库中,就可以使用了。
首先,我们新创建一个项目,并在pom.xml
文件中引入该依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.eddiegroupId>
<artifactId>tenant-spring-boot-starterartifactId>
<version>1.0.0version>
dependency>
dependencies>
然后,创建一个抽象类和其不同租户的实现类。
抽象父类:
// 父类
public abstract class AbstractBService {
public abstract String get();
}
所有租户的通用实现逻辑:
import com.eddie.annotation.TenantSelector;
import com.eddie.dao.BDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@TenantSelector
@Service
public class BServiceOfAll extends AbstractBService {
@Resource
private BDao bDao;
@Override
public String get() {
return bDao.get();
}
}
X组合的实现类:
@TenantSelector("X")
@Service
public class BServiceOfX extends AbstractBService {
@Override
public String get() {
return "X";
}
}
Y租户和Z租户的实现类:
@TenantSelector({"Y", "Z"})
@Service
public class BServiceOfYAndZ extends AbstractBService {
@Override
public String get() {
return "Y and Z";
}
}
进行测试
import com.eddie.MartSelectorApplication;
import com.eddie.context.TenantContext;
import com.eddie.service.AbstractBService;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MartSelectorApplication.class)
public class TenantSelectorTest {
@Resource
private AbstractBService abstractBService;
@After
public void removeMart() {
TenantContext.remove();
}
@Test
public void test_All() {
TenantContext.set("all");
System.out.println(abstractBService.get());
}
@Test
public void test_X() {
TenantContext.set("X");
System.out.println(abstractBService.get());
}
@Test
public void test_Y() {
TenantContext.set("Y");
System.out.println(abstractBService.get());
}
@Test
public void test_Z() {
TenantContext.set("Z");
System.out.println(abstractBService.get());
}
@Test
public void test_error() {
TenantContext.set("error");
System.out.println(abstractBService.get());
}
}
tenant-spring-boot-starter