摘要:本文主要介绍了如何使用Mybatis-Plus
的 数据变动记录插件 来记录我们的业务操作日志,下文主要改造了DataChangeRecorderInnerInterceptor
拦截器,插入了我们业务实际的一些操作,可以让我们更加方便的使用,项目采用SpringBoot + Mybatis-Plus
。
简介
官方的
DataChangeRecorderInnerInterceptor
插件会拦截每一条insert/update/delete
语句,而我们的业务中,我们只需要关注我们需要拦截的业务,所以在该插件的基础上定制了实际的业务场景代码,并且输出的变更语句对象更加容易后期扩展。
- 只针对单表的
insert/update/delete
拦截,复杂sql
不支持。 update/delete
操作如果主键不存在,可能会导致性能损耗严重。- 批量插入、更新数据不支持,需要改为
for
循环执行。
详细代码
测试数据库表
定义了两张表用于测试业务
sql
CREATE TABLE `userinfo` (
`id` bigint(20) NOT NULL,
`code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`money` decimal(15,4) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `sub` (
`id` bigint(20) NOT NULL,
`parent_id` bigint(11) DEFAULT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`num` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
操作日志数据库表
如果是单系统,可以放在一起,但是最好通过
MQ
解耦,操作日志单独在一个系统中,本案例是放在一起的,用于简单演示。
sql
CREATE TABLE `operate_log` (
`id` bigint(20) NOT NULL,
`trace_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '业务追踪ID',
`domain` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '归属领域',
`type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作类型',
`table_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作表名',
`table_desc` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作表描述',
`data_id` bigint(20) DEFAULT NULL COMMENT '数据id',
`db_operation` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据库操作类型',
`cost` bigint(20) DEFAULT NULL COMMENT '本次拦截时间消耗(ms)',
`generate_time` datetime(3) DEFAULT NULL COMMENT '拦截的数据生成时间',
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志';
CREATE TABLE `operate_log_item` (
`id` bigint(20) NOT NULL,
`operate_log_id` bigint(20) DEFAULT NULL COMMENT '操作日志ID',
`column_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '列名称',
`column_desc` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '列描述',
`original_value` text COLLATE utf8mb4_unicode_ci COMMENT '原始值',
`update_value` text COLLATE utf8mb4_unicode_ci COMMENT '更新后的值',
PRIMARY KEY (`id`),
KEY `idx_oli` (`operate_log_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
基础Entity/Mapper
相关
UserinfoEntity
less
@ApiModel(value = "用户信息")
@Data
@TableName("userinfo")
public class UserinfoEntity {
@ApiModelProperty(value = "id")
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@ApiModelProperty(value = "编码")
private String code;
@ApiModelProperty(value = "名称")
private String name;
private BigDecimal money;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
SubEntity
kotlin
@Data
@TableName("sub")
public class SubEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@ApiModelProperty(value = "父id")
private Long parentId;
@ApiModelProperty(value = "名称")
private String name;
@ApiModelProperty(value = "数量")
private Integer num;
}
OperateLogEntity
less
@OperateLogIgnore
@ApiModel(value = "操作日志表")
@Data
@TableName("operate_log")
public class OperateLogEntity {
/** 数据ID */
private Long id;
/** 追踪ID */
private String traceId;
/** 归属领域 */
private String domain;
/** 操作类型 */
private String type;
/** 表名称 */
private String tableName;
/** 表描述 */
private String tableDesc;
/** 数据ID */
private String dataId;
/** 数据库操作类型 */
private String dbOperation;
/** 花费时间毫秒 */
private Long cost;
/** 数据生成的时间 */
private Date generateTime;
/** 创建时间 */
private Date createTime;
}
OperateLogItemEntity
less
@OperateLogIgnore
@Data
@TableName("operate_log_item")
public class OperateLogItemEntity {
private Long id;
private Long operateLogId;
/** 数据库列字段 */
private String columnName;
/** 字段描述 */
private String columnDesc;
/** 原始值 */
private String originalValue;
/** 更新后的值 */
private String updateValue;
}
UserinfoMapper
less
@Repository
public interface UserinfoMapper extends BaseMapper<UserinfoEntity> {
@Update("update userinfo set code = #{code} where id = #{id}")
void complexUpdate(@Param("code") String code,@Param("id") Long id);
@Update("update userinfo,sub set userinfo.money = sub.num where userinfo.id = sub.parent_id and userinfo.id = #{id}")
void complexUpdate2(@Param("id") Long id);
}
SubMapper
java
@Repository
public interface SubMapper extends BaseMapper<SubEntity> {
}
OperateLogMapper
java
@Repository
public interface OperateLogMapper extends BaseMapper<OperateLogEntity> {
}
OperateLogItemMapper
java
@Repository
public interface OperateLogItemMapper extends BaseMapper<OperateLogItemEntity> {
}
定义注解
OperateLog
操作日志注解
只有
Controller
上的方法加了该注解才记录后面相关的操作日志。
less
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
/**
* 操作类型
* Alias for the type();
* @return
*/
String value();
/**
* 业务领域
* @return
*/
String domain() default "";
/**
* 操作类型
* @return
*/
String type() default "";
}
OperateLogIgnore
忽略记录的表注解
加了该注解后,就不会存储该表数据变动的日志。
less
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLogIgnore {
}
dto
定义
用于该业务上下文的
dto
OperateLogContextDto
业务上下文DTO
该
dto
配合@OperateLog
注解使用的,有了该注解,上下文才会存储这个对象。
arduino
@Data
public class OperateLogContextDto {
/** 调用链路ID */
private String traceId;
/** 归属领域 */
private String domain;
/** 操作类型 */
private String type;
}
TableInfoDto
表结构信息
该类存储了我们定义的
Entity
对象的表描述和字段描述信息
- 表名描述用
@ApiModel(value = "用户信息")
- 属性描述用
@ApiModelProperty(value = "编码")
这样就能把实体对应的描述存储起来。
arduino
@Data
public class TableInfoDto {
/** 表名称 */
private String tableName;
/** 表描述 */
private String tableDesc;
/** 是否忽略表 */
private Boolean ignore = Boolean.FALSE;
/**
* 属性map
* key=表的属性名称 value=属性描述
*/
private Map<String, String> columnMap = new LinkedHashMap<>();
}
数据变动相关DTO
DataChangeColumnResult
列变动记录
记录了列变动的先后值
arduino
@Data
public class DataChangeColumnResult {
/** 数据库列字段 */
private String columnName;
/** 字段描述 */
private String columnDesc;
/** 原始值 */
private String originalValue;
/** 更新后的值 */
private String updateValue;
}
DataChangeResult
数据行变动记录
记录一行数据的变动情况
ruby
@Data
public class DataChangeResult {
/** id */
private String id;
/** 其他的列变动情况 */
private List<DataChangeColumnResult> changeColumnResults;
}
OperateLogResult
每一次数据库操作影响的数据行和操作集合
arduino
@Data
public class OperateLogResult {
/** 上下文信息 */
private OperateLogContextDto context;
/** 数据库操作类型 */
private String operation;
/** 数据记录状态 */
private boolean recordStatus;
/** 操作的表名称 */
private String tableName;
/** 表描述 */
private String tableDesc;
/** 数据变更记录 */
private List<DataChangeResult> dataChangeResults = new ArrayList<>();
/**
* cost for this plugin, ms
*/
private long cost;
/** 数据构建的时间 */
private long generateTime = System.currentTimeMillis();
}
OperateLogContextHolder
上下文信息持有者
controller
的方法上有@OperateLog
注解,则会通过拦截器放入操作信息到该上下文中。
csharp
public class OperateLogContextHolder {
private static final ThreadLocal<OperateLogContextDto> operateTraceDtoContext = new ThreadLocal<>();
/**
* 设置上下文
* @param operateLogContextDto
*/
public static void set(OperateLogContextDto operateLogContextDto){
if(null != operateLogContextDto){
operateTraceDtoContext.set(operateLogContextDto);
}
}
/**
* 获取上下文
* @return
*/
public static OperateLogContextDto get(){
return operateTraceDtoContext.get();
}
/**
* 清除上下文
*/
public static void clear(){
operateTraceDtoContext.remove();
}
}
SpringEvent
事件定义
本方案采用的是事件解耦
OperateLogTransactionEvent
带事务的事件对象
scala
@Data
public class OperateLogTransactionEvent extends ApplicationEvent {
private static final long serialVersionUID = 4675816574446023168L;
private OperateLogResult operateLogResult;
public OperateLogTransactionEvent(Object source, OperateLogResult operateLogResult) {
super(source);
this.operateLogResult = operateLogResult;
}
}
OperateLogNormalEvent
普通事件对象
scala
@Data
public class OperateLogNormalEvent extends ApplicationEvent {
private static final long serialVersionUID = 2795119755419728981L;
private OperateLogResult operateLogResult;
public OperateLogNormalEvent(Object source, OperateLogResult operateLogResult) {
super(source);
this.operateLogResult = operateLogResult;
}
}
事件监听者
用来消费事件消息,做操作日志最后的存储
less
@Slf4j
@Component
public class OperateLogEventListener {
@Autowired
private OperateLogMapper operateLogMapper;
@Autowired
private OperateLogItemMapper operateLogItemMapper;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void transaction(OperateLogTransactionEvent operateLogTransactionEvent) {
// 在这里编写你想在事务提交后执行的代码
log.info(JSON.toJSONString(operateLogTransactionEvent));
this.saveLog(operateLogTransactionEvent.getOperateLogResult());
}
@EventListener
public void normal(OperateLogNormalEvent operateLogNormalEvent){
// 在这里编写你想在事务提交后执行的代码
log.info(JSON.toJSONString(operateLogNormalEvent));
this.saveLog(operateLogNormalEvent.getOperateLogResult());
}
private void saveLog(OperateLogResult operateLogResult){
for (DataChangeResult dataChangeResult : operateLogResult.getDataChangeResults()) {
OperateLogEntity operateLogEntity = new OperateLogEntity();
operateLogEntity.setId(IdWorker.getId());
operateLogEntity.setTraceId(operateLogResult.getContext().getTraceId());
operateLogEntity.setDomain(operateLogResult.getContext().getDomain());
operateLogEntity.setType(operateLogResult.getContext().getType());
operateLogEntity.setTableName(operateLogResult.getTableName());
operateLogEntity.setTableDesc(operateLogResult.getTableDesc());
operateLogEntity.setDbOperation(operateLogResult.getOperation());
operateLogEntity.setCost(operateLogResult.getCost());
operateLogEntity.setGenerateTime(new Date(operateLogResult.getGenerateTime()));
operateLogEntity.setDataId(dataChangeResult.getId());
operateLogEntity.setCreateTime(new Date());
operateLogMapper.insert(operateLogEntity);
for (DataChangeColumnResult changeColumnResult : dataChangeResult.getChangeColumnResults()) {
OperateLogItemEntity operateLogItemEntity = new OperateLogItemEntity();
operateLogItemEntity.setId(IdWorker.getId());
operateLogItemEntity.setOperateLogId(operateLogEntity.getId());
operateLogItemEntity.setColumnName(changeColumnResult.getColumnName());
operateLogItemEntity.setColumnDesc(changeColumnResult.getColumnDesc());
operateLogItemEntity.setOriginalValue(changeColumnResult.getOriginalValue());
operateLogItemEntity.setUpdateValue(changeColumnResult.getUpdateValue());
operateLogItemMapper.insert(operateLogItemEntity);
}
}
}
}
mvc
拦截器配置
OperateLogWebInterceptor
判断
controller
的方法上是否有@OperateLog
注解,然后存入上下文中
java
public class OperateLogWebInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(handler instanceof HandlerMethod){
Method method = ((HandlerMethod) handler).getMethod();
OperateLog operateLog = method.getAnnotation(OperateLog.class);
String operateType = this.buildOperateType(operateLog);
if(StrUtil.isNotBlank(operateType)){
OperateLogContextDto operateLogContextDto = new OperateLogContextDto();
operateLogContextDto.setTraceId(UUID.randomUUID().toString());
operateLogContextDto.setDomain(operateLog.domain());
operateLogContextDto.setType(operateType);
OperateLogContextHolder.set(operateLogContextDto);
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
OperateLogContextHolder.clear();
}
/**
* 构建操作类型
* @param operateLog
* @return
*/
private String buildOperateType(OperateLog operateLog){
if(null == operateLog){
return null;
}
if(StrUtil.isNotBlank(operateLog.value())){
return operateLog.value();
}
return operateLog.type();
}
}
OperateLogMvcConfig
配置拦截器在
SpringBoot
中生效
typescript
@Configuration
public class OperateLogMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new OperateLogWebInterceptor());
}
}
Mybatis-Plus
数据拦截相关
OperateLogTableInfoHelper
用来存储表的结构信息工具类
scss
@Slf4j
public class OperateLogTableInfoHelper {
private static final Map<String, TableInfoDto> TABLE_INFO_DTO_MAP = new ConcurrentHashMap<>();
/**
* 获取表结构信息
* @param tableName
* @return
*/
public static TableInfoDto getTableInfoDto(String tableName){
if(!TABLE_INFO_DTO_MAP.containsKey(tableName)){
synchronized (tableName.intern()){
initTableInfoDto(tableName);
}
}
return TABLE_INFO_DTO_MAP.get(tableName);
}
/**
* 初始化表信息
* @param tableName
*/
private static void initTableInfoDto(String tableName){
if(TABLE_INFO_DTO_MAP.containsKey(tableName)){
return;
}
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
if(null == tableInfo){
log.error("数据库表不存在,tableName={}",tableName);
return;
}
TableInfoDto tableInfoDto = new TableInfoDto();
Map<String, String> columnMap = new LinkedHashMap<>();
tableInfoDto.setTableName(tableName);
tableInfoDto.setColumnMap(columnMap);
ApiModel apiModel = tableInfo.getEntityType().getAnnotation(ApiModel.class);
if(null != apiModel){
tableInfoDto.setTableDesc(apiModel.value());
}
OperateLogIgnore operateLogIgnore = tableInfo.getEntityType().getAnnotation(OperateLogIgnore.class);
if(null != operateLogIgnore){
tableInfoDto.setIgnore(true);
}
for (TableFieldInfo tableFieldInfo : tableInfo.getFieldList()) {
Field field = ReflectUtil.getField(tableInfo.getEntityType(), tableFieldInfo.getProperty());
if(null == field){
continue;
}
ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
String propertyName = null;
if(null != apiModelProperty){
propertyName = apiModelProperty.value();
}
columnMap.put(tableFieldInfo.getColumn(), propertyName);
}
TABLE_INFO_DTO_MAP.put(tableName, tableInfoDto);
}
}
OperateLogMybatisInnerInterceptor
数据拦截插件
主题内容来源于
DataChangeRecorderInnerInterceptor
插件,我在该基础上加上了业务操作
scss
public class OperateLogMybatisInnerInterceptor implements InnerInterceptor {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@SuppressWarnings("unused")
public static final String IGNORED_TABLE_COLUMN_PROPERTIES = "ignoredTableColumns";
private final Map<String, Set<String>> ignoredTableColumns = new ConcurrentHashMap<>();
private final Set<String> ignoreAllColumns = new HashSet<>();//全部表的这些字段名,INSERT/UPDATE都忽略,delete暂时保留
//批量更新上限, 默认一次最多1000条
private int BATCH_UPDATE_LIMIT = 1000;
private boolean batchUpdateLimitationOpened = false;
private final Map<String, Integer> BATCH_UPDATE_LIMIT_MAP = new ConcurrentHashMap<>();//表名->批量更新上限
private ApplicationEventPublisher publisher;
@Override
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
if(null == OperateLogContextHolder.get()){
return;
}
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
final BoundSql boundSql = mpSh.boundSql();
SqlCommandType sct = ms.getSqlCommandType();
if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
OperateLogResult operateLogResult;
long startTs = System.currentTimeMillis();
try {
Statement statement = JsqlParserGlobal.parse(mpBs.sql());
if (statement instanceof Insert) {
operateLogResult = processInsert((Insert) statement, mpSh.boundSql());
} else if (statement instanceof Update) {
operateLogResult = processUpdate((Update) statement, ms, boundSql, connection);
} else if (statement instanceof Delete) {
operateLogResult = processDelete((Delete) statement, ms, boundSql, connection);
} else {
logger.info("other operation sql={}", mpBs.sql());
return;
}
} catch (Exception e) {
if (e instanceof DataUpdateLimitationException) {
throw (DataUpdateLimitationException) e;
}
logger.error("Unexpected error for mappedStatement={}, sql={}", ms.getId(), mpBs.sql(), e);
return;
}
long costThis = System.currentTimeMillis() - startTs;
if (operateLogResult != null) {
operateLogResult.setCost(costThis);
dealOperationResult(operateLogResult);
}
}
}
/**
* 判断哪些SQL需要处理
* 默认INSERT/UPDATE/DELETE语句
*
* @param sql
* @return
*/
protected boolean allowProcess(String sql) {
String sqlTrim = sql.trim().toUpperCase();
return sqlTrim.startsWith("INSERT") || sqlTrim.startsWith("UPDATE") || sqlTrim.startsWith("DELETE");
}
/**
* 处理数据更新结果,默认打印
*
* @param operateLogResult
*/
protected void dealOperationResult(OperateLogResult operateLogResult) {
if(null == operateLogResult){
return;
}
this.fillOperationResult(operateLogResult);
if (TransactionSynchronizationManager.isActualTransactionActive()){
publisher.publishEvent(new OperateLogTransactionEvent(this, operateLogResult));
}else{
publisher.publishEvent(new OperateLogNormalEvent(this, operateLogResult));
}
}
/**
* 填充其他数据
* @param operateLogResult
*/
private void fillOperationResult(OperateLogResult operateLogResult){
TableInfoDto tableInfoDto = OperateLogTableInfoHelper.getTableInfoDto(operateLogResult.getTableName());
if(null == tableInfoDto){
return;
}
operateLogResult.setTableDesc(tableInfoDto.getTableDesc());
operateLogResult.getDataChangeResults().forEach(dataChangeResult -> {
dataChangeResult.getChangeColumnResults().forEach(dataChangeColumnResult -> {
dataChangeColumnResult.setColumnDesc(tableInfoDto.getColumnMap().get(dataChangeColumnResult.getColumnName().toLowerCase()));
if(dataChangeColumnResult.getColumnName().equalsIgnoreCase("id") && ObjectUtils.isEmpty(dataChangeResult.getId())){
dataChangeResult.setId(dataChangeColumnResult.getUpdateValue());
}
});
dataChangeResult.getChangeColumnResults().removeIf(v -> v.getColumnName().equalsIgnoreCase("id"));
});
operateLogResult.setContext(OperateLogContextHolder.get());
}
public OperateLogResult processInsert(Insert insertStmt, BoundSql boundSql) {
String operation = SqlCommandType.INSERT.name().toLowerCase();
Table table = insertStmt.getTable();
String tableName = table.getName();
if (checkOperateLogIgnore(tableName)) {
return null;
}
Optional<OperateLogResult> optionalOperationResult = ignoredTableColumns(tableName, operation);
if (optionalOperationResult.isPresent()) {
return optionalOperationResult.get();
}
OperateLogResult result = new OperateLogResult();
result.setOperation(operation);
result.setTableName(tableName);
result.setRecordStatus(true);
Map<String, Object> updatedColumnDatas = getUpdatedColumnDatas(tableName, boundSql, insertStmt);
result.setDataChangeResults(this.buildDataStr(compareAndGetUpdatedColumnDatas(result.getTableName(), null, updatedColumnDatas)));
return result;
}
/**
* 检查是否是忽略的表
* @param tableName
* @return
*/
private boolean checkOperateLogIgnore(String tableName) {
TableInfoDto tableInfoDto = OperateLogTableInfoHelper.getTableInfoDto(tableName);
if(null == tableInfoDto || tableInfoDto.getIgnore()){
return true;
}
return false;
}
public OperateLogResult processUpdate(Update updateStmt, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
Expression where = updateStmt.getWhere();
PlainSelect selectBody = new PlainSelect();
Table table = updateStmt.getTable();
String tableName = table.getName();
if (checkOperateLogIgnore(tableName)) {
return null;
}
String operation = SqlCommandType.UPDATE.name().toLowerCase();
Optional<OperateLogResult> optionalOperationResult = ignoredTableColumns(tableName, operation);
if (optionalOperationResult.isPresent()) {
return optionalOperationResult.get();
}
selectBody.setFromItem(table);
List<Column> updateColumns = new ArrayList<>();
for (UpdateSet updateSet : updateStmt.getUpdateSets()) {
updateColumns.addAll(updateSet.getColumns());
}
Columns2SelectItemsResult buildColumns2SelectItems = buildColumns2SelectItems(tableName, updateColumns);
selectBody.setSelectItems(buildColumns2SelectItems.getSelectItems());
selectBody.setWhere(where);
SelectItem<PlainSelect> plainSelectSelectItem = new SelectItem<>(selectBody);
BoundSql boundSql4Select = new BoundSql(mappedStatement.getConfiguration(), plainSelectSelectItem.toString(),
prepareParameterMapping4Select(boundSql.getParameterMappings(), updateStmt),
boundSql.getParameterObject());
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
Map<String, Object> additionalParameters = mpBoundSql.additionalParameters();
if (additionalParameters != null && !additionalParameters.isEmpty()) {
for (Map.Entry<String, Object> ety : additionalParameters.entrySet()) {
boundSql4Select.setAdditionalParameter(ety.getKey(), ety.getValue());
}
}
Map<String, Object> updatedColumnDatas = getUpdatedColumnDatas(tableName, boundSql, updateStmt);
OriginalDataObj originalData = buildOriginalObjectData(updatedColumnDatas, selectBody, buildColumns2SelectItems.getPk(), mappedStatement, boundSql4Select, connection);
OperateLogResult result = new OperateLogResult();
result.setOperation(operation);
result.setTableName(tableName);
result.setRecordStatus(true);
result.setDataChangeResults(this.buildDataStr(compareAndGetUpdatedColumnDatas(result.getTableName(), originalData, updatedColumnDatas)));
return result;
}
private Optional<OperateLogResult> ignoredTableColumns(String table, String operation) {
final Set<String> ignoredColumns = ignoredTableColumns.get(table.toUpperCase());
if (ignoredColumns != null) {
if (ignoredColumns.stream().anyMatch("*"::equals)) {
OperateLogResult result = new OperateLogResult();
result.setOperation(operation);
result.setTableName(table + ":*");
result.setRecordStatus(false);
return Optional.of(result);
}
}
return Optional.empty();
}
private TableInfo getTableInfoByTableName(String tableName) {
for (TableInfo tableInfo : TableInfoHelper.getTableInfos()) {
if (tableName.equalsIgnoreCase(tableInfo.getTableName())) {
return tableInfo;
}
}
return null;
}
/**
* 将update SET部分的jdbc参数去除
*
* @param originalMappingList 这里只会包含JdbcParameter参数
* @param updateStmt
* @return
*/
private List<ParameterMapping> prepareParameterMapping4Select(List<ParameterMapping> originalMappingList, Update updateStmt) {
List<Expression> updateValueExpressions = new ArrayList<>();
for (UpdateSet updateSet : updateStmt.getUpdateSets()) {
updateValueExpressions.addAll(updateSet.getValues());
}
int removeParamCount = 0;
for (Expression expression : updateValueExpressions) {
if (expression instanceof JdbcParameter) {
++removeParamCount;
}
}
return originalMappingList.subList(removeParamCount, originalMappingList.size());
}
protected Map<String, Object> getUpdatedColumnDatas(String tableName, BoundSql updateSql, Statement statement) {
Map<String, Object> columnNameValMap = new HashMap<>(updateSql.getParameterMappings().size());
Map<Integer, String> columnSetIndexMap = new HashMap<>(updateSql.getParameterMappings().size());
List<Column> selectItemsFromUpdateSql = new ArrayList<>();
if (statement instanceof Update) {
Update updateStmt = (Update) statement;
int index = 0;
for (UpdateSet updateSet : updateStmt.getUpdateSets()) {
selectItemsFromUpdateSql.addAll(updateSet.getColumns());
final ExpressionList<Expression> updateList = (ExpressionList<Expression>) updateSet.getValues();
for (int i = 0; i < updateList.size(); ++i) {
Expression updateExps = updateList.get(i);
if (!(updateExps instanceof JdbcParameter)) {
columnNameValMap.put(updateSet.getColumns().get(i).getColumnName().toUpperCase(), updateExps.toString());
}
columnSetIndexMap.put(index++, updateSet.getColumns().get(i).getColumnName().toUpperCase());
}
}
} else if (statement instanceof Insert) {
Insert insert = (Insert) statement;
selectItemsFromUpdateSql.addAll(insert.getColumns());
columnNameValMap.putAll(detectInsertColumnValuesNonJdbcParameters(insert));
}
Map<String, String> relatedColumnsUpperCaseWithoutUnderline = new HashMap<>(selectItemsFromUpdateSql.size(), 1);
for (Column item : selectItemsFromUpdateSql) {
//FIRSTNAME: FIRST_NAME/FIRST-NAME/FIRST$NAME/FIRST.NAME
relatedColumnsUpperCaseWithoutUnderline.put(item.getColumnName().replaceAll("[._\-$]", "").toUpperCase(), item.getColumnName().toUpperCase());
}
MetaObject metaObject = SystemMetaObject.forObject(updateSql.getParameterObject());
int index = 0;
for (ParameterMapping parameterMapping : updateSql.getParameterMappings()) {
String propertyName = parameterMapping.getProperty();
if (propertyName.startsWith("ew.paramNameValuePairs")) {
++index;
continue;
}
String[] arr = propertyName.split("\.");
String propertyNameTrim = arr[arr.length - 1].replace("_", "").toUpperCase();
try {
final String columnName = columnSetIndexMap.getOrDefault(index++, getColumnNameByProperty(propertyNameTrim, tableName));
if (relatedColumnsUpperCaseWithoutUnderline.containsKey(propertyNameTrim)) {
final String colkey = relatedColumnsUpperCaseWithoutUnderline.get(propertyNameTrim);
Object valObj = metaObject.getValue(propertyName);
if (valObj instanceof IEnum) {
valObj = ((IEnum<?>) valObj).getValue();
} else if (valObj instanceof Enum) {
valObj = getEnumValue((Enum) valObj);
}
if (columnNameValMap.containsKey(colkey)) {
columnNameValMap.put(relatedColumnsUpperCaseWithoutUnderline.get(propertyNameTrim), String.valueOf(columnNameValMap.get(colkey)).replace("?", valObj == null ? "" : valObj.toString()));
}
if (columnName != null && !columnNameValMap.containsKey(columnName)) {
columnNameValMap.put(columnName, valObj);
}
} else {
if (columnName != null) {
columnNameValMap.put(columnName, String.valueOf(metaObject.getValue(propertyName)));
}
}
} catch (Exception e) {
logger.warn("get value error,propertyName:{},parameterMapping:{}", propertyName, parameterMapping);
}
}
dealWithUpdateWrapper(columnSetIndexMap, columnNameValMap, updateSql);
return columnNameValMap;
}
/**
* @param originalDataObj
* @return
*/
private List<DataChangedRecord> compareAndGetUpdatedColumnDatas(String tableName, OriginalDataObj originalDataObj, Map<String, Object> columnNameValMap) {
final Set<String> ignoredColumns = ignoredTableColumns.get(tableName.toUpperCase());
if (originalDataObj == null || originalDataObj.isEmpty()) {
DataChangedRecord oneRecord = new DataChangedRecord();
List<DataColumnChangeResult> updateColumns = new ArrayList<>(columnNameValMap.size());
for (Map.Entry<String, Object> ety : columnNameValMap.entrySet()) {
String columnName = ety.getKey();
if ((ignoredColumns == null || !ignoredColumns.contains(columnName)) && !ignoreAllColumns.contains(columnName)) {
updateColumns.add(DataColumnChangeResult.constrcutByUpdateVal(columnName, ety.getValue()));
}
}
oneRecord.setUpdatedColumns(updateColumns);
// oneRecord.setUpdatedColumns(Collections.EMPTY_LIST);
return Collections.singletonList(oneRecord);
}
List<DataChangedRecord> originalDataList = originalDataObj.getOriginalDataObj();
List<DataChangedRecord> updateDataList = new ArrayList<>(originalDataList.size());
for (DataChangedRecord originalData : originalDataList) {
if (originalData.hasUpdate(columnNameValMap, ignoredColumns, ignoreAllColumns)) {
updateDataList.add(originalData);
}
}
return updateDataList;
}
private Object getEnumValue(Enum enumVal) {
Optional<String> enumValueFieldName = MybatisEnumTypeHandler.findEnumValueFieldName(enumVal.getClass());
if (enumValueFieldName.isPresent()) {
return SystemMetaObject.forObject(enumVal).getValue(enumValueFieldName.get());
}
return enumVal;
}
@SuppressWarnings("rawtypes")
private void dealWithUpdateWrapper(Map<Integer, String> columnSetIndexMap, Map<String, Object> columnNameValMap, BoundSql updateSql) {
if (columnSetIndexMap.size() <= columnNameValMap.size()) {
return;
}
MetaObject mpgenVal = SystemMetaObject.forObject(updateSql.getParameterObject());
if(!mpgenVal.hasGetter(Constants.WRAPPER)){
return;
}
Object ew = mpgenVal.getValue(Constants.WRAPPER);
if (ew instanceof UpdateWrapper || ew instanceof LambdaUpdateWrapper) {
final String sqlSet = ew instanceof UpdateWrapper ? ((UpdateWrapper) ew).getSqlSet() : ((LambdaUpdateWrapper) ew).getSqlSet();// columnName=#{val}
if (sqlSet == null) {
return;
}
MetaObject ewMeta = SystemMetaObject.forObject(ew);
final Map paramNameValuePairs = (Map) ewMeta.getValue("paramNameValuePairs");
String[] setItems = sqlSet.split(",");
for (String setItem : setItems) {
//age=#{ew.paramNameValuePairs.MPGENVAL1}
String[] nameAndValuePair = setItem.split("=", 2);
if (nameAndValuePair.length == 2) {
String setColName = nameAndValuePair[0].trim().toUpperCase();
String setColVal = nameAndValuePair[1].trim();//#{.mp}
if (columnSetIndexMap.containsValue(setColName)) {
String[] mpGenKeyArray = setColVal.split("\.");
String mpGenKey = mpGenKeyArray[mpGenKeyArray.length - 1].replace("}", "");
final Object setVal = paramNameValuePairs.get(mpGenKey);
if (setVal instanceof IEnum) {
columnNameValMap.put(setColName, String.valueOf(((IEnum<?>) setVal).getValue()));
} else {
columnNameValMap.put(setColName, String.valueOf(setVal));
}
}
}
}
}
}
private Map<String, String> detectInsertColumnValuesNonJdbcParameters(Insert insert) {
Map<String, String> columnNameValMap = new HashMap<>(4);
final Select select = insert.getSelect();
List<Column> columns = insert.getColumns();
if (select instanceof SetOperationList) {
SetOperationList setOperationList = (SetOperationList) select;
final List<Select> selects = setOperationList.getSelects();
if (CollectionUtils.isEmpty(selects)) {
return columnNameValMap;
}
final Select selectBody = selects.get(0);
if (!(selectBody instanceof Values)) {
return columnNameValMap;
}
Values valuesStatement = (Values) selectBody;
if (valuesStatement.getExpressions() instanceof ExpressionList) {
ExpressionList expressionList = valuesStatement.getExpressions();
List<Expression> expressions = expressionList;
for (Expression expression : expressions) {
if (expression instanceof RowConstructor) {
final ExpressionList exprList = ((RowConstructor) expression);
final List<Expression> insertExpList = exprList;
for (int i = 0; i < insertExpList.size(); ++i) {
Expression e = insertExpList.get(i);
if (!(e instanceof JdbcParameter)) {
final String columnName = columns.get(i).getColumnName();
final String val = e.toString();
columnNameValMap.put(columnName, val);
}
}
}
}
}
}
return columnNameValMap;
}
private String getColumnNameByProperty(String propertyName, String tableName) {
for (TableInfo tableInfo : TableInfoHelper.getTableInfos()) {
if (tableName.equalsIgnoreCase(tableInfo.getTableName())) {
final List<TableFieldInfo> fieldList = tableInfo.getFieldList();
if (CollectionUtils.isEmpty(fieldList)) {
return propertyName;
}
for (TableFieldInfo tableFieldInfo : fieldList) {
if (propertyName.equalsIgnoreCase(tableFieldInfo.getProperty())) {
return tableFieldInfo.getColumn().toUpperCase();
}
}
return propertyName;
}
}
return propertyName;
}
private Map<String, Object> buildParameterObjectMap(BoundSql boundSql) {
MetaObject metaObject = PluginUtils.getMetaObject(boundSql.getParameterObject());
Map<String, Object> propertyValMap = new HashMap<>(boundSql.getParameterMappings().size());
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
String propertyName = parameterMapping.getProperty();
if (propertyName.startsWith("ew.paramNameValuePairs")) {
continue;
}
Object propertyValue = metaObject.getValue(propertyName);
propertyValMap.put(propertyName, propertyValue);
}
return propertyValMap;
}
private List<DataChangeResult> buildOriginalData(Select selectStmt, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
List<DataChangeResult> dataChangeResults = new ArrayList<>();
try (PreparedStatement statement = connection.prepareStatement(selectStmt.toString())) {
DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql);
parameterHandler.setParameters(statement);
ResultSet resultSet = statement.executeQuery();
final ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
int count = 0;
while (resultSet.next()) {
++count;
if (checkTableBatchLimitExceeded(selectStmt, count)) {
logger.error("batch delete limit exceed: count={}, BATCH_UPDATE_LIMIT={}", count, BATCH_UPDATE_LIMIT);
throw DataUpdateLimitationException.DEFAULT;
}
DataChangeResult dataChangeResult = new DataChangeResult();
List<DataChangeColumnResult> columnChangeResults = new ArrayList<>();
dataChangeResult.setChangeColumnResults(columnChangeResults);
for (int i = 1; i <= columnCount; ++i) {
DataChangeColumnResult changeColumnResult = new DataChangeColumnResult();
changeColumnResult.setColumnName(metaData.getColumnName(i));
Object res = resultSet.getObject(i);
if (res instanceof Clob) {
changeColumnResult.setOriginalValue(DataColumnChangeResult.convertClob((Clob) res));
} else {
changeColumnResult.setOriginalValue(null != res ? res.toString() : null);
}
if("id".equalsIgnoreCase(changeColumnResult.getColumnName())){
dataChangeResult.setId(changeColumnResult.getOriginalValue());
}
columnChangeResults.add(changeColumnResult);
}
dataChangeResults.add(dataChangeResult);
}
resultSet.close();
return dataChangeResults;
} catch (Exception e) {
if (e instanceof DataUpdateLimitationException) {
throw (DataUpdateLimitationException) e;
}
logger.error("try to get record tobe deleted for selectStmt={}", selectStmt, e);
return null;
}
}
private OriginalDataObj buildOriginalObjectData(Map<String, Object> updatedColumnDatas, Select selectStmt, Column pk, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
try (PreparedStatement statement = connection.prepareStatement(selectStmt.toString())) {
DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql);
parameterHandler.setParameters(statement);
ResultSet resultSet = statement.executeQuery();
List<DataChangedRecord> originalObjectDatas = new LinkedList<>();
int count = 0;
while (resultSet.next()) {
++count;
if (checkTableBatchLimitExceeded(selectStmt, count)) {
logger.error("batch update limit exceed: count={}, BATCH_UPDATE_LIMIT={}", count, BATCH_UPDATE_LIMIT);
throw DataUpdateLimitationException.DEFAULT;
}
originalObjectDatas.add(prepareOriginalDataObj(updatedColumnDatas, resultSet, pk));
}
OriginalDataObj result = new OriginalDataObj();
result.setOriginalDataObj(originalObjectDatas);
resultSet.close();
return result;
} catch (Exception e) {
if (e instanceof DataUpdateLimitationException) {
throw (DataUpdateLimitationException) e;
}
logger.error("try to get record tobe updated for selectStmt={}", selectStmt, e);
return new OriginalDataObj();
}
}
/**
* 防止出现全表批量更新
* 默认一次更新不超过1000条
*
* @param selectStmt
* @param count
* @return
*/
private boolean checkTableBatchLimitExceeded(Select selectStmt, int count) {
if (!batchUpdateLimitationOpened) {
return false;
}
final PlainSelect selectBody = (PlainSelect) selectStmt;
final FromItem fromItem = selectBody.getFromItem();
if (fromItem instanceof Table) {
Table fromTable = (Table) fromItem;
final String tableName = fromTable.getName().toUpperCase();
if (!BATCH_UPDATE_LIMIT_MAP.containsKey(tableName)) {
if (count > BATCH_UPDATE_LIMIT) {
logger.error("batch update limit exceed for tableName={}, BATCH_UPDATE_LIMIT={}, count={}",
tableName, BATCH_UPDATE_LIMIT, count);
return true;
}
return false;
}
final Integer limit = BATCH_UPDATE_LIMIT_MAP.get(tableName);
if (count > limit) {
logger.error("batch update limit exceed for configured tableName={}, BATCH_UPDATE_LIMIT={}, count={}",
tableName, limit, count);
return true;
}
return false;
}
return count > BATCH_UPDATE_LIMIT;
}
/**
* get records : include related column with original data in DB
*
* @param resultSet
* @param pk
* @return
* @throws SQLException
*/
private DataChangedRecord prepareOriginalDataObj(Map<String, Object> updatedColumnDatas, ResultSet resultSet, Column pk) throws SQLException {
final ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
List<DataColumnChangeResult> originalColumnDatas = new LinkedList<>();
DataColumnChangeResult pkval = null;
for (int i = 1; i <= columnCount; ++i) {
String columnName = metaData.getColumnName(i).toUpperCase();
DataColumnChangeResult col;
Object updateVal = updatedColumnDatas.get(columnName);
if (updateVal != null && updateVal.getClass().getCanonicalName().startsWith("java.")) {
col = DataColumnChangeResult.constrcutByOriginalVal(columnName, resultSet.getObject(i, updateVal.getClass()));
} else {
col = DataColumnChangeResult.constrcutByOriginalVal(columnName, resultSet.getObject(i));
}
if (pk != null && columnName.equalsIgnoreCase(pk.getColumnName())) {
pkval = col;
} else {
originalColumnDatas.add(col);
}
}
DataChangedRecord changedRecord = new DataChangedRecord();
changedRecord.setOriginalColumnDatas(originalColumnDatas);
if (pkval != null) {
changedRecord.setPkColumnName(pkval.getColumnName());
changedRecord.setPkColumnVal(pkval.getOriginalValue());
}
return changedRecord;
}
private Columns2SelectItemsResult buildColumns2SelectItems(String tableName, List<Column> columns) {
if (columns == null || columns.isEmpty()) {
return Columns2SelectItemsResult.build(Collections.singletonList(new SelectItem<>(new AllColumns())), 0);
}
List<SelectItem<?>> selectItems = new ArrayList<>(columns.size());
for (Column column : columns) {
selectItems.add(new SelectItem<>(column));
}
TableInfo tableInfo = getTableInfoByTableName(tableName);
if (tableInfo == null) {
return Columns2SelectItemsResult.build(selectItems, 0);
}
Column pk = new Column(tableInfo.getKeyColumn());
selectItems.add(new SelectItem<>(pk));
Columns2SelectItemsResult result = Columns2SelectItemsResult.build(selectItems, 1);
result.setPk(pk);
return result;
}
private String buildParameterObject(BoundSql boundSql) {
Object paramObj = boundSql.getParameterObject();
StringBuilder sb = new StringBuilder();
sb.append("{");
if (paramObj instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) paramObj;
int index = 1;
boolean hasParamIndex = false;
String key;
while (paramMap.containsKey((key = "param" + index))) {
Object paramIndex = paramMap.get(key);
sb.append(""").append(key).append(""").append(":").append(""").append(paramIndex).append(""").append(",");
hasParamIndex = true;
++index;
}
if (hasParamIndex) {
sb.delete(sb.length() - 1, sb.length());
sb.append("}");
return sb.toString();
}
for (Map.Entry<String, Object> ety : paramMap.entrySet()) {
sb.append(""").append(ety.getKey()).append(""").append(":").append(""").append(ety.getValue()).append(""").append(",");
}
sb.delete(sb.length() - 1, sb.length());
sb.append("}");
return sb.toString();
}
sb.append("param:").append(paramObj);
sb.append("}");
return sb.toString();
}
public OperateLogResult processDelete(Delete deleteStmt, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
Table table = deleteStmt.getTable();
if (checkOperateLogIgnore(table.getName())) {
return null;
}
Expression where = deleteStmt.getWhere();
PlainSelect selectBody = new PlainSelect();
selectBody.setFromItem(table);
selectBody.setSelectItems(Collections.singletonList(new SelectItem<>((new AllColumns()))));
selectBody.setWhere(where);
List<DataChangeResult> dataChangeResults = buildOriginalData(selectBody, mappedStatement, boundSql, connection);
OperateLogResult result = new OperateLogResult();
result.setOperation("delete");
result.setTableName(table.getName());
result.setRecordStatus(null != dataChangeResults);
result.setDataChangeResults(null != dataChangeResults ? dataChangeResults : new ArrayList<>());
return result;
}
/**
* 设置批量更新记录条数上限
*
* @param limit
* @return
*/
public OperateLogMybatisInnerInterceptor setBatchUpdateLimit(int limit) {
this.BATCH_UPDATE_LIMIT = limit;
return this;
}
public OperateLogMybatisInnerInterceptor openBatchUpdateLimitation() {
this.batchUpdateLimitationOpened = true;
return this;
}
public OperateLogMybatisInnerInterceptor configTableLimitation(String tableName, int limit) {
this.BATCH_UPDATE_LIMIT_MAP.put(tableName.toUpperCase(), limit);
return this;
}
/**
* ignoredColumns = TABLE_NAME1.COLUMN1,COLUMN2; TABLE2.COLUMN1,COLUMN2; TABLE3.*; *.COLUMN1,COLUMN2
* 多个表用分号分隔
* TABLE_NAME1.COLUMN1,COLUMN2 : 表示忽略这个表的这2个字段
* TABLE3.*: 表示忽略这张表的INSERT/UPDATE,delete暂时还保留
* *.COLUMN1,COLUMN2:表示所有表的这个2个字段名都忽略
*
* @param properties
*/
@Override
public void setProperties(Properties properties) {
String ignoredTableColumns = properties.getProperty("ignoredTableColumns");
if (ignoredTableColumns == null || ignoredTableColumns.trim().isEmpty()) {
return;
}
String[] array = ignoredTableColumns.split(";");
for (String table : array) {
int index = table.indexOf(".");
if (index == -1) {
logger.warn("invalid data={} for ignoredColumns, format should be TABLE_NAME1.COLUMN1,COLUMN2; TABLE2.COLUMN1,COLUMN2;", table);
continue;
}
String tableName = table.substring(0, index).trim().toUpperCase();
String[] columnArray = table.substring(index + 1).split(",");
Set<String> columnSet = new HashSet<>(columnArray.length);
for (String column : columnArray) {
column = column.trim().toUpperCase();
if (column.isEmpty()) {
continue;
}
columnSet.add(column);
}
if ("*".equals(tableName)) {
ignoreAllColumns.addAll(columnSet);
} else {
this.ignoredTableColumns.put(tableName, columnSet);
}
}
}
public List<DataChangeResult> buildDataStr(List<DataChangedRecord> records) {
List<DataChangeResult> dataChangeResults = new ArrayList<>();
for (DataChangedRecord r : records) {
DataChangeResult dataChangeResult = new DataChangeResult();
dataChangeResult.setId(null != r.getPkColumnVal() ? r.getPkColumnVal().toString() : null);
List<DataChangeColumnResult> changeColumnResults = new ArrayList<>();
dataChangeResult.setChangeColumnResults(changeColumnResults);
for (DataColumnChangeResult updatedColumn : r.getUpdatedColumns()) {
DataChangeColumnResult changeColumnResult = new DataChangeColumnResult();
changeColumnResult.setColumnName(updatedColumn.getColumnName());
changeColumnResult.setOriginalValue(updatedColumn.convertDoubleQuotes(updatedColumn.getOriginalValue()));
changeColumnResult.setUpdateValue(updatedColumn.convertDoubleQuotes(updatedColumn.getUpdateValue()));
changeColumnResults.add(changeColumnResult);
}
dataChangeResults.add(dataChangeResult);
}
return dataChangeResults;
}
@Data
public static class Columns2SelectItemsResult {
private Column pk;
/**
* all column with additional columns: ID, etc.
*/
private List<SelectItem<?>> selectItems;
/**
* newly added column count from meta data.
*/
private int additionalItemCount;
public static Columns2SelectItemsResult build(List<SelectItem<?>> selectItems, int additionalItemCount) {
Columns2SelectItemsResult result = new Columns2SelectItemsResult();
result.setSelectItems(selectItems);
result.setAdditionalItemCount(additionalItemCount);
return result;
}
}
@Data
public static class OriginalDataObj {
private List<DataChangedRecord> originalDataObj;
public boolean isEmpty() {
return originalDataObj == null || originalDataObj.isEmpty();
}
}
@Data
public static class DataColumnChangeResult {
private String columnName;
private Object originalValue;
private Object updateValue;
@SuppressWarnings("rawtypes")
public boolean isDataChanged(Object updateValue) {
if (!Objects.equals(originalValue, updateValue)) {
if (originalValue instanceof Clob) {
String originalStr = convertClob((Clob) originalValue);
setOriginalValue(originalStr);
return !originalStr.equals(updateValue);
}
if (originalValue instanceof Comparable) {
Comparable original = (Comparable) originalValue;
Comparable update = (Comparable) updateValue;
try {
return update == null || original.compareTo(update) != 0;
} catch (Exception e) {
return true;
}
}
return true;
}
return false;
}
public static String convertClob(Clob clobObj) {
try {
return clobObj.getSubString(0, (int) clobObj.length());
} catch (Exception e) {
try (Reader is = clobObj.getCharacterStream()) {
char[] chars = new char[64];
int readChars;
StringBuilder sb = new StringBuilder();
while ((readChars = is.read(chars)) != -1) {
sb.append(chars, 0, readChars);
}
return sb.toString();
} catch (Exception e2) {
//ignored
return "unknown clobObj";
}
}
}
public static DataColumnChangeResult constrcutByUpdateVal(String columnName, Object updateValue) {
DataColumnChangeResult res = new DataColumnChangeResult();
res.setColumnName(columnName);
res.setUpdateValue(updateValue);
return res;
}
public static DataColumnChangeResult constrcutByOriginalVal(String columnName, Object originalValue) {
DataColumnChangeResult res = new DataColumnChangeResult();
res.setColumnName(columnName);
res.setOriginalValue(originalValue);
return res;
}
public String generateDataStr() {
StringBuilder sb = new StringBuilder();
sb.append(""").append(columnName).append(""").append(":").append(""").append(convertDoubleQuotes(originalValue)).append("->").append(convertDoubleQuotes(updateValue)).append(""").append(",");
return sb.toString();
}
public String convertDoubleQuotes(Object obj) {
if (obj == null) {
return null;
}
return obj.toString().replace(""", "\"");
}
}
@Data
public static class DataChangedRecord {
private String pkColumnName;
private Object pkColumnVal;
private List<DataColumnChangeResult> originalColumnDatas;
private List<DataColumnChangeResult> updatedColumns;
public boolean hasUpdate(Map<String, Object> columnNameValMap, Set<String> ignoredColumns, Set<String> ignoreAllColumns) {
if (originalColumnDatas == null) {
return true;
}
boolean hasUpdate = false;
updatedColumns = new ArrayList<>(originalColumnDatas.size());
for (DataColumnChangeResult originalColumn : originalColumnDatas) {
final String columnName = originalColumn.getColumnName().toUpperCase();
if (ignoredColumns != null && ignoredColumns.contains(columnName) || ignoreAllColumns.contains(columnName)) {
continue;
}
Object updatedValue = columnNameValMap.get(columnName);
if (originalColumn.isDataChanged(updatedValue)) {
hasUpdate = true;
originalColumn.setUpdateValue(updatedValue);
updatedColumns.add(originalColumn);
}
}
return hasUpdate;
}
public String generateUpdatedDataStr() {
StringBuilder sb = new StringBuilder();
sb.append("{");
if (pkColumnName != null) {
sb.append(""").append(pkColumnName).append(""").append(":").append(""").append(convertDoubleQuotes(pkColumnVal)).append(""").append(",");
}
for (DataColumnChangeResult update : updatedColumns) {
sb.append(update.generateDataStr());
}
sb.replace(sb.length() - 1, sb.length(), "}");
return sb.toString();
}
public String convertDoubleQuotes(Object obj) {
if (obj == null) {
return null;
}
return obj.toString().replace(""", "\"");
}
}
public static class DataUpdateLimitationException extends MybatisPlusException {
public DataUpdateLimitationException(String message) {
super(message);
}
public static DataUpdateLimitationException DEFAULT = new DataUpdateLimitationException("本次操作 因超过系统安全阈值 被拦截,如需继续,请联系管理员!");
}
public void setPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
}
MybatisPlusConfig
java
@Configuration
public class MybatisPlusConfig {
@Autowired
private ApplicationEventPublisher publisher;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
OperateLogMybatisInnerInterceptor operateLogMybatisInnerInterceptor = new OperateLogMybatisInnerInterceptor();
// 配置安全阈值,例如限制批量更新或插入的记录数不超过 1000 条
operateLogMybatisInnerInterceptor.setBatchUpdateLimit(1000);
operateLogMybatisInnerInterceptor.setPublisher(publisher);
interceptor.addInnerInterceptor(operateLogMybatisInnerInterceptor);
return interceptor;
}
}
Swagger
配置
less
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket petApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("日志审计")
.select()
.apis(RequestHandlerSelectors.basePackage("com.huzhihui.data.change.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo(){
Contact contact = new Contact("huzhihui", "https://xxx.com/", "xxx@qq.com");
return new ApiInfo(
"日志审计",
"日志审计DEMO",
"v1.0",
"https://xxx.com/",
contact,
"Apache 2.0",
"https://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());
}
}
测试方法
less
@Api(tags = {"用户信息"})
@RestController
@RequestMapping("userinfo")
public class UserinfoController {
@Autowired
private UserinfoService userinfoService;
@Autowired
private UserinfoMapper userinfoMapper;
@Autowired
private SubMapper subMapper;
@OperateLog("新增")
@ApiOperation(value = "新增", extensions = {@Extension(name = "", properties = {@ExtensionProperty(name = "", value = "")})})
@Transactional(rollbackFor = Exception.class)
@PostMapping(value = "add")
public Object add(@RequestBody UserinfoEntity entity){
userinfoService.save(entity);
return "SUCCESS";
}
@Transactional(rollbackFor = Exception.class)
@GetMapping(value = "batchAdd")
public Object batchAdd(int count, String preName){
List<UserinfoEntity> entities = new ArrayList<>();
for (int i = 0; i < count; i++) {
UserinfoEntity entity = new UserinfoEntity();
entity.setName(preName + i);
entities.add(entity);
}
userinfoService.saveBatch(entities, count);
return "SUCCESS";
}
@Transactional(rollbackFor = Exception.class)
@PostMapping(value = "update")
public Object update(@RequestBody UserinfoEntity entity){
userinfoService.updateById(entity);
return "SUCCESS";
}
@Transactional(rollbackFor = Exception.class)
@GetMapping(value = "delete")
public Object delete(Long id){
userinfoService.removeById(id);
return "SUCCESS";
}
@Transactional(rollbackFor = Exception.class)
@GetMapping(value = "otherUpdate1")
public Object otherUpdate1(String name){
LambdaUpdateWrapper<UserinfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(UserinfoEntity::getName, name + new Date())
.set(UserinfoEntity::getUpdateTime, new Date())
.eq(UserinfoEntity::getName, name);
userinfoService.update(updateWrapper);
return "SUCCESS";
}
@Transactional(rollbackFor = Exception.class)
@GetMapping(value = "otherUpdate2")
public Object otherUpdate2(String name){
UserinfoEntity userinfoEntity = new UserinfoEntity();
userinfoEntity.setMoney(new BigDecimal(1));
userinfoEntity.setCode("T1");
LambdaUpdateWrapper<UserinfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(UserinfoEntity::getName, name + new Date())
.set(UserinfoEntity::getCode, "C1")
.set(UserinfoEntity::getUpdateTime, new Date())
.eq(UserinfoEntity::getName, name);
userinfoService.update(userinfoEntity, updateWrapper);
return "SUCCESS";
}
@Transactional(rollbackFor = Exception.class)
@GetMapping(value = "complexUpdate1")
public Object complexUpdate1(Long id, String code){
userinfoMapper.complexUpdate(code,id);
return "SUCCESS";
}
@Transactional(rollbackFor = Exception.class)
@GetMapping(value = "complexUpdate2")
public Object complexUpdate2(Long id){
userinfoMapper.complexUpdate2(id);
return "SUCCESS";
}
@GetMapping(value = "tableInfo")
public Object tableInfo(String tableName){
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
return tableInfo;
}
@OperateLog("复合新增")
@GetMapping(value = "complexAdd")
public Object complexAdd(){
UserinfoEntity entity = new UserinfoEntity();
entity.setId(IdWorker.getId());
entity.setName("XXX");
userinfoMapper.insert(entity);
SubEntity subEntity = new SubEntity();
subEntity.setId(IdWorker.getId());
subEntity.setParentId(entity.getId());
subMapper.insert(subEntity);
return "SUCCESS";
}
}