需求:給原來的sql都加上一個條件過濾,實現多租戶數據隔離。
一個是sql語句散佈在xml裏,dao註解裏,量非常大,再一個是租戶字段定義在實體基類中,接口參數是對象只需修改sql即可,倒是不麻煩,機械性複製粘貼,如果是非對象例如get(id),那就有的你改了,所以第一時間排除掉一個個修改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
* @version 2020-01-19
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface addTenantId {
String value() default "";
}
爲dao基類打註解攔截實現統一處理
爲我的dao基類打上註解,註解值可以獲取拼接在sql上。
public interface CrudDao<T> 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<T> findList(T entity);
@addTenantId("a.tenant_id")
public List<T> findAllList(T entity);
@addTenantId("a.tenant_id")
@Deprecated
public List<T> 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> {
AppVersion findLastVersion();
}
當然也可以給重寫的方法打空註解跳過處理
@MyBatisDao
public interface XXXDao extends CrudDao<AppVersion> {
@addTenantId()
AppVersion get(int id);
}
優先級 dao的類註解 > dao的方法註解 > 基類註解
在mybatis的xml配置里加上攔截器
自定義攔截器配在分頁攔截器後面,優先執行。
<plugins>
<plugin interceptor="你的路徑.interceptor.PaginationInterceptor" />
<plugin interceptor="你的路徑.interceptor.TenantInterceptor" />
</plugins>
最後
如果出了問題,只需要註釋上面那句攔截器配置,一切就恢復如初。