需求:给原来的sql都加上一个条件过滤,实现多租户数据隔离。
一个是sql语句散布在xml里,dao注解里,量非常大,再一个是租户字段定义在实体基类中,接口参数是对象只需修改sql即可,倒是不麻烦,机械性复制粘贴,如果是非对象例如get(id),那就有的你改了,所以第一时间排除掉一个个修改sql。用mybatis自定义拦截器来对sql进行后期动态修改,原理和分页插件类似。
建一个mybatis拦截器处理sql 【mybatis自定义拦截器实现统一过滤动态修改sql】可拦截方法有 Executor、ParameterHandler 、ResultSetHandler 、StatementHandler,
由于项目分页插件是在Executor方法拦截,所以此例也是拦截Executor,它和StatementHandler获取sql的方式是不同的,在此不讨论。args参数可进入Executor接口里一一对应。Executor只能处理query、update,如果要拦截其它sql,得再写一个拦截StatementHandler的prepare方法。
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class TenantInterceptor extends BaseInterceptor {private static final long serialVersionUID = 1L;
@Override
public Object intercept(Invocation invocation) throws Throwable {final MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
if (StringUtils.isBlank(boundSql.getSql())) {
return null;
}
String originalSql = boundSql.getSql().trim();
String mid = mappedStatement.getId();
String nname = StringUtils.substringAfterLast(mid, ".");
Class> classType = Class.forName(mid.substring(0, mid.lastIndexOf(".")));
addTenantId addTenantId = null;
//拦截类
if (classType.isAnnotationPresent(addTenantId.class) && classType.getAnnotation(addTenantId.class) != null) {
addTenantId = classType.getAnnotation(addTenantId.class);
originalSql = handleSQL(originalSql, addTenantId);
} else {
//拦截方法
for (Method method : classType.getMethods()) {
if (!nname.equals(method.getName())) {
continue;
} else {
if (method.isAnnotationPresent(addTenantId.class) && method.getAnnotation(addTenantId.class) != null) {
addTenantId = method.getAnnotation(addTenantId.class);
originalSql = handleSQL(originalSql, addTenantId);
}
break;
}
}
}BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), originalSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
if (Reflections.getFieldValue(boundSql, "metaParameters") != null) {
MetaObject mo = (MetaObject) Reflections.getFieldValue(boundSql, "metaParameters");
Reflections.setFieldValue(newBoundSql, "metaParameters", mo);
}
MappedStatement newMs = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
invocation.getArgs()[0] = newMs;
return invocation.proceed();
}public String handleSQL(String originalSql, addTenantId addTenantId){
String atv = addTenantId.value();
if (StringUtils.isNotBlank(atv)){try{
/**
此处应为你的sql拼接,替换第一个where可以实现绝大多数sql,当然复杂sql除外,所以复杂sql还是需要例外处理
User user = null;
user = UserUtils.getUser();
String tid;
if(user != null && StringUtils.isNotBlank(tid = user.getTenantId())){
originalSql = replace(originalSql, "where", "where"+atv+"='"+tid+"' and");
originalSql = replace(originalSql, "WHERE", "WHERE"+atv+"='"+tid+"' and");
}
**/
}catch (Exception e){
log.debug(e.getMessage());
}
}
return originalSql;
}public static String replace(String string, String toReplace, String replacement) {
//int pos = string.lastIndexOf(toReplace);
int pos = string.indexOf(toReplace);
if (pos > -1) {
return string.substring(0, pos)
+ replacement
+ string.substring(pos + toReplace.length(), string.length());
} else {
return string;
}
}@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}@Override
public void setProperties(Properties properties) {
super.initProperties(properties);
}private MappedStatement copyFromMappedStatement(MappedStatement ms,
SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(),
ms.getId(), newSqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null) {
for (String keyProperty : ms.getKeyProperties()) {
builder.keyProperty(keyProperty);
}
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.cache(ms.getCache());
return builder.build();
}public static class BoundSqlSqlSource implements SqlSource {
BoundSql boundSql;
public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}
通过反射获取类或方法的注解,他的反射执行方法比直接执行方法慢个几十倍,并不是很明显,当然优化有很多种优化,不作讨论。
建一个自定义注解 需求虽然是大部分sql需要统一拦截,但是事实上绝对存在不要拦截的表又或者方法,这就需要自定义注解去区分开拦截。ElementType.METHOD,ElementType.TYPE 表示可打在类和方法上。
/**
* Mybatis租户过滤注解,拦截StatementHandler的prepare方法 拦截器见TenantInterceptor
* 无值表示不过滤 有值表示过滤的租户字段 如a.tenant_id
* @author bbq
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface addTenantId {
String value() default "";
}
为dao基类打注解拦截实现统一处理 为我的dao基类打上注解,注解值可以获取拼接在sql上。
public interface CrudDao extends BaseDao { @addTenantId("a.tenant_id")
public T get(String id);
@addTenantId("a.tenant_id")
public T get(T entity);
@addTenantId("a.tenant_id")
public List findList(T entity);
@addTenantId("a.tenant_id")
public List findAllList(T entity);
@addTenantId("a.tenant_id")
@Deprecated
public List findAllList();
public int insert(T entity);
@addTenantId("tenant_id")
public int update(T entity);
@addTenantId("tenant_id")
@Deprecated
public int delete(String id);
@addTenantId("tenant_id")
public int delete(T entity);
}
打空值注解跳过处理 没有租户字段的表,也就是不需要拦截sql的dao打上空注解在拦截器里跳过处理
@addTenantId()
@MyBatisDao
public interface XXXDao extends CrudDao {
AppVersion findLastVersion();
}
当然也可以给重写的方法打空注解跳过处理
@MyBatisDao
public interface XXXDao extends CrudDao {
@addTenantId()
AppVersion get(int id);
}
优先级 dao的类注解 > dao的方法注解 > 基类注解
在mybatis的xml配置里加上拦截器 自定义拦截器配在分页拦截器后面,优先执行。
>>>
最后 如果出了问题,只需要注释上面那句拦截器配置,一切就恢复如初。
推荐阅读
- 框架|Mybatis的一级缓存和二级缓存
- Mybatis日志工厂
- MyBatis的功能架构是怎样的
- Mybatis入门之CRUD
- mybatis之缓存机制
- Mybatis练习(1)
- Java|MyBatis(五)——MyBatis中的缓存机制
- mybatis之脚本解析器
- mybatis|记mybatis查询null字段导致的NPE
- Mybatis 动态查询、插入、修改操作