MyBatis 通过拦截修改SQL

假如我们想实现多租户,或者在某些SQL后面自动拼接查询条件。在开发过程中大部分场景可能都是一个查询写一个SQL去处理,我们如果想修改最终SQL可以通过修改各个mapper.xml中的SQL来处理。但实际过程中我们可能穿插着ORM和SQL的混合使用,隐藏在代码中不容易被发现,还有假如项目中有很多很多的SQL我们不可能一一的去修改解决。这个时候我们就需要通过mybatis拦截SQL并且最终修改SQL。

maven依赖


    org.mybatis
    mybatis-spring
    2.0.4



    org.springframework
    spring-core
    5.2.4.RELEASE
    compile




    mysql
    mysql-connector-java
    8.0.26




    com.github.jsqlparser
    jsqlparser
    4.2



    org.mybatis
    mybatis-spring
    2.0.4


    com.baomidou
    mybatis-plus
    2.3.3



    org.mybatis
    mybatis
    3.5.4


    com.alibaba
    druid-spring-boot-starter
    1.2.6


    org.projectlombok
    lombok


    org.projectlombok
    lombok

配置部分

这部分是传统mybatis的xml配置,如果是Springboot项目或者使用JavaConfig配置的请查看官方文档配置方式。无非就是Springboot封装了mybatis-xxx-stater包将部分配置都转为了参数控制以及部分autoconfig,大同小异这里不做过多讨论。





    
    
        
        
        
        
        
        
        
        
        
        
        
        
        
    

    
    
    

    

    
    

   
    
        
        
    

    
        
            
            
                
                
                
                
            
        
    

    
        
    


整个拦截调用链路流程图:

[图片上传失败...(image-fbe423-1656999539717)]

① mybatis在这一层包装了StatementHandler返回代理对象,下一步调用prepare的时候会先调用增强拦截器。
Configuration.newStatementHandler

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    // 对statementhandler进行代理
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

InterceptorChain.pluginAll

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
       target = interceptor.plugin(target);
     }
     return target;
}

Interceptor.plugin

default Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

包装成代理对象
Plugin.wrap

public static Object wrap(Object target, Interceptor interceptor) {
    Map, Set> signatureMap = getSignatureMap(interceptor);
    Class type = target.getClass();
    // 获取target对象的所有接口类型
    Class[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
    // 学习过JDK动态代理的同学对这段代码肯定很熟,这里是生成一个代理对象
    return Proxy.newProxyInstance(
           type.getClassLoader(), // 类加载器
           interfaces, // JDK动态代理必须要有接口
           new Plugin(target, interceptor, signatureMap));
     }
     return target;
}

如果大家觉得这篇文章写的还可以请关注我,我后续会出mybatis的源码解析。

调用代码

这部分代码负责调用mybatis,如果使用springboot这部分就是你的rest接口。

import com.allens.mybatis.model.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TestMybatis {

    public static void main(String[] args) throws IOException {
        // 读取配置文件
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        // 通过SqlSessionFactoryBuilder创建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 获取到SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 调用Mapper中的指定方法 com.wyh.mapper.UserMapper.queryAll是statementId
        Map map = new HashMap<>();
        map.put("pageSize", 1);
        map.put("desc", "desc");
        map.put("name", "Allens");
        List userList = sqlSession.selectList("com.allens.mybatis.mappers.UserMapper.selectUsers", map);
        System.out.println("++++++++++++++++++++++");
        userList.forEach(System.out::println);
    }
}

拦截器的代码实现

这使用了Druid的SQLParser进行解析SQL,如果不想使用druid可以使用sqlparser包进行sql解析。不管用什么样的工具把SQL修改掉就行了,形式不限。

如果想使用sqlparser进行解析sql可以看我的下一篇文章

