springboot实现多租户动态路由代码

01 背景

当我们在做项目时,特别是ToB的项目,会发生一种场景,即大的业务流程是一样的,但是在某个节点,不同的租户有不同的业务需求。

这就需要我们针对不同的租户将代码路由到不同的实现上面,从而执行正确的业务逻辑。

如下图所示,我们现在有个业务逻辑,需要依次执行A、B、C、D四段代码逻辑。
但是B和D节点,不同的租户有不同的业务逻辑,需要单独去实现,这时就需要我们能通过租户标识动态的路由到自己的实现上面。
springboot实现多租户动态路由代码_第1张图片

02 思路

我们上节图中的B节点为例。

首先,我们得创建一个X租户和所有租户的父类,比如我们这里就叫AbstractBService

然后,我们创建AbstractBService的两个子实现,分别为BServiceOfBBServiceOfAll

最后,我们用一个注解来区分不同租户的实现,我这里是用的自定义的一个注解@TenantSelector

我们主要是在程序启动时,扫描该注解,然后将拥有同一个父类的划分为一组。
通过动态代理创建一个bean,我们项目使用的就是这个动态代理创建的bean,这个bean会拥有所有实现类的引用。
当执行具体的方法时,该动态代理就会通过上下文的租户信息去获取对应的实现类的bean,然后执行该实现类的bean的方法。

这样,我们代码运行的时候,就可以根据不同的租户路由到不同的实现上面。具体实现逻辑,看下面的代码实现

03 代码实现

首先,我们需要实现一个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仓库中,就可以使用了。

03 使用测试

首先,我们新创建一个项目,并在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());
    }
}

测试结果如下,满足我们的预期要求。
springboot实现多租户动态路由代码_第2张图片

05 码云地址

tenant-spring-boot-starter

你可能感兴趣的:(springboot,Java,spring,java,spring,boot,maven)