Mybatis 的 Interceptor(拦截器) 与 JSqlparser 结合解析SQL 使SpringBoot项目多数据库兼容的尝试(推荐)

 更新时间:2026年01月08日 09:22:25   作者:刘子丙  
MyBatis插件提供了在SQL执行前进行拦截、扩展的功能,允许用户自定义逻辑处理,本文介绍Mybatis 的 Interceptor(拦截器) 与 JSqlparser 结合解析SQL 使SpringBoot项目多数据库兼容的尝试(推荐),感兴趣的朋友跟随小编一起看看吧

Mybaits插件简单描述

插件简介和具体作用

Mybatis官方提供了插件机制为用户开辟了一道可以自定义的拦截扩展功能,在 系统最终执行SQL 之前,分别有四个部位可以做扩展,允许用户在不修改Mybatis核心代码的情况下,添加自己的逻辑处理,去完成各种各样的业务场景

官方简介:mybatis – MyBatis 3 | 配置 

典型案例:

1.MyBatis 分页插件 PageHelper 

2.分页插件 | MyBatis-Plus (baomidou.com)

业务场景示例

  1. 性能监控:记录SQL执行时间,统计慢查询,帮助开发者发现并优化性能瓶颈
  2. 访问控制:根据用户的权限,动态修改SQL语句,实现数据行级或列级的权限控制。
  3. 动态SQL注入:根据不同的业务需求,动态拼接或修改SQL语句。
  4. 数据脱敏:在查询结果返回给客户端之前,对敏感信息进行脱敏处理。
  5. 缓存控制:对查询结果进行缓存,减少数据库访问次数,提高系统性能。
  6. 自定义分页:用户也可以像pageHelper、mybatis-plus的分页那样自定义自己的分页
  7. 加密解密:利用这个机制,对数据库中敏感字段的对称加密,诸如密码、卡号、身份信息之类的
  8. 自定义日志输出:拦截SQL执行,实现自定义的日志输出格式,便于问题追踪和调试。
  9. 动态数据源切换:在拦截器中根据业务规则,动态选择不同的数据源。
  10. 参数校验:在执行SQL语句前,对参数进行校验,确保数据的完整性和一致性。

四个核心组件简介

这四个部位其实就是Mybatis的四大核心组件:

  1. Executor (执行器):负责增删改查和事务,它调度(另外三个)StatementHandlerParameterHandlerResultSetHandler等来执行对应的SQL
  2. StatementHandler(语句预处理) : 封装JDBC,构建SQL语法,负责和数据库进行交互执行sql语句,(后期下手操作和修改SQL也主要是以它为主)
  3. ParameterHandler (参数处理): 负责将参数真正加入到SQL语句中的部分
  4. ResultSetHandler (结果处理) :负责将JDBC查询结果映射到java对象

以下是流程图:

拦截方法划分

精细划分的目的是为了允许在数据库操作的不同阶段进行精确的干预和拦截

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
                Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
                Object.class,
                RowBounds.class,
                ResultHandler.class})})
@Component
public class MyApplicationInterceptor implements Interceptor {
       // 具体实现内容...
}

可拦截的方法:

  • update:负责执行 insert、update、delete 三种类型的 SQL 语句。
  • query:负责执行 select 类型的 SQL 语句。
  • queryCursor:负责执行 select 类型的 SQL 语句,返回 Cursor 对象。
  • flushStatements:提交批处理语句,返回批处理结果。
  • commit:事务提交。
  • rollback:事务回滚。
  • getTransaction:获取事务对象。
  • close:关闭 executor,同时根据参数决定是否强制回滚未提交的事务。
  • isClosed:检查 executor 是否已经关闭。
  • clearLocalCache:清除本地缓存。
ParameterHandler (getParameterObject, setParameters)
@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
@Slf4j
public class ParameterPluginInterceptor implements Interceptor {}

  • 可拦截的方法:
    • getParameterObject:此方法用于获取 SQL 参数对象。
    • setParameters:此方法将 SQL 命令中的参数与实际的参数对象相匹配。它负责将传入的参数设置到 PreparedStatement 中。
StatementHandler (prepare, parameterize, batch, update, query)
@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
@Slf4j
public class StatementPluginInterceptor implements Interceptor {}
  • prepare:准备一个数据库 Statement 对象以待执行。这个方法根据配置和上下文信息来创建一个 PreparedStatement 或 CallableStatement 对象。
  • parameterize:在 SQL 语句被执行之前,该方法负责将 SQL 参数设置到 PreparedStatement 对象中。
  • batch:负责处理批量执行的逻辑,将多个更新语句作为一个批处理提交。
  • update:执行写操作(insert、update、delete)的 SQL 语句。
  • query:执行查询操作(select)的 SQL 语句,并返回结果。
  • queryCursor:负责执行查询操作(select)SQL 语句,返回 Cursor 对象。
  • getBoundSql:返回 BoundSql 对象,这个对象包含了要执行的 SQL 语句以及该语句中所需的参数信息。
