java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Mybatis  Interceptor拦截器

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

作者:刘子丙

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 {
       // 具体实现内容...
}

可拦截的方法:

ParameterHandler (getParameterObject, setParameters)
@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
@Slf4j
public class ParameterPluginInterceptor implements Interceptor {}

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 {}
ResultSetHandler (handleResultSets, handleOutputParameters)
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
})
/*@Component*/
@Slf4j
public class ResultInterceptor implements Interceptor {

拦截器实现:

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

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 中已经夹了很多做拦截器需要的内容了

最重要的东西就是

实现一个简单的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拦截器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文