以前公司的一个项目需要实现多租户,这里简单记录一下实现的流程,希望有需要的朋友们能用的上。
原理:基于mycat的注解动态切换schema,在数据库设计的时候分为公共库、租户库,公共库放公共表,比如用户表、租户配置表。租户库放租户自己的数据表。
1.接口请求:用户登陆后,每个接口请求均会有tenant_id参数
2.controller拦截器:拦截每一个请求,根据tenant_id参数获取schema(库名称,冲公共库、或者redis中获取),并存入ThreadLocal中,以便后续使用
3.sql拦截器:拦截每个sql执行语句,从ThreadLocal中获取schema,在sql语句中拼接上mycat注解,然后再执行,这样sql在执行的时候就会动态切换schema
controller拦截器 代码:
public class MyCatFilter implements Filter {
private static final ThreadLocal SCHEMA_LOCAL = new ThreadLocal<>();
private static final ThreadLocal TENANT_ID_LOCAL = new ThreadLocal<>();
public static String getSchema() {
String schema = SCHEMA_LOCAL.get();
return StringUtils.isEmpty(schema) ? "":schema;
}
public static String getTenantId(){
String tenantId=TENANT_ID_LOCAL.get();
return StringUtils.isEmpty(tenantId) ? "":tenantId;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
HttpServletRequest req = (HttpServletRequest) request;
String tenantId = req.getParameter("tenant_id");
//根据租户id查询租户对应的数据库
if (!StringUtils.isEmpty(tenantId)) {
// 静态的本地线程变量来存储租户信息
TENANT_ID_LOCAL.set(tenantId);
// TODO 根据租户id查询schema逻辑数据库 从redis或者数据库中查询
String schema = null;
// 静态的本地线程变量来存储数据库信息
SCHEMA_LOCAL.set(schema);
// chain
chain.doFilter(request, response);
} else {
chain.doFilter(request, response);
}
} finally {
SCHEMA_LOCAL.remove();
}
}
@Override
public void destroy() {
}
}
sql拦截器 代码:
@Intercepts({
@Signature(method = "query", type = Executor.class, args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }),
@Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) })
public class MyInterceptor implements Interceptor {
private final static String[] scrm_public = { "public_tenant"};
@Override
public Object intercept(Invocation invocation) throws Throwable {
String schema = MyCatFilter.getSchema();
if (invocation.getTarget() instanceof RoutingStatementHandler) {
RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation
.getTarget();
StatementHandler delegate = (StatementHandler) ReflectHelper
.getFieldValue(statementHandler, "delegate");
BoundSql boundSql = delegate.getBoundSql();
Object obj = boundSql.getParameterObject();
// 通过反射获取delegate父类BaseStatementHandler的mappedStatement属性
MappedStatement mappedStatement = (MappedStatement) ReflectHelper
.getFieldValue(delegate, "mappedStatement");
// 获取当前要执行的Sql语句,也就是我们直接在Mapper映射语句中写的Sql语句
String sql = boundSql.getSql();
String NowSql = "/*!mycat:schema = " + schema + " */ " + sql;
System.out.println(NowSql);
// 利用反射设置当前BoundSql对应的sql属性为我们修改完的Sql语句
ReflectHelper.setFieldValue(boundSql, "sql", NowSql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
ReflectHelper.class
public class ReflectHelper {
public static Object getFieldValue(Object obj , String fieldName ){
if(obj == null){
return null ;
}
Field targetField = getTargetField(obj.getClass(), fieldName);
try {
return FieldUtils.readField(targetField, obj, true ) ;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null ;
}
public static Field getTargetField(Class> targetClass, String fieldName) {
Field field = null;
try {
if (targetClass == null) {
return field;
}
if (Object.class.equals(targetClass)) {
return field;
}
field = FieldUtils.getDeclaredField(targetClass, fieldName, true);
if (field == null) {
field = getTargetField(targetClass.getSuperclass(), fieldName);
}
} catch (Exception e) {
}
return field;
}
public static void setFieldValue(Object obj , String fieldName , Object value ){
if(null == obj){return;}
Field targetField = getTargetField(obj.getClass(), fieldName);
try {
FieldUtils.writeField(targetField, obj, value) ;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
demo传送门