背景需求
在业务功能开发过程中,我们可能会遇到如下一些需求
1、统计每条sql的执行时间
后台打印每条查询sql的执行时间,用于后续分析。
2、对机构数据权限做通用的处理
对每条执行的sql进行统一的机构数据权限校验,开发人员不用每个都单独编写机构校验逻辑。
思路
下面仅提供实现思路和具体的相关demo,后续的实际实现留待以后遇到相似的业务需求先!
我们可以用Mybatis的拦截器来处理。
1、统计每条sql的执行时间
我们新建一个Mybatis的拦截器,在执行sql的前后打印时间日志即可。
拦截器代码如下
package com.suibibk.interceptor;import org.apache.ibatis.cache.CacheKey;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.BoundSql;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.plugin.*;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.RowBounds;import java.util.Properties;@Intercepts({@Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),@Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class, method = "queryCursor",args = {MappedStatement.class, Object.class, RowBounds.class}),@Signature(type = Executor.class, method = "update",args = {MappedStatement.class, Object.class})})public class SqlExecuteTimeInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object[] args = invocation.getArgs();MappedStatement mappedStatement = (MappedStatement) args[0];long startTime = System.currentTimeMillis();Object result = invocation.proceed();long endTime = System.currentTimeMillis();long timeElapsed = endTime - startTime;String sqlId = mappedStatement.getId();System.out.println("执行SQL【" + sqlId + "】耗时:" + timeElapsed + "ms");return result;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {// 可以配置拦截器的属性}}
然后注册拦截器
package com.suibibk.config;import com.suibibk.interceptor.AuthorityInterceptor;import com.suibibk.interceptor.SqlExecuteTimeInterceptor;import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class MyBatisConfig {@Beanpublic ConfigurationCustomizer configurationCustomizer() {return configuration -> {//configuration.addInterceptor(new AuthorityInterceptor());configuration.addInterceptor(new SqlExecuteTimeInterceptor());};}}
上面的 AuthorityInterceptor拦截器是第二个问题的拦截器,这里不用理会。
搞定。
2、对机构数据权限做通用的处理
这里的思路是,我们新增一个注解,在需要校验机构数据权限的Mapper方法里面加上该注解,然后拦截器里面判断到方法是否有这个注解,有则修改sql,在后面拼接机构数据权限校验的代码,这里先举个例子,后续其实应该是根据当前登录用户所属机构来拼接。具体怎么获取用户的信息,我们可以借助使用 ThreadLocal 存储当前用户信息,比如如下代码例子
public class UserContextHolder {private static final ThreadLocal<Long> userContextHolder = new ThreadLocal<>();public static void setUserId(Long userId) {userContextHolder.set(userId);}public static Long getUserId() {return userContextHolder.get();}public static void clear() {userContextHolder.remove();}}
我们可以在controller或者springboot的拦截器过滤器将用户的信息token放入当前线程中,后续Mybatis的拦截器就可以获取了。这里不展开。
首先定义注解
package com.suibibk.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/*** Mybatis权限检查注解,拦截StatementHandler的prepare方法 拦截器见AuthorityInterceptor* @author bbq*/@Target({ElementType.METHOD,ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface ACheck {String value() default "";}
然后使用注解
public interface TestMapper {@ACheck("admin")public List<LevelInfo> getLevelInfos();}
然后拦截器
package com.suibibk.interceptor;import com.suibibk.annotation.ACheck;import org.apache.commons.lang.StringUtils;import org.apache.ibatis.cache.CacheKey;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.BoundSql;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.mapping.SqlSource;import org.apache.ibatis.plugin.*;import org.apache.ibatis.reflection.MetaObject;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.RowBounds;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.lang.reflect.Modifier;import java.util.Properties;@Intercepts({@Signature(type = Executor.class,//四种类型接口中的某一个接口,如Executor.class。可拦截方法有 Executor、ParameterHandler 、ResultSetHandler 、StatementHandler,method = "query",////对应接口中的某一个方法名,比如Executor的query方法。//对应接口中的某一个方法的参数,比如Executor中query方法因为重载原因,有多个,args就是指明参数类型,从而确定是具体哪一个方法。args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),@Signature(type = Executor.class, method = "update",args = {MappedStatement.class, Object.class})})public class AuthorityInterceptor implements Interceptor {private static final long serialVersionUID = 1L;@Overridepublic 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 name = StringUtils.substringAfterLast(mid, ".");Class<?> classType = Class.forName(mid.substring(0, mid.lastIndexOf(".")));ACheck aCheck = null;//拦截类if (classType.isAnnotationPresent(ACheck.class) && classType.getAnnotation(ACheck.class) != null) {aCheck = classType.getAnnotation(ACheck.class);originalSql = handleSQL(originalSql, aCheck);} else {//拦截方法for (Method method : classType.getMethods()) {if (!name.equals(method.getName())) {continue;} else {if (method.isAnnotationPresent(ACheck.class) && method.getAnnotation(ACheck.class) != null) {aCheck = method.getAnnotation(ACheck.class);originalSql = handleSQL(originalSql, aCheck);}break;}}}BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), originalSql, boundSql.getParameterMappings(), boundSql.getParameterObject());MappedStatement newMs = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));invocation.getArgs()[0] = newMs;invocation.getArgs()[5] = newBoundSql;return invocation.proceed();}public String handleSQL(String originalSql, ACheck aCheck){String atv = aCheck.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");}**/originalSql = "select * from levelinfo where id=1104145748397654016 order by sort";}catch (Exception e){e.printStackTrace();}}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;}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties 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,用了这段逻辑
invocation.getArgs()[0] = newMs;invocation.getArgs()[5] = newBoundSql;
这里不一定是5,还要根据参数的数目,具体可以去看Mybatis的分页插件pageHelper怎么做的,才能做到兼容各种情况。
然后就可以了。
后续实际使用再来更新!
