jeesite mybatis添加拦截器,推送指定表的变更数据到其他数据库

一、在mybatis-config.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>

    <!-- 全局参数 -->
    <settings>
        <!-- 使全局的映射器启用或禁用缓存。 -->
        <setting name="cacheEnabled" value="true"/>

        <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
        <setting name="lazyLoadingEnabled" value="true"/>

        <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
        <setting name="aggressiveLazyLoading" value="true"/>

        <!-- 是否允许单条sql 返回多个数据集  (取决于驱动的兼容性) default:true -->
        <setting name="multipleResultSetsEnabled" value="true"/>

        <!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
        <setting name="useColumnLabel" value="true"/>

        <!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。  default:false  -->
        <setting name="useGeneratedKeys" value="false"/>

        <!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不映射 PARTIAL:部分  FULL:全部  -->
        <setting name="autoMappingBehavior" value="PARTIAL"/>

        <!-- 这是默认的执行类型(SIMPLE: 简单;REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新)  -->
        <setting name="defaultExecutorType" value="SIMPLE"/>

        <!-- 使用驼峰命名法转换字段。 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>

        <!-- 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
        <setting name="localCacheScope" value="SESSION"/>

        <!-- 设置但JDBC类型为空时,某些驱动程序 要指定值,default:OTHER,插入空值时不需要指定类型 -->
        <setting name="jdbcTypeForNull" value="NULL"/>

        <!-- 迭代集合的时候如果空值,则忽略而不抛出异常 -->
        <setting name="nullableOnForEach" value="true"/>

        <!-- 返回值为Map时,当返回空值字段时,仍然需要返回这个Key -->
        <setting name="callSettersOnNulls" value="true"/>

    </settings>

    <!-- 类型别名 -->
    <typeAliases>
        <typeAlias alias="Page" type="com.jeesite.common.entity.Page" /><!--分页  -->
    </typeAliases>

    <!-- 插件配置 -->
    <plugins>
        <plugin interceptor="com.jeesite.common.mybatis.interceptor.DataSourceInterceptor" />
        <plugin interceptor="com.jeesite.common.mybatis.interceptor.PaginationInterceptor" />
        <plugin interceptor="com.jeesite.modules.station.entity.SqlExtractInterceptor"/>
    </plugins>

</configuration>

二、新增拦截器SqlExtractInterceptor

复制代码
package com.jeesite.modules.station.entity;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.jeesite.modules.station.entity.CrossDbExecutor.executeInAnotherDb;

@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})
})
public class SqlExtractInterceptor implements Interceptor {

    // ==== 新增:目标监听表名(可通过配置文件注入)====
    private Set<String> targetTables = new HashSet<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        BoundSql boundSql = ms.getBoundSql(parameter);
        String sql = boundSql.getSql();
        Object[] params = getParameters(boundSql, parameter);

        // ==== 新增:1. 过滤非修改操作(只处理 INSERT/UPDATE/DELETE)====
        SqlCommandType commandType = ms.getSqlCommandType();
        if (commandType != SqlCommandType.INSERT
                && commandType != SqlCommandType.UPDATE
                && commandType != SqlCommandType.DELETE) {
            return invocation.proceed(); // 非修改操作,直接放行
        }

        // ==== 新增:2. 提取 SQL 中的表名并匹配目标表 ====
        String tableName = extractTableName(sql, commandType);
        if (tableName == null || !targetTables.contains(tableName.toLowerCase())) {
            return invocation.proceed(); // 表名不匹配,直接放行
        }

        // ==== 原有逻辑:仅当上述条件满足时执行跨库操作 ====
        executeInAnotherDb(sql, params);