ResultSetHandler (handleResultSets, handleOutputParameters)
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
})
/*@Component*/
@Slf4j
public class ResultInterceptor implements Interceptor {
  • handleResultSets:这是主要的方法之一,它接受一个 Statement 对象作为参数,并将 SQL执行的结果 ResultSet 映射到结果对象。
  • handleOutputParameters:当存储过程调用完成之后,这个方法会处理其输出参数。它同样接受一个 Statement 对象作为参数。

拦截器实现:

如何实现一个自己的拦截器

implements Interceptor  以实现一个Mybatis的拦截器,之后必须实现以上三个方法

  1. intercept 拦截 : 主要实现拦截逻辑的地方,也就是用户拿来自定义业务的地方
  2. plugin:包装方法,用来创建代理对象
  3. setProperties:配置方法,用来设置拦截器的属性

以下是一个空白的Executor拦截:集成Interceptor之后,再加入注解内容选择要拦截的部分

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
                Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
                Object.class,
                RowBounds.class,
                ResultHandler.class})})
@Component
@Slf4j
public class ExecutorInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement)  invocation.getTarget();
        log.info("==> ExecutorInterceptor: {}", mappedStatement.getId());
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
    }
}

Invocation中有什么

以下是一个使用mybaits interceptor拦截器,拦截StatementHandler

可以看到 invocation 中已经夹了很多做拦截器需要的内容了

最重要的东西就是

  • BoundSql(解析后的sql对象) 首先它能拿到这次执行中的SQL,其次就是参数,等同于拿到一个完全可以执行的SQL语句。很多IDEA里Mybaits log这样的插件就是根据这个东西这么来的。
  • BoundSql—ParameterObject对象 顾名思义,就是SQL的参数对象,它直接接受到我们从外界传到内部的参数对象,它是一个Object,根据传递的类型而改变
  • BoundSql—ParameterMapping对象 再mybatis的 mapper xml中写的每一个#{var},最后都会形成一个 ?,这和我们直接使用jdbc是一个道理,最终都是挨个往 ?(问号) 里面塞值,只不过ParameterMapping记录了这些变量,它会在SQL最终要执行的时候去设置参数

实现一个简单的SQL语句拦截

以下实现了一个简单的拦截器,拦截MappedStatement

import lombok.extern.slf4j.Slf4j;
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 org.springframework.stereotype.Component;
import java.util.Properties;
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
                Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
                Object.class,
                RowBounds.class,
                ResultHandler.class})})
@Component
@Slf4j
public class ExecutorInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf('.'));
        String methodName = id.substring(id.lastIndexOf('.') + 1);
        BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
        Object parameterObject = boundSql.getParameterObject();
        log.info("==> id: {}",id);
        log.info("==> ClassName: {}", className);
        log.info("==> MethodName: {}", methodName);
        log.info("==> SQL语句: {}", boundSql.getSql());
        log.info("==> 参数: {}", parameterObject);
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }
}

最终效果:(可以看到,打印了SQL的来源,包、方法、SQL语句、参数都拿到了)

拦截SQL执行的最终结果

ResultSetHandler是对SQL最终操作的结果映射到java中的步骤,以下代码的操作,最终将拦截这个步骤,并对MAP相关结果做出操作,将MAP中的Key都转成大写

@Slf4j
@Component
@Intercepts(
        {@Signature(
                type = ResultSetHandler.class,
                method = "handleResultSets",
                args = {Statement.class}
        )})
