当前位置:   article > 正文

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

ccjsqlparserutil mybatis引用

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)
  1. @Intercepts({
  2. @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
  3. Object.class}),
  4. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
  5. Object.class,
  6. RowBounds.class,
  7. ResultHandler.class})})
  8. @Component
  9. public class MyApplicationInterceptor implements Interceptor {
  10. // 具体实现内容...
  11. }

可拦截的方法:

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

    可拦截的方法:

  • getParameterObject:此方法用于获取 SQL 参数对象。
  • setParameters:此方法将 SQL 命令中的参数与实际的参数对象相匹配。它负责将传入的参数设置到 PreparedStatement 中。
StatementHandler (prepare, parameterize, batch, update, query)
  1. @Component
  2. @Intercepts({
  3. @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
  4. })
  5. @Slf4j
  6. 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)
  1. @Intercepts({
  2. @Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
  3. })
  4. /*@Component*/
  5. @Slf4j
  6. public class ResultInterceptor implements Interceptor {
  • handleResultSets:这是主要的方法之一,它接受一个 Statement 对象作为参数,并将 SQL执行的结果 ResultSet 映射到结果对象。
  • handleOutputParameters:当存储过程调用完成之后,这个方法会处理其输出参数。它同样接受一个 Statement 对象作为参数。

拦截器实现:

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

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

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

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

  1. @Intercepts({
  2. @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
  3. Object.class}),
  4. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
  5. Object.class,
  6. RowBounds.class,
  7. ResultHandler.class})})
  8. @Component
  9. @Slf4j
  10. public class ExecutorInterceptor implements Interceptor {
  11. @Override
  12. public Object intercept(Invocation invocation) throws Throwable {
  13. MappedStatement mappedStatement = (MappedStatement) invocation.getTarget();
  14. log.info("==> ExecutorInterceptor: {}", mappedStatement.getId());
  15. return invocation.proceed();
  16. }
  17. @Override
  18. public Object plugin(Object target) {
  19. return Plugin.wrap(target, this);
  20. }
  21. @Override
  22. public void setProperties(Properties properties) {
  23. }
  24. }

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

  1. import lombok.extern.slf4j.Slf4j;
  2. import org.apache.ibatis.executor.Executor;
  3. import org.apache.ibatis.mapping.BoundSql;
  4. import org.apache.ibatis.mapping.MappedStatement;
  5. import org.apache.ibatis.plugin.*;
  6. import org.apache.ibatis.session.ResultHandler;
  7. import org.apache.ibatis.session.RowBounds;
  8. import org.springframework.stereotype.Component;
  9. import java.util.Properties;
  10. @Intercepts({
  11. @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
  12. Object.class}),
  13. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
  14. Object.class,
  15. RowBounds.class,
  16. ResultHandler.class})})
  17. @Component
  18. @Slf4j
  19. public class ExecutorInterceptor implements Interceptor {
  20. @Override
  21. public Object intercept(Invocation invocation) throws Throwable {
  22. MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
  23. String id = mappedStatement.getId();
  24. String className = id.substring(0, id.lastIndexOf('.'));
  25. String methodName = id.substring(id.lastIndexOf('.') + 1);
  26. BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
  27. Object parameterObject = boundSql.getParameterObject();
  28. log.info("==> id: {}",id);
  29. log.info("==> ClassName: {}", className);
  30. log.info("==> MethodName: {}", methodName);
  31. log.info("==> SQL语句: {}", boundSql.getSql());
  32. log.info("==> 参数: {}", parameterObject);
  33. return invocation.proceed();
  34. }
  35. @Override
  36. public Object plugin(Object target) {
  37. return Plugin.wrap(target, this);
  38. }
  39. @Override
  40. public void setProperties(Properties properties) {
  41. Interceptor.super.setProperties(properties);
  42. }
  43. }

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