        return invocation.proceed();
    }

    // ==== 新增:从 SQL 中提取表名 ====
    private String extractTableName(String sql, SqlCommandType commandType) {
        String lowerSql = sql.toLowerCase().trim();
        Pattern pattern = null;

        // 根据操作类型匹配表名(简单处理,可根据实际 SQL 复杂度增强)
        switch (commandType) {
            case INSERT:
                // 新增:支持反引号包裹的表名(如 `js_sys_log`)
                pattern = Pattern.compile("insert\\s+into\\s+`?([a-zA-Z0-9_]+)`?");
                break;
            case UPDATE:
                // 保持不变(或同步增强,如支持反引号)
                pattern = Pattern.compile("update\\s+`?([a-zA-Z0-9_]+)`?");
                break;
            case DELETE:
                // 保持不变(或同步增强,如支持反引号)
                pattern = Pattern.compile("delete\\s+from\\s+`?([a-zA-Z0-9_]+)`?");
                break;
            default:
                return null;
        }

        Matcher matcher = pattern.matcher(lowerSql);
        if (matcher.find()) {
            return matcher.group(1); // 返回匹配到的表名
        }
        return null;
    }

    // ==== 新增:允许通过配置文件设置目标表(如 mybatis-config.xml 或 Spring 配置)====
    @Override
    public void setProperties(Properties properties) {
//        String tables = properties.getProperty("targetTables");
        String tables = "jk_station_config,js_sys_log";
        if (tables != null && !tables.isEmpty()) {
            targetTables.addAll(Arrays.asList(tables.split(",")));
        }
    }

    private Object[] getParameters(BoundSql boundSql, Object parameter) {
        if (parameter == null) {
            return new Object[0]; // 无参数时返回空数组
        }

        // 1. 处理数组类型参数
        if (parameter.getClass().isArray()) {
            return (Object[]) parameter;
        }

        // 2. 处理集合类型参数(List/Set等)
        if (parameter instanceof Collection) {
            return ((Collection<?>) parameter).toArray(new Object[0]);
        }

        // 3. 处理MyBatis参数包装(重点修复:支持命名参数和StrictMap)
        if (parameter instanceof Map) {
            Map<?, ?> paramMap = (Map<?, ?>) parameter;

            // 3.1 处理MyBatis内部StrictMap(多参数场景)
            if (paramMap.containsKey("__frch_item_0")) { // 迭代参数标记(如foreach)
                List<Object> paramList = new ArrayList<>();
                for (Object key : paramMap.keySet()) {
                    if (key.toString().startsWith("__frch_")) {
                        paramList.add(paramMap.get(key));
                    }
                }
                return paramList.toArray(new Object[0]);
            }

            // 3.2 处理命名参数(@Param注解)或param1,param2...格式
            List<Object> paramList = new ArrayList<>();
            if (paramMap.containsKey("param1")) {
                // 按param1,param2...顺序提取
                int i = 1;
                while (paramMap.containsKey("param" + i)) {
                    paramList.add(paramMap.get("param" + i));
                    i++;
                }
            } else {
                // 按参数名排序后提取(确保顺序一致)
                List<String> sortedKeys = new ArrayList<>(paramMap.keySet().stream()
                        .map(Object::toString)
                        .collect(Collectors.toList()));
                Collections.sort(sortedKeys);
                for (String key : sortedKeys) {
                    paramList.add(paramMap.get(key));
                }
            }
            return paramList.toArray(new Object[0]);
        }

        // ===== 新增:处理实体对象参数 =====
        // 判断是否为自定义实体(排除Java内置类型)
        if (isCustomEntity(parameter)) {
            try {
//                List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
                List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
                List<Object> paramValues = new ArrayList<>();

                // 遍历SQL参数映射,提取实体属性值
                for (ParameterMapping pm : parameterMappings) {
                    String propertyName = pm.getProperty();
                    if (propertyName == null || propertyName.isEmpty()) {
                        continue;
                    }

                    // 通过反射获取实体属性值
                    PropertyDescriptor pd = new PropertyDescriptor(propertyName, parameter.getClass());
                    Method getter = pd.getReadMethod();
                    Object value = getter.invoke(parameter);
                    paramValues.add(value);
                }
                return paramValues.toArray(new Object[0]);
            } catch (Exception e) {
                // 反射失败时降级为原逻辑(避免阻断执行)
                System.err.println("实体参数解析失败: " + e.getMessage());
            }
        }

        // 4. 单个实体对象直接包装为数组
        return new Object[]{parameter};
    }

    // 判断是否为自定义实体(排除Java内置类型和MyBatis包装类型)
    private boolean isCustomEntity(Object parameter) {
        Class<?> clazz = parameter.getClass();
        // 排除基本类型、包装类型、字符串、日期等内置类型
        if (clazz.isPrimitive() ||
                clazz.getName().startsWith("java.") ||
                clazz.getName().startsWith("javax.") ||
                parameter instanceof Number ||
                parameter instanceof Boolean ||
                parameter instanceof Character) {
            return false;
        }
        // 排除MyBatis内部参数对象
        if (clazz.getName().startsWith("org.apache.ibatis")) {
            return false;
        }
        return true;
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

}

