背景
在做saas项目时,我们既想统一接口提供给各端调用,又希望各端在通用功能基础上做差异化定制,并满足不同租户做个性化定制的诉求,代码层面我想好好设计一下,其中比较关键的一步就是如何动态的根据渠道和租户加载不同的实现,本文将围绕“统一接口”、“渠道定制”、“租户定制”方面进行实现。
概念抽象
上面我们抽象出来两个概念,渠道和租户。渠道比如pc页面、移动app1、移动app2,租户就是我们saas系统中不同的商家。由简入难,我们先提供一个基础功能,渠道和租户基于基础功能可定制可不定制,不定制就默认使用基础实现。
优先级
我们定义两个渠道pc和app1,pc和app1同级,pc并作为我们的基础实现;定义两个租户,001和002.
优先级:渠道+租户精确匹配 > 渠道+无租户 > pc+租户 > pc+无租户,无租户我们认为租户为"-1"。
如图,左分支是我们的默认实现和默认实现的租户定制,原则是自下而上,自右向左匹配。举个栗子,如果传入渠道为app1和租户001,查找的顺序是优先查找app1#001,找不到再找app1#-1,接着pc#001,pc#-1按照这样的顺序找到最终的实现。
注解
我们将租户和渠道抽象出注解用来打标
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TenantOrderTag {
String value() default "";
AccessChannel accessChannel() default AccessChannel.Pc;
String tenantId() default "-1";
}
匹配逻辑
逻辑部分基本就是上面定义的优先级的实现
@Slf4j
public class TenantBeanFactory {
/***
* 实现类必须指定{@link TenantOrderTag}注解<p/>
* 当前请求的'accessChannel'和'tenantId'必须存在<p/>
*
* bean加载优先级由高到低:<p/>
* 1.accessChannel + tenantId 精确匹配
* 2.accessChannel + '-1' ('-1'是指实现类父级别实现, 未被租户定制; 非'-1'指租户定制实现类)
* 3.AccessChannel.Pc + tenantId
* 4.AccessChannel.Pc + '-1'
*
* @param clz
* @param <T>
* @return
*/
public static <T> T tagBean(Class<T> clz){
abstractClassOnly(clz);
// 租户、渠道取值传值工具类
Integer accessChannel = Optional.ofNullable(OrderRequestThreadLocal.getChannel()).orElse(AccessChannel.Pc.k());
Long tenantId = TenantUtils.getCurrentTenantId();
log.info("渠道={},租户={}", accessChannel, tenantId);
T returnBean = get(accessChannel, tenantId, clz);
Assert.notNull(returnBean, String.format("类'%s'未匹配到bean", clz.getSimpleName()));
log.info("渠道租户定制类匹配到'{}'", returnBean.getClass().getSuperclass().getSimpleName());
return returnBean;
}
private static void abstractClassOnly(Class cls){
boolean isInterface = cls.isInterface();
boolean isAbstract = Modifier.isAbstract(cls.getModifiers());
if(!isInterface && !isAbstract){
throw new RuntimeException(String.format("类'%s'不是抽象类或接口", cls.getSimpleName()));
}
}
private static <T> T get(Integer accessChannel, Long tenantId, Class<T> clz){
// key为渠道+租户,value为bean
Map<String, T> beanMap = beanMap(clz);
// 优先级由高到低
String key1 = beanKey(accessChannel, String.valueOf(tenantId));
String key2 = beanKey(accessChannel, "-1");
String key3 = beanKey(AccessChannel.Pc.k(), String.valueOf(tenantId));
String key4 = beanKey(AccessChannel.Pc.k(), "-1");
if(beanMap.containsKey(key1)){
return beanMap.get(key1);
}
if(beanMap.containsKey(key2)){
return beanMap.get(key2);
}
if(beanMap.containsKey(key3)){
return beanMap.get(key3);
}
if(beanMap.containsKey(key4)){
return beanMap.get(key4);
}
return null;
}
private static <T> Map<String, T> beanMap(Class<T> clz){
Map<String, T> beans = ApplicationContextUtil.getBeansOfType(clz);
Map<String, T> beanMap = new HashMap<>();
for(Map.Entry<String, T> entry : beans.entrySet()){
T bean = entry.getValue();
// spring管理的bean是生成的代理对象,这里获取原始类拿到注解信息
Class srcCls = bean.getClass().getSuperclass();
TenantOrderTag tag = (TenantOrderTag) srcCls.getAnnotation(TenantOrderTag.class);
Integer _channel;
String _tenant;
if(null != tag){
_channel = tag.accessChannel().k();
_tenant = tag.tenantId();
}else{
throw new RuntimeException(String.format("类'%s'缺失'TenantOrderTag'注解", srcCls.getSimpleName()));
}
String key = beanKey(_channel, _tenant);
T replaced = beanMap.put(key, entry.getValue());
if(null != replaced){
Class replCls = replaced.getClass().getSuperclass();
String msg = "注解'TenantOrderTag'在类'%s'和'%s'之间发生冲突,请确认注解配置'accessChannel'和'tenantId'组合是否正确";
throw new RuntimeException(String.format(msg, replCls.getSimpleName(), srcCls.getSimpleName()));
}
}
return beanMap;
}
private static String beanKey(Integer accessChannel, String tenantId){
return new StringBuilder().append(accessChannel).append("#").append(tenantId).toString();
}
}
结语
大家可以基于业务自定义抽象类或接口,编写实现类,注意实现类必须使用注解打标,而且按渠道和租户定制时务必保证组合唯一。
我们经常看到的在业务包装类中使用
// 注入list
@Autowired
private List<ITestService> testServices; // 或者如下
// 注入map
@Autowired
private Map<String, ITestService> testServiceMap; // 又或者使用applicationContext
// 显示的获取map
Map<String, ITestService> beans = applicationContext.getBeansOfType(ITestService.class);
来加载ITestService接口的所有实现类,再根据实现类打标的类型枚举或者实现接口的getType获取到类型信息,为啥还要费力写这样一个加载bean的工具呢,如果加一个抽象类或接口,就要再写一遍上面注入和匹配bean的逻辑,实在是太烦了,TenantBeanFactory可以很好的解决这个问题。