1、为什么要自定义sql拦截器与pageHelper兼容?
场景:对已经写完的数据基础层进行字段扩充,需要重新编写与mysql交互的代码,但是数据基础层sql语句编写的方式有三种,分别是mybatis、tkmybatis、mybatis-plus,如果每个都要进行修改会浪费大量的时间,而且不小心会会编写错误,所以最好用一个拦截器去处理以前的sql进行处理,便于之后的使用,也便于功能的扩展;
2、使用这样做的好处
1)代码编写范围小;
2)可以对多种sql功能进行统一的处理(select、insert、update、where)
3)省去了程序员大量的时间
4)兼容的更加透明
3、思路与考量
根据切面,将编写的注释进行统一的数据格式处理,通过对pagehelper的架构分析,可以将注释中的数据统一存入到ThreadLocal之中,以threaName作为key值,这样就不担心并发导致的数据混乱问题;
然后就可以对相应的sql进行相应的处理;
注:1)如果利用@Intercept注解的话,每次加载都会将自定义的拦截器放在pageIntercept之后,需要自定义一个configration将自定义的拦截器放入page之前;
2)因为pageIntercept拦截器有两次的exector,第一次是count(*),第二次是数据的查询,所以对ThreadLocal数据的清理要两次之后才可以执行;
4、为什么要拦截statementHandler?(详细解析请看源码分析)
拦截器的拦截级别是exector为第一位,最后才是statementHandler,而我们的拦截器是继承了mybatis-plus的抽象类,所以page每次new exector()的时候都会进行拦截器的遍历,若是我们拦截exector方法就不能使用该抽象类,第二点statementHandler其中包含了sql的基本处理方法,若以后再进行修改的话更加的方便;(有其它的考量点请评论)
5、源码分析
1、功能描述:项目后期需要添加新的字段,mybatis用的是tk、pius、原生,修改起来及其麻烦而且浪费时间,所以需要一个拦截器将sql语句进行升级,加字段,但是在项目之中又使用了pageHelper分页,本派文章将对兼容进行讲解并且讲解下拦截器的逻辑;
2、mybatis拦截器的使用方式:
1)注解类(这次的主要讲解,主要是在调用拦截器的时候才会扫描,属于懒加载):
@Intercepts标记该方法是一个拦截器,在我们调用InterceptorChain中的pluginAll方法时,加载的拦截器会顺序执行每个拦截器之中的plugin方法
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
//调用自定义的plugin方法,一般我们只需要使用默认的就好
target = interceptor.plugin(target);
}
return target;
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
通过调用wrap方法,我们会调用到getSignatureMap方法,这是一个Map<Class<?>, Set<Method>>,key是我们定义在@Signature注解之中的type的全路径,而value是定义在@Signature的methode
然后通过动态代理调用Proxy.newProxyInstance相对的intercept方法;
2)xml配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<plugins>
<plugin interceptor="com.testmybatis.interceptor.SqlInterceptor" />
</plugins>
</configuration>
加载主要是在项目启动时,XMLConfigBuilder类pluginElement进行xml解析加载;
3)注解参数解析:
1)@Intercepts之中的Signature可以配置多个,通过plugin方法会根据type值、method值、args(方法对应的参数)加载一个对应类的方法来构造Invocation类执行intercept方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
2)type分析:
StatementHandler.class、Executor这些组件我们下一篇再分析
3、代码实现,代码比较全,针对tk等普遍的mybatis:
package com.example.study.practice.interceptor.annotation;
import com.example.study.practice.entity.SQLEnum;
import org.apache.ibatis.mapping.SqlCommandType;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AND {
String fileName() default "";
String fileValue() default "";
SQLEnum type() default SQLEnum.UNKNOW;
SqlCommandType sqlType() default SqlCommandType.UNKNOWN;
}
@Override
@AND(fileName = "age" , fileValue = "8" ,type = SQLEnum.WHERE,sqlType = SqlCommandType.SELECT)
public PageInfo<User> selectUsers() {
PageHelper.clearPage();
PageHelper.startPage(1,3);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo(users);
return pageInfo;
}
package com.example.study.practice.entity;
public enum SQLEnum {
UNKNOW , GROUP , OREDER , HAVING , WHERE , AND
}
借鉴了pageHelper的实现,利用threadLocal进行存储需要传递到拦截器的数据
package com.example.study.practice.interceptor;
import com.example.study.practice.interceptor.abstractAspect.AbstractAspect;
import com.example.study.practice.interceptor.annotation.AND;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
@Aspect
public class SQLAspect extends AbstractAspect {
@Pointcut("@annotation(com.example.study.practice.interceptor.annotation.AND)")
public void andSQLCut(){}
@Before("andSQLCut()")
public void andMethod(JoinPoint point){
StringBuilder str = new StringBuilder();
StringBuilder strOne = new StringBuilder();
MethodSignature methodSignature = (MethodSignature)point.getSignature();
// 获得对应注解
AND and = methodSignature.getMethod().getAnnotation(AND.class);
if (!StringUtils.isEmpty(and)){
str.append(and.fileName()).append(" = ").append(and.fileValue());
strOne.append(and.fileName());
putSQL(str.toString(),and.type(),strOne.toString(),and.fileValue());
}
}
}
package com.example.study.practice.interceptor.abstractAspect;
import com.example.study.practice.entity.SQLEntity;
import com.example.study.practice.entity.SQLEnum;
import com.example.study.practice.entity.factory.FactoryProducer;
import org.aspectj.lang.JoinPoint;
public abstract class AbstractAspect {
protected static final ThreadLocal<SQLEntity> LOCAL_SQL_ENTITY = new ThreadLocal<SQLEntity>();
public void putSQL(String sql , SQLEnum sqlEnum,String strOne,String value){
SQLEntity sqlEntity = FactoryProducer.getFactory("SQL").getSQLEntity();
sqlEntity.setANDSql(sql);
sqlEntity.setSqlEnum(sqlEnum);
sqlEntity.setBySql(strOne);
sqlEntity.setValue(value);
LOCAL_SQL_ENTITY.set(sqlEntity);
}
public static SQLEntity getSQLEntity(){
return LOCAL_SQL_ENTITY.get();
}
public static void remove(){
LOCAL_SQL_ENTITY.remove();
}
}
package com.example.study.practice.interceptor;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
import com.example.study.practice.entity.SQLEntity;
import com.example.study.practice.server.Context;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.Connection;
import java.util.*;
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = StatementHandler.class, method = "getBoundSql", args = {}),
})
//@Component
public class DataFilterInterceptor extends AbstractSqlParserHandler implements Interceptor {
private static final String WHERE = "WHERE";
private static final List<SqlCommandType> sqlCommandTypeList = new ArrayList<>();
static {
sqlCommandTypeList.add(SqlCommandType.UPDATE);
sqlCommandTypeList.add(SqlCommandType.SELECT);
sqlCommandTypeList.add(SqlCommandType.INSERT);
}
//TODO:1.在使用完之后清除ThreadLocal数据 2.多线程测试 3.与pageHelper加载兼容
//1完成 2
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
sqlParser(metaObject);
SQLEntity sqlEntity = SQLAspect.getSQLEntity();
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
if (!sqlCommandTypeList.contains(mappedStatement.getSqlCommandType()) || sqlEntity == null) {
return invocation.proceed();
}
try {
// 取出原始SQL 取出参数
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
StringBuilder dataSql = new StringBuilder(boundSql.getSql());
dataSql = new StringBuilder(dataSql.toString().toUpperCase());
int i1 = 0;
if (dataSql.indexOf(WHERE) > 0) {
i1 = dataSql.indexOf(WHERE);
}
Context context = new Context(mappedStatement.getSqlCommandType());
String dataSqlString = context.jointSQL(sqlEntity, i1, dataSql);
BoundSql sql = new BoundSql(mappedStatement.getConfiguration(), dataSqlString, boundSql.getParameterMappings(), invocation.getArgs()[1]);
metaObject.setValue("delegate.boundSql", sql);
} catch (Exception e) {
throw new Exception(e);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
}
public static void main(String[] args) {
StringBuilder dataSql = new StringBuilder("ccccccWHERE");
System.out.println(dataSql.indexOf("where"));
}
}
具体的sql拦截就不粘贴过来了,代价可以利用策略模式处理select、update、insert的sql
4、分析如何将自己的拦截器防入pageHelper拦截器前的:
通过注解类将自定义的拦截器放入
package com.example.study.practice.interceptor.config;
import com.example.study.practice.interceptor.DataFilterInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.List;
@Configuration
public class MybatisConfig {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void add(){
DataFilterInterceptor dataFilterInterceptor = new DataFilterInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
//自己添加
configuration.addInterceptor(dataFilterInterceptor);
}
}
}
我们自定义的拦截器出现在了分页的前面,所以先加载我们自定义的,但是因为plugin方法我们只对StatementHandler进行了实现,所以分页的执行顺序在我们前面,因为pluginAll方法会先进行exector调用,执行到了具体的query时,在调用exector之后会再次调用拦截器,而分页拦截器虽然配置了statment,但会先执行我们配置的,所以实现成功;
调用两次的原因是因为page拦截器中调用了两次executor.query方法,query的方法实现也是statment下的方法,所以直接返回了,没有往下走;
5、改进:因为用的是mybatisplus,所以只能使用StatementHandler,期望在分页之前对我们的sql进行处理;