此处修改要推送的表:

三、新增JDBC连接器

复制代码
package com.jeesite.modules.station.entity;

import java.sql.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import lombok.extern.log4j.Log4j2;

@Log4j2
public class CrossDbExecutor {
    // 目标数据库连接信息(建议通过配置文件读取)
    private static final String TARGET_JDBC_URL = "jdbc:mysql://127.0.0.1:3306/datang_znaf_jk?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true";
    private static final String TARGET_USERNAME = "root";
    private static final String TARGET_PASSWORD = "123456";

    public static void executeInAnotherDb(String sql, Object[] params) {
        if (sql == null || sql.trim().isEmpty()) {
            return;
        }

        // ==== 新增:校验参数数量与SQL占位符数量是否匹配 ====
        int placeholderCount = countPlaceholders(sql);
        int paramCount = (params == null) ? 0 : params.length;
        if (paramCount > 0 && placeholderCount == 0) {
            System.err.println("警告:SQL无参数占位符,但传入了" + paramCount + "个参数,已跳过执行");
            return; // 无占位符时不执行参数设置
        }
        if (paramCount != placeholderCount) {
            System.err.println("警告:参数数量(" + paramCount + ")与占位符数量(" + placeholderCount + ")不匹配,已跳过执行");
            return;
        }

        log.info("执行跨库SQL: {},参数: {}", sql, params);
        try (Connection conn = DriverManager.getConnection(TARGET_JDBC_URL, TARGET_USERNAME, TARGET_PASSWORD);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            // 设置参数(仅当参数不为空且有占位符时)
            if (params != null && params.length > 0 && placeholderCount > 0) {
                for (int i = 0; i < params.length; i++) {
                    pstmt.setObject(i + 1, params[i]); // 参数索引从1开始
                }
            }

            // 执行SQL(根据类型选择执行方法)
            if (sql.trim().toLowerCase().startsWith("select")) {
                try (ResultSet rs = pstmt.executeQuery()) {
                    // 处理查询结果(按需实现)
                }
            } else {
                int affectedRows = pstmt.executeUpdate();
            }

        } catch (SQLException e) {
//            throw new RuntimeException("跨库执行SQL失败: " + e.getMessage(), e);
            log.info("跨库执行SQL失败: {}", e.getMessage());
        }
        log.info("跨库执行SQL成功: {}", sql);
    }

    // ==== 新增:统计SQL中的参数占位符数量(支持标准?占位符) ====
    private static int countPlaceholders(String sql) {
        // 正则匹配SQL中的?占位符(排除字符串中的?)
        Pattern pattern = Pattern.compile("(?<!')\\?(?!')");
        Matcher matcher = pattern.matcher(sql);
        int count = 0;
        while (matcher.find()) {
            count++;
        }
        return count;
    }
}
相关推荐
api_180079054604 小时前
异步数据采集实践:用 Python/Node.js 构建高并发淘宝商品 API 调用引擎
大数据·开发语言·数据库·数据挖掘·node.js
lunz_fly19924 小时前
【源码解读之 Mybatis】【核心篇】-- 第6篇:StatementHandler语句处理器
mybatis
怕什么真理无穷4 小时前
mysql server 9.4 windows安装教程(sqlyog 下载)
数据库
Olrookie4 小时前
MySQL运维常用SQL
运维·数据库·sql·mysql·dba
lunzi_fly4 小时前
【源码解读之 Mybatis】【核心篇】-- 第6篇:StatementHandler语句处理器
mybatis
数据库生产实战5 小时前
ORACLE 19C ADG环境 如何快速删除1.8TB的分区表?有哪些注意事项?
数据库·oracle
blackorbird5 小时前
使用 Overpass Turbo 查找监控摄像头
运维·服务器·数据库·windows
IT永勇5 小时前
SQLite数据库基本操作
数据库·sqlite·嵌入式开发·增删改查·关系型数据库
洋不写bug5 小时前
数据库的创建,查看,修改,删除,字符集编码和校验操作
android·数据库·adb