public class ResultMapCaseInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ResultSetHandler resultSetHandler = (ResultSetHandler) invocation.getTarget();
        MetaObject metaResultSetHandler = MetaObject.forObject(resultSetHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
        MappedStatement mappedStatement = (MappedStatement) metaResultSetHandler.getValue("mappedStatement");
        Object returnValue = invocation.proceed();
        List<ResultMap> resultMaps = mappedStatement.getResultMaps();
        String resultMapTypeName = resultMaps.get(0).getType().getName();
        String MAP = "java.util.Map";
        String HASH_MAP = "java.util.HashMap";
        String LINKED_HASH_MAP = "java.util.LinkedHashMap";
        if(StrUtil.equalsIgnoreCase(MAP, resultMapTypeName) ||
                StrUtil.equalsIgnoreCase(HASH_MAP, resultMapTypeName) ||
                StrUtil.equalsIgnoreCase(LINKED_HASH_MAP, resultMapTypeName)){
            if(returnValue instanceof List<?> list){
                if(CollectionUtil.isNotEmpty(list)) {
                    list.forEach(item -> {
                            Map<String, Object> map = (Map<String, Object>) item;
                            Map<String, Object> newMap = new LinkedHashMap<>();
                            map.forEach((k, v) -> {
                                    newMap.put(k.toUpperCase(), v);
                            });
                            map.clear();
                            map.putAll(newMap);
                    });
                }
            }
        }
        return returnValue;
    }
    @Override
    public Object plugin(Object target) {
        return Interceptor.super.plugin(target);
    }
    @Override
    public void setProperties(Properties properties) {
    }
}

使用JSQLparser解析SQL语句

简述及安装

JSqlParser 是一个 SQL 语句解析器。它转换 Java 类的可遍历层次结构中的 SQL。JSqlParser不仅限于一个数据库,而是支持Oracle,SqlServer,MySQL,PostgreSQL等等,至今它仍在更新,而Mybatisplus之中也包含了这个库,可以直接使用,如果没有使用mybatis plus那么需要手动引入它

官方网站:JSQLParser 4.9 documentation 

Github:JSQLParser/JSqlParser(github.com)

Maven:

<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>4.9</version>
</dependency>

Gradle/KT:

implementation("com.github.jsqlparser:jsqlparser:4.9")
简单的使用

以下是一个简单的例子,为了展示Jsqlparser可以解析SQL的内容

    @Test
    public void Test2() throws JSQLParserException, ParseException {
        String originalSql = "SELECT " +
                "t1.id, " +
                "t1.name, " +
                "SUM(t2.amount) AS total_amount, " +
                "(SELECT COUNT(*) FROM orders o WHERE o.customer_id = t1.id) AS order_count " +
                "FROM customers t1 " +
                "JOIN orders t2 ON t1.id = t2.customer_id " +
                "LEFT JOIN payments t3 ON t2.id = t3.order_id " +
                "WHERE t1.status = 'active' " +
                "AND t2.order_date BETWEEN '2023-01-01' AND '2023-12-31' " +
                "GROUP BY t1.id, t1.name " +
                "HAVING SUM(t2.amount) > 1000 " +
                "ORDER BY total_amount DESC, t1.name ASC;";
        CCJSqlParser parser = CCJSqlParserUtil.newParser(originalSql);
        Statement statement = parser.Statement();
        parser.getASTRoot().jjtAccept(sqlModifier, null);
        log.info("==> JsqlParser SQL: {}", statement.toString());
        Select selectStatement = (Select) statement;
        PlainSelect plainSelect = selectStatement.getPlainSelect();
        Table table = (Table) plainSelect.getFromItem();
        System.out.println(table.toString());
        //获取表名们
        Set<String> tableNames = TablesNamesFinder.findTables(originalSql);
        System.out.println("表名们:"+tableNames);
        // Print SELECT clause
        System.out.println("SELECT clause: " + plainSelect.getSelectItems());
        // Print FROM clause
        System.out.println("FROM clause: " + plainSelect.getFromItem());
        // Print JOIN clauses
        if (plainSelect.getJoins() != null) {
            for (Join join : plainSelect.getJoins()) {
                System.out.println("JOIN clause: " + join);
            }
        }
        // Print WHERE clause
        System.out.println("WHERE clause: " + plainSelect.getWhere());
        // Print GROUP BY clause
        System.out.println("GROUP BY clause: " + plainSelect.getGroupBy());
        // Print HAVING clause
        System.out.println("HAVING clause: " + plainSelect.getHaving());
        // Print ORDER BY clause
        System.out.println("ORDER BY clause: " + plainSelect.getOrderByElements());
    }
直接使用

以下是一个简单的使用,在代码里使用Jsqlparser,最直接的方式就是直接调用CCJSqlParserUtil

String originalSql = "select * from t_user" // 需要解析的SQL语句
CCJSqlParser parser = CCJSqlParserUtil.newParser(originalSql);
Statement statement = parser.Statement();
拆解SQL语句