拦截SQL执行的最终结果

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

  1. @Slf4j
  2. @Component
  3. @Intercepts(
  4. {@Signature(
  5. type = ResultSetHandler.class,
  6. method = "handleResultSets",
  7. args = {Statement.class}
  8. )})
  9. public class ResultMapCaseInterceptor implements Interceptor {
  10. @Override
  11. public Object intercept(Invocation invocation) throws Throwable {
  12. ResultSetHandler resultSetHandler = (ResultSetHandler) invocation.getTarget();
  13. MetaObject metaResultSetHandler = MetaObject.forObject(resultSetHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
  14. MappedStatement mappedStatement = (MappedStatement) metaResultSetHandler.getValue("mappedStatement");
  15. Object returnValue = invocation.proceed();
  16. List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  17. String resultMapTypeName = resultMaps.get(0).getType().getName();
  18. String MAP = "java.util.Map";
  19. String HASH_MAP = "java.util.HashMap";
  20. String LINKED_HASH_MAP = "java.util.LinkedHashMap";
  21. if(StrUtil.equalsIgnoreCase(MAP, resultMapTypeName) ||
  22. StrUtil.equalsIgnoreCase(HASH_MAP, resultMapTypeName) ||
  23. StrUtil.equalsIgnoreCase(LINKED_HASH_MAP, resultMapTypeName)){
  24. if(returnValue instanceof List<?> list){
  25. if(CollectionUtil.isNotEmpty(list)) {
  26. list.forEach(item -> {
  27. Map<String, Object> map = (Map<String, Object>) item;
  28. Map<String, Object> newMap = new LinkedHashMap<>();
  29. map.forEach((k, v) -> {
  30. newMap.put(k.toUpperCase(), v);
  31. });
  32. map.clear();
  33. map.putAll(newMap);
  34. });
  35. }
  36. }
  37. }
  38. return returnValue;
  39. }
  40. @Override
  41. public Object plugin(Object target) {
  42. return Interceptor.super.plugin(target);
  43. }
  44. @Override
  45. public void setProperties(Properties properties) {
  46. }
  47. }

使用JSQLparser解析SQL语句

简述及安装

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

官方网站:JSQLParser 4.9 documentation 

Github:JSQLParser/JSqlParser(github.com)

Maven:

  1. <dependency>
  2. <groupId>com.github.jsqlparser</groupId>
  3. <artifactId>jsqlparser</artifactId>
  4. <version>4.9</version>
  5. </dependency>

Gradle/KT:

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

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

  1. @Test
  2. public void Test2() throws JSQLParserException, ParseException {
  3. String originalSql = "SELECT " +
  4. "t1.id, " +
  5. "t1.name, " +
  6. "SUM(t2.amount) AS total_amount, " +
  7. "(SELECT COUNT(*) FROM orders o WHERE o.customer_id = t1.id) AS order_count " +
  8. "FROM customers t1 " +
  9. "JOIN orders t2 ON t1.id = t2.customer_id " +
  10. "LEFT JOIN payments t3 ON t2.id = t3.order_id " +
  11. "WHERE t1.status = 'active' " +
  12. "AND t2.order_date BETWEEN '2023-01-01' AND '2023-12-31' " +
  13. "GROUP BY t1.id, t1.name " +
  14. "HAVING SUM(t2.amount) > 1000 " +
  15. "ORDER BY total_amount DESC, t1.name ASC;";
  16. CCJSqlParser parser = CCJSqlParserUtil.newParser(originalSql);
  17. Statement statement = parser.Statement();
  18. parser.getASTRoot().jjtAccept(sqlModifier, null);
  19. log.info("==> JsqlParser SQL: {}", statement.toString());
  20. Select selectStatement = (Select) statement;
  21. PlainSelect plainSelect = selectStatement.getPlainSelect();
  22. Table table = (Table) plainSelect.getFromItem();
  23. System.out.println(table.toString());
  24. //获取表名们
  25. Set<String> tableNames = TablesNamesFinder.findTables(originalSql);
  26. System.out.println("表名们:"+tableNames);
  27. // Print SELECT clause
  28. System.out.println("SELECT clause: " + plainSelect.getSelectItems());
  29. // Print FROM clause
  30. System.out.println("FROM clause: " + plainSelect.getFromItem());
  31. // Print JOIN clauses
  32. if (plainSelect.getJoins() != null) {
  33. for (Join join : plainSelect.getJoins()) {
  34. System.out.println("JOIN clause: " + join);
  35. }
  36. }
  37. // Print WHERE clause
  38. System.out.println("WHERE clause: " + plainSelect.getWhere());
  39. // Print GROUP BY clause
  40. System.out.println("GROUP BY clause: " + plainSelect.getGroupBy());
  41. // Print HAVING clause
  42. System.out.println("HAVING clause: " + plainSelect.getHaving());
  43. // Print ORDER BY clause
  44. System.out.println("ORDER BY clause: " + plainSelect.getOrderByElements());
  45. }
直接使用

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

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

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

 获取各种子元素
  1. String originalSql = "SELECT rrr.* FROM rel_role_resource rrr " +
  2. "JOIN rel_role_user rru ON rrr.role_id = rru.role_id " +
  3. "JOIN `user` u ON u.id = rru.user_id " +
  4. "AND rrr.org_id = ? " +
  5. "AND rrr.role_id = rru.role_id " +
  6. "AND u.id = 123";
  7. CCJSqlParser parser = CCJSqlParserUtil.newParser(originalSql);
  8. Statement statement = parser.Statement();
  9. Select select = (Select) statement;
  10. log.info("==> Select: {}", select.toString());
  11. PlainSelect plain = select.getPlainSelect();
  12. plain.getSelectItems().forEach(selectItem -> {
  13. log.info("==> 查询的字段: {}", selectItem.toString());
  14. });
  15. if (plain.getFromItem() != null) {
  16. log.info("==> 查询的表: {}", plain.getFromItem().toString());
  17. }
  18. if (plain.getWhere() != null){
  19. log.info("==> 查询的条件: {}", plain.getWhere().toString());
  20. }
  21. if(CollectionUtil.isNotEmpty(plain.getJoins())){
  22. plain.getJoins().forEach(join -> {
  23. join.getOnExpressions().forEach(expression -> {
  24. log.info("==> Join 条件: {}", expression.toString());
  25. });
  26. log.info("==> Join: {}", join.toString());
  27. });
  28. }
  29. TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
  30. Set<String> tableList = tablesNamesFinder.getTables(statement);
  31. tableList.forEach(table -> {
  32. log.info("==> 查询的表: {}", table);
  33. });
  34. log.info("==> 查询的表: {}", tableList);

运行后结果:

该文未完结....持续更新 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/寸_铁/article/detail/735085
推荐阅读
相关标签
  

闽ICP备14008679号