最近項目中的一個需求:涉及到數據同步,由於環境原因導致只能通過webservice同步執行的sql語句,故需要獲取數據庫實際執行的增刪改sql語句到sql_log表中以同步。
編寫mybatis插件攔截sql語句
package net.mshome.twisted.tmall.interceptor;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.enums.IEnum;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
import lombok.extern.slf4j.Slf4j;
import net.mshome.twisted.tmall.entity.SqlLog;
import net.mshome.twisted.tmall.service.ISqlLogService;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.util.CollectionUtils;
import java.sql.Connection;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
/**
* 自定義mybatis插件,實現輸出實際執行sql語句
*
* @author [email protected]
* @date 2019-08-18
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
@Slf4j
public class MybatisSqlInterceptor extends AbstractSqlParserHandler implements Interceptor {
/**
* 獲取配置中需要攔截的表
*/
@Value("#{'${tmall.sync.tables:}'.split(',')}")
private List<String> tableNames;
@Lazy
@Autowired
private ISqlLogService sqlLogService;
/**
* 忽略插入sql_log表的語句
*/
private static final String IGNORE_SQL_PREFIX = "insert into sql_log";
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (CollectionUtils.isEmpty(tableNames)) {
return invocation.proceed();
}
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
String sql = boundSql.getSql().replaceAll("\\s+", " ").toLowerCase();
if (sql.toLowerCase().startsWith(IGNORE_SQL_PREFIX)) {
return invocation.proceed();
}
List<ParameterMapping> parameterMappings = new ArrayList<>(boundSql.getParameterMappings());
Object parameterObject = boundSql.getParameterObject();
if (parameterMappings.isEmpty() && parameterObject == null) {
log.warn("parameterMappings is empty or parameterObject is null");
return invocation.proceed();
}
Configuration configuration = mappedStatement.getConfiguration();
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
try {
this.sqlParser(metaObject);
String parameter = "null";
MetaObject newMetaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() == ParameterMode.OUT) {
continue;
}
String propertyName = parameterMapping.getProperty();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
parameter = getParameterValue(parameterObject);
} else if (newMetaObject.hasGetter(propertyName)) {
parameter = getParameterValue(newMetaObject.getValue(propertyName));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
parameter = getParameterValue(boundSql.getAdditionalParameter(propertyName));
}
// fixme 此處不嚴謹,若sql語句中有❓,則替換錯位。🤔️
sql = sql.replaceFirst("\\?", parameter);
}
// 將攔截到的sql語句插入日誌表中
sqlLogService.save(SqlLog.builder().executedSql(sql).build());
} catch (Exception e) {
log.error(String.format("intercept sql error: [%s]", sql), e);
}
return invocation.proceed();
}
/**
* 獲取參數
*
* @param param Object類型參數
* @return 轉換之後的參數
*/
private static String getParameterValue(Object param) {
if (param == null) {
return "null";
}
if (param instanceof Number) {
return param.toString();
}
String value = null;
if (param instanceof String) {
value = param.toString();
} else if (param instanceof Date) {
DateUtil.format((Date) param, "yyyy-MM-dd HH:mm:ss");
} else if (param instanceof IEnum) {
value = String.valueOf(((IEnum) param).getValue());
} else {
value = param.toString();
}
return StringUtils.quotaMark(value);
}
@Override
public Object plugin(Object o) {
if (o instanceof StatementHandler) {
return Plugin.wrap(o, this);
}
return o;
}
@Override
public void setProperties(Properties properties) {
}
}
1、mybatis插件通過攔截起方式實現,此處實現一個攔截器,攔截StatementHandler類中2個方法。
2、類似的還有mybatis-plus自帶的分頁插件,可參考Interceptor接口的實現類。