/**
 * MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
 * 

* Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) * ParameterHandler (getParameterObject, setParameters) * ResultSetHandler (handleResultSets, handleOutputParameters) * StatementHandler (prepare, parameterize, batch, update, query) * 这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。 *

* 通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可 */ //@Intercepts({@Signature( // type= Executor.class, // method = "query", // args = {StatementHandler.class, Object.class, RowBounds.class, ResultHandler.class})}) @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class SQLInterceptor implements Interceptor { private Properties properties = new Properties(); public List getInsertPropertiesName(List> properties) { List list = new ArrayList<>(); properties.forEach(data -> data.keySet() .stream() .filter(e -> e.equals("columnName")) .forEach(e -> list.add((String) data.get("columnName")))); return list; } public void setInsertProperties(List> properties, String columnName, Object value) { properties.forEach(element -> element.forEach((k, v) -> { if (k.equals("columnName") && element.get(k).equals(columnName)) { element.put("columnValue", value); element.put("columnName", columnName); } })); } @Override public Object intercept(Invocation invocation) throws Throwable { // implement pre processing if need List> insertProperties = new ArrayList<>(); Map nameProperties = new HashMap<>(); nameProperties.put("columnName", "name"); nameProperties.put("columnValue", "baby"); nameProperties.put("expr", "and"); insertProperties.add(nameProperties); Map namePropertiesOr = new HashMap<>(); namePropertiesOr.put("columnName", "id"); namePropertiesOr.put("columnValue", "1"); namePropertiesOr.put("expr", "or"); insertProperties.add(namePropertiesOr); // BoundSql boundSql = ((MappedStatement)invocation.getArgs()[0]).getBoundSql(invocation.getArgs()[1]); StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); // 获取传入的参数 Object parameterMappings = statementHandler.getBoundSql().getParameterObject(); if (parameterMappings instanceof Map) { Map parameterMappingsConvert = (Map) parameterMappings; getInsertPropertiesName(insertProperties).forEach(e -> { Object value = parameterMappingsConvert.get(e); if (value != null) { // 如果传参值不为空就覆盖配置值 setInsertProperties(insertProperties, e, value); } }); } MetaObject metaObject = MetaObject .forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory()); BoundSql boundSql = statementHandler.getBoundSql(); SQLStatementParser sqlParser = SQLParserUtils.createSQLStatementParser(boundSql.getSql(), DbType.MYSQL.getValue()); SQLStatement stmt = sqlParser.parseStatementList().get(0); System.out.println("Origin SQL is:" + boundSql.getSql()); System.out.println("++++++++++++++++++++++"); if (stmt instanceof SQLSelectStatement) { // ((SQLSelectStatement) stmt).addWhere(sqlExpr); // convert conditions to 'and' statement StringBuilder constraintsBuffer = new StringBuilder(); boolean first = true; for (Map data : insertProperties) { if (String.valueOf(data.get("expr")).equalsIgnoreCase("and")) { if (!first) { constraintsBuffer.append(" AND "); } constraintsBuffer.append(String.format(" %s = '%s' ", data.get("columnName"), data.get("columnValue"))); } else if (String.valueOf(data.get("expr")).equalsIgnoreCase("or")) { if (!first) { constraintsBuffer.append(" OR "); } constraintsBuffer.append(String.format(" %s = '%s' ", data.get("columnName"), data.get("columnValue"))); } first = false; } SQLExprParser constraintsParser = SQLParserUtils.createExprParser(constraintsBuffer.toString(), JdbcUtils.MYSQL); SQLExpr constraintsExpr = constraintsParser.expr(); SQLSelectStatement selectStmt = (SQLSelectStatement) stmt; // 拿到SQLSelect 通过在这里打断点看对象我们可以看出这是一个树的结构 SQLSelect sqlselect = selectStmt.getSelect(); SQLSelectQueryBlock query = (SQLSelectQueryBlock) sqlselect.getQuery(); SQLExpr whereExpr = query.getWhere(); // 修改where表达式 if (whereExpr == null) { query.setWhere(constraintsExpr); } else { SQLBinaryOpExpr newWhereExpr = new SQLBinaryOpExpr( whereExpr, SQLBinaryOperator.BooleanAnd, constraintsExpr); query.setWhere(newWhereExpr); } sqlselect.setQuery(query); String sql = sqlselect.toString(); //通过反射修改sql语句 Field field = boundSql.getClass().getDeclaredField("sql"); field.setAccessible(true); field.set(boundSql, sql); System.out.println("modify sql is:" + sql); } // implement post processing if need return invocation.proceed(); } @Override public Object plugin(Object target) { return Interceptor.super.plugin(target); } @Override public void setProperties(Properties properties) { this.properties = properties; } }

可以看到上面使用了反射去设置boundsql.sql,有可能有同学会问这样会不会触发JVM优化修改不了这个final String属性。这里我下一个结论是可以的,为了解释这个问题我写了端代码帮助理解:

package base;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class FinalPropertyModify {

    private final String a = "123";

    private final String ab;

    public FinalPropertyModify(String ab) {
        this.ab = ab;
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
        // 直接赋值初始化
        FinalPropertyModify finalPropertyModify = new FinalPropertyModify("123");
        Field a = FinalPropertyModify.class.getDeclaredField("a");
        a.setAccessible(true);
        a.set(finalPropertyModify, "12345");

        // 构造函数初始化
        Field ab = FinalPropertyModify.class.getDeclaredField("ab");
        ab.setAccessible(true);
        ab.set(finalPropertyModify, "123456");

        System.out.println("==================反射获取==================");
        System.out.println(a.get(finalPropertyModify));
        System.out.println(ab.get(finalPropertyModify));

        System.out.println("==================直接获取==================");
        System.out.println(finalPropertyModify.a);
        System.out.println(finalPropertyModify.ab);
    }
}

最终输出结果为:

[图片上传失败...(image-87672a-1656999539717)]

可以看到如果是直接赋值进行初始化final属性的话,会被JVM给优化掉,如果使用的是构造函数进行初始化属性是不是触发JVM优化的。我们再看一下boundsql类的属性定义,很显然sql属性是在构造函数中进行初始化的。我们可以大胆的去modify sql,但一定要注意不能修改成错误的SQL和一定要考虑安全问题,mybatis没有提供sql的修改方法也是考虑这一点,可能会不安全。

public class BoundSql {

  // 我们要修改的SQL属性
  private final String sql;
  private final List parameterMappings;
  private final Object parameterObject;
  private final Map additionalParameters;
  private final MetaObject metaParameters;

  public BoundSql(Configuration configuration, String sql, List parameterMappings, Object parameterObject) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.parameterObject = parameterObject;
    this.additionalParameters = new HashMap<>();
    this.metaParameters = configuration.newMetaObject(additionalParameters);
  }

  public String getSql() {
    return sql;
  }

  public List getParameterMappings() {
    return parameterMappings;
  }

  public Object getParameterObject() {
    return parameterObject;
  }

  public boolean hasAdditionalParameter(String name) {
    String paramName = new PropertyTokenizer(name).getName();
    return additionalParameters.containsKey(paramName);
  }

  public void setAdditionalParameter(String name, Object value) {
    metaParameters.setValue(name, value);
  }

  public Object getAdditionalParameter(String name) {
    return metaParameters.getValue(name);
  }
}

UserMapper.xml

很简单的一个SQL




    

测试

运行成功:

[图片上传失败...(image-5524a2-1656999539717)]

你可能感兴趣的:(MyBatis 通过拦截修改SQL)