自定义sql拦截器与pageHelper兼容

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进行处理;

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值