通过 getPlainSelect() 获得Statement中的PlainSelect,而这个PlainSelect就可以拿到很多SQL语句中的内容

 获取各种子元素
        String originalSql = "SELECT rrr.* FROM rel_role_resource rrr " +
                "JOIN rel_role_user rru ON rrr.role_id = rru.role_id " +
                "JOIN `user` u ON u.id = rru.user_id " +
                "AND rrr.org_id = ? " +
                "AND rrr.role_id = rru.role_id " +
                "AND u.id = 123";
        CCJSqlParser parser = CCJSqlParserUtil.newParser(originalSql);
        Statement statement = parser.Statement();
        Select select = (Select) statement;
        log.info("==> Select: {}", select.toString());
        PlainSelect plain = select.getPlainSelect();
        plain.getSelectItems().forEach(selectItem -> {
            log.info("==> 查询的字段: {}", selectItem.toString());
        });
        if (plain.getFromItem() != null) {
            log.info("==> 查询的表: {}", plain.getFromItem().toString());
        }
        if (plain.getWhere() != null){
            log.info("==> 查询的条件: {}", plain.getWhere().toString());
        }
        if(CollectionUtil.isNotEmpty(plain.getJoins())){
            plain.getJoins().forEach(join -> {
                join.getOnExpressions().forEach(expression -> {
                    log.info("==> Join 条件: {}", expression.toString());
                });
                log.info("==> Join: {}", join.toString());
            });
        }
        TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
        Set<String> tableList = tablesNamesFinder.getTables(statement);
        tableList.forEach(table -> {
            log.info("==> 查询的表: {}", table);
        });
        log.info("==> 查询的表: {}", tableList);

运行后结果:

关于Jsqlparser的具体使用,可以看另外一篇博客【JSqlParser】Java使用JSqlParser解析SQL语句总结

总结

使用Springboot + mybatis + Jsqlparser,通过Mybatis的interceptor,可以获取到整个项目中通过mybatis执行的SQL语句,最终在StatementHandler 中截取SQL,对截取到的SQL进行分析,再通过Jsqlparser的visit 对SQL 的不同位置,例如替换 引号、特殊字符、函数等,做处理,最终可以达到数据库迁移并适配的效果

到此这篇关于Mybatis 的 Interceptor(拦截器) 与 JSqlparser 结合解析SQL 使SpringBoot项目多数据库兼容的尝试(推荐)的文章就介绍到这了,更多相关Mybatis Interceptor拦截器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java 对象输入输出流读写文件的操作实例

    java 对象输入输出流读写文件的操作实例

    这篇文章主要介绍了java 对象输入输出流读写文件的操作实例的相关资料,这里使用实现Serializable接口,需要的朋友可以参考下
    2017-07-07
  • 一文教你使用Java Calendar类进行日期计算

    一文教你使用Java Calendar类进行日期计算

    在日常开发中,我们常常需要进行日期计算,比如计算两个日期之间的天数、月数,在Java中,我们可以使用Java Calendar类来进行日期计算,下面就跟随小编一起来学习一下吧
    2023-10-10
  • SpringBoot中@Scheduled()注解以及cron表达式详解

    SpringBoot中@Scheduled()注解以及cron表达式详解

    这篇文章主要介绍了SpringBoot中@Scheduled()注解以及cron表达式详解,@Scheduled注解是Spring Boot提供的用于定时任务控制的注解,主要用于控制任务在某个指定时间执行,或者每隔一段时间执行,需要的朋友可以参考下
    2023-08-08
  • Spring Boot项目维护全局json数据代码实例

    Spring Boot项目维护全局json数据代码实例

    这篇文章主要介绍了Spring Boot项目维护全局json数据代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • FactoryBean BeanFactory方法使用示例详解讲解

    FactoryBean BeanFactory方法使用示例详解讲解

    这篇文章主要为大家介绍了FactoryBean BeanFactory方法使用示例详解讲解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • Mybatis Mapper中多参数方法不使用@param注解报错的解决

    Mybatis Mapper中多参数方法不使用@param注解报错的解决

    这篇文章主要介绍了Mybatis Mapper中多参数方法不使用@param注解报错的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教。
    2022-01-01
  • java大文件上传处理方法实例代码

    java大文件上传处理方法实例代码

    在Java中实现大文件上传功能,确实需要考虑到文件大小可能超出内存限制、网络传输稳定性等因素,这篇文章主要介绍了java大文件上传处理方法的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-09-09
  • Javac/javap 自带工具简单使用讲解

    Javac/javap 自带工具简单使用讲解

    这篇文章主要介绍了Javac/javap 自带工具简单使用讲解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • MyBatis开启二级缓存实现过程解析

    MyBatis开启二级缓存实现过程解析

    这篇文章主要介绍了MyBatis开启二级缓存实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07
  • 简单分析java中CMS回收器

    简单分析java中CMS回收器

    在本篇文章里我们给大家分享了关于java中CMS回收器的相关知识点内容,有需要的朋友们可以跟着学习下。
    2018-10-10

最新评论