目录
1、Mybatis 拦截器介绍
1.1 Mybatis 执行流程
- 首先读取配置文件,然后加载映射文件,由SqlSessionFactory工厂对象去创建核心对象SqlSession,SqlSession对象会通过Executor执行器对象执行sql。然后Executor执行器对象会调用StatementHandler对象去真正的访问数据库执行sql语句。
- 在执行sql语句前MapperStatement会先对映射信息进行封装,然后StatementHandler调用ParameterHandler去设置编译参数【#{},${}】,编译在StatementHandler中进行。然后StatementHandler调用JBDC原生API进行处理,获取执行结果,这个执行结果交给ResultSetHandler 来进行结果集封装,然后将结果返回给StatementHandler。
- 注意: 这里MapperStatement是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。TypeHandler进行数据库类型和JavaBean类型映射处理。
1.2 Mybatis中可以被拦截的类型
- Executor:拦截执行器的方法。
- ParameterHandler:拦截参数的处理。
- ResultHandler:拦截结果集的处理。
- StatementHandler:拦截Sql语法构建的处理。
1.3 使用规则
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
/**
* 定义拦截点
* 只有符合拦截点的条件才会进入到拦截器
*/
Signature[] value();
}
Signature来指定咱们需要拦截那个类对象的哪个方法
- type:上述四种类型中的一种;
- method:对应接口中的哪类方法(因为可能存在重载方法);
- args:对应哪一个方法的入参;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
/**
* 定义拦截的类 Executor、ParameterHandler、StatementHandler、ResultSetHandler当中的一个
*/
Class<?> type();
/**
* 在定义拦截类的基础之上,在定义拦截的方法
*/
String method();
/**
* 在定义拦截方法的基础之上在定义拦截的方法对应的参数,
* JAVA里面方法可能重载,故注意参数的类型和顺序
*/
Class<?>[] args();
}
1.4 拦截器重写的方法
public interface Interceptor {
//起拦截作用,在此定义一些功能
Object intercept(Invocation var1) throws Throwable;
//这个方法的作用是就是让mybatis判断,是否要进行拦截,然后做出决定是否生成一个代理
Object plugin(Object var1);
//拦截器需要一些变量对象,而且这个对象是支持可配置的。
void setProperties(Properties var1);
}
2、实战部分:拦截实现
自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DepartAuth {
/**
* 添加查询条件的字段名
* @return
*/
String field();
EnumDepartAuthType authType() default EnumDepartAuthType.DEPART_ID;
}
在所在接口上添加注解
@DepartAuth(field = "xxx", authType = )
切面(AuthAspect)
@Slf4j
@Aspect
@Component
public class DepartAuthAspect {
@Autowired
private DepartAuthHandler departAuthHandler;
@Pointcut("@annotation(org.jeecg.common.auth.depart.annotation.DepartAuth)")
public void departAuthPoint() {
}
@Before("departAuthPoint()")
public void before(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Annotation[] annotations = methodSignature.getMethod().getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof DepartAuth) {
String field = ((DepartAuth) annotation).field();
departAuthHandler.beforeHandler((DepartAuth) annotation);
}
}
}
@After("departAuthPoint()")
public void after(JoinPoint pjp) {
departAuthHandler.afterHandler();
}
}
DepartAuthHandler
将信息存储到 TreadLocalhost 中,方便后续修改sql的时候可以读取到需要修改的字段
@Slf4j
@Component
public class DepartAuthHandler {
public static ThreadLocal<DepartAuth> DEPART_AUTH_CACHE = new ThreadLocal<>();
public static ThreadLocal<Integer> DEPART_AUTH_COUNT = new ThreadLocal<>();
public void beforeHandler(DepartAuth departAuth) {
String field = departAuth.field();
if(StringUtils.isNotBlank(field)) {
DEPART_AUTH_CACHE.set(departAuth);
DEPART_AUTH_COUNT.remove();
}
PriorityQueue queue = new PriorityQueue<>();
queue.peek();
}
public void afterHandler() {
DEPART_AUTH_CACHE.remove();
DEPART_AUTH_COUNT.remove();
}
}
拦截器部分
- 获取StatementHandler:通过 invocation.getTarget() 获取当前被拦截的 StatementHandler 对象。由于 MyBatis 使用了代理模式,因此这里得到的是一个代理对象。接着,通过反射获取其实际的代理对象 ,即最终执行SQL的 StatementHandler 实例。
- 获取MappedStatement:通过反射从 StatementHandler 中获取 mappedStatement 对象。这个对象包含了关于即将执行的SQL语句的所有信息,包括SQL类型、参数类型等。
- 判断SQL类型:通过mappedStatement.getSqlCommandType()获取SQL命令类型。如果类型是SELECT,说明当前是一个查询操作,需要进行权限检查和处理。
- 获取并解析原始SQL:通过delegate.getBoundSql()获取BoundSql对象,它包含了实际执行的SQL语句和相关的参数信息。然后使用CCJSqlParserUtil.parse()解析这个SQL语句,得到一个抽象语法树(AST)。
- 修改SQL:从AST中提取出PlainSelect对象(即SELECT语句的主体)。然后调用自定义的buildWhereClause方法,根据departAuth中的权限信息构建一个权限检查条件,并将其注入到原始的SELECT语句中。这通常是通过在WHERE子句后追加额外的条件来实现的。
- 更新BoundSql对象:将修改后的SQL语句重新设置回BoundSql对象中,以便MyBatis在执行时能够使用修改后的SQL。
- 继续执行后续流程:在完成SQL修改后,调用invocation.proceed()继续执行MyBatis的后续处理流程,包括实际的SQL执行、结果集处理等。
@Data
@Slf4j
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class DepartAuthMapperInterceptor implements Interceptor {
private Properties properties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
DepartAuth departAuth = DepartAuthHandler.DEPART_AUTH_CACHE.get();
Integer count = DepartAuthHandler.DEPART_AUTH_COUNT.get();
if(departAuth != null && count == null) {
// 说明当前线程已经执行了过滤条件,避免递归调用
DepartAuthHandler.DEPART_AUTH_COUNT.set(1);
RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
//获取StatementHandler构造器
StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
// 通过反射获取delegate父类BaseStatementHandler的mappedStatement属性
MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
SqlCommandType commandType = mappedStatement.getSqlCommandType();
// 处理select对象
if (SqlCommandType.SELECT.equals(commandType)) {
// 获取原始sql
BoundSql boundSql = delegate.getBoundSql();
Statement statement = CCJSqlParserUtil.parse(boundSql.getSql());
PlainSelect selectBody = (PlainSelect) ((Select) statement).getSelectBody();
log.info("原始 sql:{}", boundSql.getSql());
// 拼接新条件
buildWhereClause(selectBody, getSql(departAuth));
ReflectUtil.setFieldValue(boundSql, "sql", statement.toString());
}
return invocation.proceed();
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
/**
* 添加查询条件
* @param select
* @param dataFilter
* @throws JSQLParserException
*/
private void buildWhereClause(PlainSelect select, String dataFilter) throws JSQLParserException {
if(StringUtils.isBlank(dataFilter)) {
return;
}
if (select.getWhere() == null) {
select.setWhere(CCJSqlParserUtil.parseCondExpression(dataFilter));
} else {
AndExpression and = new AndExpression(
CCJSqlParserUtil.parseCondExpression(dataFilter), select.getWhere());
select.setWhere(and);
}
}
private String getSql(DepartAuth departAuth) {
//结合自己的业务,拼接相对应的sql语句
}
}