一、在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;
}
}