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>

最后

如果出了问题,只需要注释上面那句拦截器配置,一切就恢复如初。

原创文章 6 获赞 18 访问量 3166

猜你喜欢

转载自blog.csdn.net/qq_24054301/article/details/104066864
今日推荐