mybatis自定義攔截器實現統一過濾動態修改sql

需求:給原來的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>

最後

如果出了問題,只需要註釋上面那句攔截器配置,一切就恢復如初。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章