背景需求
在业务功能开发过程中,我们可能会遇到如下一些需求
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 {
@Override
public 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;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public 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;
@Configuration
public class MyBatisConfig {
@Bean
public 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;
@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 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;
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public 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怎么做的,才能做到兼容各种情况。
然后就可以了。
后续实际使用再来更新!