引文

  本文主要介绍如何使用Spring AOP + mybatis插件实现拦截数据库操作并根据不同需求进行数据对比分析,主要适用于系统中需要对数据操作进行记录、在更新数据时准确记录更新字段

核心:AOP、mybatis插件(拦截器)、mybatis-Plus实体规范、数据对比

1、相关技术简介

mybatis插件

  mybatis插件实际上就是官方针对4层数据操作处理预留的拦截器,使用者可以根据不同的需求进行操作拦截并处理。这边笔者不做详细描述,详细介绍请到官网了解,这里笔者就复用官网介绍。

插件(plugins)

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为如果在试图修改或重写已有方法的行为的时候,你很可能在破坏 MyBatis 的核心模块。 这些都是更低层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

// ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
}
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>

上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行低层映射语句的内部对象。

提示 覆盖配置类

除了用插件来修改 MyBatis 核心行为之外,还可以通过完全覆盖配置类来达到目的。只需继承后覆盖其中的每个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,这可能会严重影响 MyBatis 的行为,务请慎之又慎。

重点讲下4层处理,MyBatis两级缓存就是在其中两层中实现

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

    • 所有数据库操作到达底层后都由该执行器进行任务分发,主要有update(插入、更新、删除),query(查询),提交,回滚,关闭链接等
  • ParameterHandler (getParameterObject, setParameters) 
    • 参数处理器(获取参数,设置参数)
  • ResultSetHandler (handleResultSets, handleOutputParameters) 
    • 结果集处理器(结果集,输出参数)
  • StatementHandler (prepare, parameterize, batch, update, query) 
    • 声明处理器、准备链接jdbc前处理,prepare(预处理):生成sql语句,准备链接数据库进行操作

以上4层执行顺序为顺序执行

  • Executor是 Mybatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过 ResultSetHandler进行自动映射,另外,他还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。
  • ParameterHandler是Mybatis实现Sql入参设置的对象。插件可以改变我们Sql的参数默认设置。
  • ResultSetHandler是Mybatis把ResultSet集合映射成POJO的接口对象。我们可以定义插件对Mybatis的结果集自动映射进行修改。
  • StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。

MyBatis-Plus:

  MyBatis增强器,主要规范了数据实体,在底层实现了简单的增删查改,使用者不再需要开发基础操作接口,小编认为是最强大、最方便易用的,没有之一,不接受任何反驳。详细介绍请看官网

数据实体的规范让底层操作更加便捷,本例主要实体规范中的表名以及主键获取,下面上实体规范demo

package com.lith.datalog.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode; /**
* <p>
* 用户表
* </p>
*
* @author Tophua
* @since 2020/5/7
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends Model<User> {
/**
* 主键id
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String name;
private Integer age;
private String email;
}

2、实现

本文所要讲述的就是在第一级(Executor)进行拦截并实现数据对比记录。
本例为公共模块实现,然后在其它模块中依赖此公共模块,根据每个模块不同的需求自定义实现不同的处理。
结构目录

一、配置

 1 package com.lith.datalog.config;
2
3 import com.lith.datalog.handle.DataUpdateInterceptor;
4 import org.mybatis.spring.annotation.MapperScan;
5 import org.springframework.context.annotation.Bean;
6 import org.springframework.context.annotation.Configuration;
7 import org.springframework.context.annotation.Profile;
8 import org.springframework.transaction.annotation.EnableTransactionManagement;
9
10 import javax.sql.DataSource;
11
12 /**
13 * <p>
14 * Mybatis-Plus配置
15 * </p>
16 *
17 * @author Tophua
18 * @since 2020/5/7
19 */
20 @Configuration
21 @EnableTransactionManagement
22 @MapperScan("com.lith.**.mapper")
23 public class MybatisPlusConfig {
24
25 /**
26 * <p>
27 * SQL执行效率插件 设置 dev test 环境开启
28 * </p>
29 *
30 * @return cn.rc100.common.data.mybatis.EplusPerformanceInterceptor
31 * @author Tophua
32 * @since 2020/3/11
33 */
34 @Bean
35 @Profile({"dev","test"})
36 public PerformanceInterceptor performanceInterceptor() {
37 return new PerformanceInterceptor();
38 }
39
40 /**
41 * <p>
42 * 数据更新操作处理
43 * </p>
44 *
45 * @return com.lith.datalog.handle.DataUpdateInterceptor
46 * @author Tophua
47 * @since 2020/5/11
48 */
49 @Bean
50 @Profile({"dev","test"})
51 public DataUpdateInterceptor dataUpdateInterceptor(DataSource dataSource) {
52 return new DataUpdateInterceptor(dataSource);
53 }
54 }

二、实现拦截器

DataUpdateInterceptor,根据官网demo实现拦截器,在拦截器中根据增、删、改操作去调用各个模块中自定义实现的处理方法来达到不同的操作处理。

 package com.lith.datalog.handle;

 import cn.hutool.db.Db;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.TableNameParser;
import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
import com.lith.datalog.aspect.DataLogAspect;
import com.lith.datalog.aspect.DataTem;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject; import javax.sql.DataSource;
import java.lang.reflect.Proxy;
import java.sql.Statement;
import java.util.*; /**
* <p>
* 数据更新拦截器
* </p>
*
* @author Tophua
* @since 2020/5/11
*/
@Slf4j
@AllArgsConstructor
@Intercepts({@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})})
public class DataUpdateInterceptor extends AbstractSqlParserHandler implements Interceptor {
private final DataSource dataSource; @Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取线程名,使用线程名作为同一次操作记录
String threadName = Thread.currentThread().getName();
// 判断是否需要记录日志
if (!DataLogAspect.hasThread(threadName)) {
return invocation.proceed();
}
Statement statement;
Object firstArg = invocation.getArgs()[0];
if (Proxy.isProxyClass(firstArg.getClass())) {
statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
} else {
statement = (Statement) firstArg;
}
MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);
try {
statement = (Statement) stmtMetaObj.getValue("stmt.statement");
} catch (Exception e) {
// do nothing
}
if (stmtMetaObj.hasGetter("delegate")) {
//Hikari
try {
statement = (Statement) stmtMetaObj.getValue("delegate");
} catch (Exception ignored) { }
} String originalSql = statement.toString();
originalSql = originalSql.replaceAll("[\\s]+", StringPool.SPACE);
int index = indexOfSqlStart(originalSql);
if (index > 0) {
originalSql = originalSql.substring(index);
} StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
this.sqlParser(metaObject);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); // 获取执行Sql
String sql = originalSql.replace("where", "WHERE");
// 插入
if (SqlCommandType.INSERT.equals(mappedStatement.getSqlCommandType())) {
}
// 更新
if (SqlCommandType.UPDATE.equals(mappedStatement.getSqlCommandType())) {
try {
// 使用mybatis-plus 工具解析sql获取表名
Collection<String> tables = new TableNameParser(sql).tables();
if (CollectionUtils.isEmpty(tables)) {
return invocation.proceed();
}
String tableName = tables.iterator().next();
// 使用mybatis-plus 工具根据表名找出对应的实体类
Class<?> entityType = TableInfoHelper.getTableInfos().stream().filter(t -> t.getTableName().equals(tableName))
.findFirst().orElse(new TableInfo(null)).getEntityType(); DataTem dataTem = new DataTem();
dataTem.setTableName(tableName);
dataTem.setEntityType(entityType);
// 设置sql用于执行完后查询新数据
dataTem.setSql("SELECT * FROM " + tableName + " WHERE id in ");
String selectSql = "SELECT * FROM " + tableName + " " + sql.substring(sql.lastIndexOf("WHERE"));
// 查询更新前数据
List<?> oldData = Db.use(dataSource).query(selectSql, entityType);
dataTem.setOldData(oldData);
DataLogAspect.put(threadName, dataTem);
} catch (Exception e) {
e.printStackTrace();
}
}
// 删除
if (SqlCommandType.DELETE.equals(mappedStatement.getSqlCommandType())) {
}
return invocation.proceed();
} /**
* 获取sql语句开头部分
*
* @param sql ignore
* @return ignore
*/
private int indexOfSqlStart(String sql) {
String upperCaseSql = sql.toUpperCase();
Set<Integer> set = new HashSet<>();
set.add(upperCaseSql.indexOf("SELECT "));
set.add(upperCaseSql.indexOf("UPDATE "));
set.add(upperCaseSql.indexOf("INSERT "));
set.add(upperCaseSql.indexOf("DELETE "));
set.remove(-1);
if (CollectionUtils.isEmpty(set)) {
return -1;
}
List<Integer> list = new ArrayList<>(set);
list.sort(Comparator.naturalOrder());
return list.get(0);
}
}

二、AOP

使用AOP主要是考虑到一个方法中会出现多次数据库操作,而这些操作在记录中只能算作用户的一次操作,故使用AOP进行操作隔离,将一个方法内的所有数据库操作合并为一次记录。

此外AOP还代表着是否需要记录日志,有切点才会进行记录。

AOP 切点注解

 package com.lith.datalog.annotation;

 import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; /**
* <p>
* 数据日志
* </p>
*
* @author Tophua
* @since 2020/7/15
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataLog {
}

三、AOP切面实现

采用方法执行前后进行处理

 package com.lith.datalog.aspect;

 import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Db;
import cn.hutool.json.JSONUtil;
import com.lith.datalog.annotation.DataLog;
import com.lith.datalog.handle.CompareResult;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component; import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; /**
* <p>
* DataLog切面
* </p>
*
* @author Tophua
* @since 2020/7/15
*/
@Aspect
@Order(99)
@Component
@AllArgsConstructor
public class DataLogAspect { private final DataSource dataSource; private static final Map<String, List<DataTem>> TEM_MAP = new ConcurrentHashMap<>(); /**
* <p>
* 判断线程是否需要记录日志
* </p>
*
* @param threadName threadName
* @return boolean
* @author Tophua
* @since 2020/7/15
*/
public static boolean hasThread(String threadName) {
return TEM_MAP.containsKey(threadName);
} /**
* <p>
* 增加线程数据库操作
* </p>
*
* @param threadName threadName
* @param dataTem dataTem
* @return void
* @author Tophua
* @since 2020/7/15
*/
public static void put(String threadName, DataTem dataTem) {
if (TEM_MAP.containsKey(threadName)) {
TEM_MAP.get(threadName).add(dataTem);
}
} /**
* <p>
* 切面前执行
* </p>
*
* @param dataLog dataLog
* @return void
* @author Tophua
* @since 2020/7/15
*/
@SneakyThrows
@Before("@annotation(dataLog)")
public void before(DataLog dataLog) {
// 获取线程名,使用线程名作为同一次操作记录
String threadName = Thread.currentThread().getName();
TEM_MAP.put(threadName, new LinkedList<>());
} /**
* <p>
* 切面后执行
* </p>
*
* @param dataLog dataLog
* @return void
* @author Tophua
* @since 2020/7/15
*/
@SneakyThrows
@After("@annotation(dataLog)")
public void after(DataLog dataLog) {
// 获取线程名,使用线程名作为同一次操作记录
String threadName = Thread.currentThread().getName();
List<DataTem> list = TEM_MAP.get(threadName);
if (CollUtil.isEmpty(list)) {
return;
}
list.forEach(dataTem -> {
List<?> oldData = dataTem.getOldData();
if (CollUtil.isEmpty(oldData)) {
return;
}
String ids = oldData.stream().map(o -> {
try {
Method method = o.getClass().getMethod("getId");
return method.invoke(o).toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}).filter(ObjectUtil::isNotNull).collect(Collectors.joining(","));
String sql = dataTem.getSql() + "(" + ids + ")";
try {
List<?> newData = Db.use(dataSource).query(sql, dataTem.getEntityType());
dataTem.setNewData(newData);
System.out.println("oldData:" + JSONUtil.toJsonStr(dataTem.getOldData()));
System.out.println("newData:" + JSONUtil.toJsonStr(dataTem.getNewData())); } catch (SQLException e) {
e.printStackTrace();
}
});
// 异步对比存库
this.compareAndSave(list);
} /**
* <p>
* 对比保存
* </p>
*
* @param list list
* @return void
* @author Tophua
* @since 2020/7/15
*/
@Async
public void compareAndSave(List<DataTem> list) {
StringBuilder sb = new StringBuilder();
list.forEach(dataTem -> {
List<?> oldData = dataTem.getOldData();
List<?> newData = dataTem.getNewData();
// 按id排序
oldData.sort(Comparator.comparingLong(d -> {
try {
Method method = d.getClass().getMethod("getId");
return Long.parseLong(method.invoke(d).toString());
} catch (Exception e) {
e.printStackTrace();
}
return 0L;
}));
newData.sort(Comparator.comparingLong(d -> {
try {
Method method = d.getClass().getMethod("getId");
return Long.parseLong(method.invoke(d).toString());
} catch (Exception e) {
e.printStackTrace();
}
return 0L;
})); for (int i = 0; i < oldData.size(); i++) {
final int[] finalI = {0};
sameClazzDiff(oldData.get(i), newData.get(i)).forEach(r -> {
if (finalI[0] == 0) {
sb.append(StrUtil.LF);
sb.append(StrUtil.format("修改表:【{}】", dataTem.getTableName()));
sb.append(StrUtil.format("id:【{}】", r.getId()));
}
sb.append(StrUtil.LF);
sb.append(StrUtil.format("把字段[{}]从[{}]改为[{}]", r.getFieldName(), r.getOldValue(), r.getNewValue()));
finalI[0]++;
});
}
});
if (sb.length() > 0) {
sb.deleteCharAt(0);
}
// 存库
System.err.println(sb.toString());
} /**
* <p>
* 相同类对比
* </p>
*
* @param obj1 obj1
* @param obj2 obj2
* @return java.util.List<com.lith.datalog.handle.CompareResult>
* @author Tophua
* @since 2020/7/15
*/
private List<CompareResult> sameClazzDiff(Object obj1, Object obj2) {
List<CompareResult> results = new ArrayList<>();
Field[] obj1Fields = obj1.getClass().getDeclaredFields();
Field[] obj2Fields = obj2.getClass().getDeclaredFields();
Long id = null;
for (int i = 0; i < obj1Fields.length; i++) {
obj1Fields[i].setAccessible(true);
obj2Fields[i].setAccessible(true);
Field field = obj1Fields[i];
try {
Object value1 = obj1Fields[i].get(obj1);
Object value2 = obj2Fields[i].get(obj2);
if ("id".equals(field.getName())) {
id = Long.parseLong(value1.toString());
}
if (!ObjectUtil.equal(value1, value2)) {
CompareResult r = new CompareResult();
r.setId(id);
r.setFieldName(field.getName());
// 获取注释
r.setFieldComment(field.getName());
r.setOldValue(value1);
r.setNewValue(value2);
results.add(r);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return results;
} }

3、测试及结果

经过测试,不管怎么使用数据更新操作,结果都可以进行拦截记录,完美达到预期。

小笔这里并没有将记录保存在数据库,由大家自行保存。

测试demo

 package com.lith.datalog.controller;

 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.lith.datalog.annotation.DataLog;
import com.lith.datalog.entity.User;
import com.lith.datalog.mapper.UserMapper;
import com.lith.datalog.service.UserService;
import lombok.AllArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; /**
* <p>
* UserController
* </p>
*
* @author Tophua
* @since 2020/5/7
*/
@RestController
@AllArgsConstructor
@RequestMapping("/user")
public class UserController { private final UserService userService;
private final UserMapper userMapper; @GetMapping("{id}")
public User getById(@PathVariable Integer id) {
return userService.getById(id);
} @DataLog
@PostMapping
public Boolean save(@RequestBody User user) {
return userService.save(user);
} @DataLog
@PutMapping
@Transactional(rollbackFor = Exception.class)
public Boolean updateById(@RequestBody User user) {
User nUser = new User();
nUser.setId(2);
nUser.setName("代码更新");
nUser.updateById();
userService.update(Wrappers.<User>lambdaUpdate()
.set(User::getName, "批量")
.in(User::getId, 3, 4));
userMapper.updateTest();
return userService.updateById(user);
} @DeleteMapping("{id}")
public Boolean removeById(@PathVariable Integer id) {
return userService.removeById(id);
}
}

结果显示:

Time:2 ms - ID:com.lith.datalog.mapper.UserMapper.updateById
Execute SQL:UPDATE user SET name='代码更新' WHERE id=2 Time:2 ms - ID:com.lith.datalog.mapper.UserMapper.update
Execute SQL:UPDATE user SET name='批量' WHERE (id IN (3,4)) Time:2 ms - ID:com.lith.datalog.mapper.UserMapper.updateTest
Execute SQL:update user set age = 44 where id in (5,6) Time:0 ms - ID:com.lith.datalog.mapper.UserMapper.updateById
Execute SQL:UPDATE user SET name='4564', age=20, email='dsahkdhkashk' WHERE id=1 oldData:[{"name":"1","id":2,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"代码更新","id":2,"age":10,"email":"dsahkdhkashk"}]
oldData:[{"name":"1","id":3,"age":10,"email":"dsahkdhkashk"},{"name":"1","id":4,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"批量","id":3,"age":10,"email":"dsahkdhkashk"},{"name":"批量","id":4,"age":10,"email":"dsahkdhkashk"}]
oldData:[{"name":"1","id":5,"age":10,"email":"dsahkdhkashk"},{"name":"1","id":6,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"1","id":5,"age":44,"email":"dsahkdhkashk"},{"name":"1","id":6,"age":44,"email":"dsahkdhkashk"}]
oldData:[{"name":"1","id":1,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"4564","id":1,"age":20,"email":"dsahkdhkashk"}]
修改表:【user】id:【2】
把字段[name]从[1]改为[代码更新]
修改表:【user】id:【3】
把字段[name]从[1]改为[批量]
修改表:【user】id:【4】
把字段[name]从[1]改为[批量]
修改表:【user】id:【5】
把字段[age]从[10]改为[44]
修改表:【user】id:【6】
把字段[age]从[10]改为[44]
修改表:【user】id:【1】
把字段[name]从[1]改为[4564]
把字段[age]从[10]改为[20]

4、总结

  本次综合前车经验,优化设计思想,改为从底层具体执行的 sql 语句入手,通过解析表名及更新条件来构造数据更新前后的查询sql,再使用Spring AOP对方法执行前后进行处理,记录更新前后的数据。最后再使用java反射机制将数据更新前后进行对比记录。

注:

使用AOP涉及到一点,就是需要保证AOP与Spring 数据库事务之间的执行顺序,如果AOP先执行然后再提交事务,那结果则是数据无变化。

在此小笔已将AOP处理级别放到最后,保证先提交事务再去查询更新后的数据,这样才能得出正确的结果。

欢迎各路大神交流意见。。。。。。

最后附上源码地址:

https://gitee.com/TopSkyhua/datalog

package com.lith.datalog.aspect;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Db;
import cn.hutool.json.JSONUtil;
import com.lith.datalog.annotation.DataLog;
import com.lith.datalog.handle.CompareResult;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component; import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; /**
* <p>
* DataLog切面
* </p>
*
* @author Tophua
* @since 2020/7/15
*/
@Aspect
@Order()
@Component
@AllArgsConstructor
public class DataLogAspect { private final DataSource dataSource; private static final Map<String, List<DataTem>> TEM_MAP = new ConcurrentHashMap<>(); /**
* <p>
* 判断线程是否需要记录日志
* </p>
*
* @param threadName threadName
* @return boolean
* @author Tophua
* @since 2020/7/15
*/
public static boolean hasThread(String threadName) {
return TEM_MAP.containsKey(threadName);
} /**
* <p>
* 增加线程数据库操作
* </p>
*
* @param threadName threadName
* @param dataTem dataTem
* @return void
* @author Tophua
* @since 2020/7/15
*/
public static void put(String threadName, DataTem dataTem) {
if (TEM_MAP.containsKey(threadName)) {
TEM_MAP.get(threadName).add(dataTem);
}
} /**
* <p>
* 切面前执行
* </p>
*
* @param dataLog dataLog
* @return void
* @author Tophua
* @since 2020/7/15
*/
@SneakyThrows
@Before("@annotation(dataLog)")
public void before(DataLog dataLog) {
// 获取线程名,使用线程名作为同一次操作记录
String threadName = Thread.currentThread().getName();
TEM_MAP.put(threadName, new LinkedList<>());
} /**
* <p>
* 切面后执行
* </p>
*
* @param dataLog dataLog
* @return void
* @author Tophua
* @since 2020/7/15
*/
@SneakyThrows
@After("@annotation(dataLog)")
public void after(DataLog dataLog) {
// 获取线程名,使用线程名作为同一次操作记录
String threadName = Thread.currentThread().getName();
List<DataTem> list = TEM_MAP.get(threadName);
if (CollUtil.isEmpty(list)) {
return;
}
list.forEach(dataTem -> {
List<?> oldData = dataTem.getOldData();
if (CollUtil.isEmpty(oldData)) {
return;
}
String ids = oldData.stream().map(o -> {
try {
Method method = o.getClass().getMethod("getId");
return method.invoke(o).toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}).filter(ObjectUtil::isNotNull).collect(Collectors.joining(","));
String sql = dataTem.getSql() + "(" + ids + ")";
try {
List<?> newData = Db.use(dataSource).query(sql, dataTem.getEntityType());
dataTem.setNewData(newData);
System.out.println("oldData:" + JSONUtil.toJsonStr(dataTem.getOldData()));
System.out.println("newData:" + JSONUtil.toJsonStr(dataTem.getNewData())); } catch (SQLException e) {
e.printStackTrace();
}
});
// 异步对比存库
this.compareAndSave(list);
} /**
* <p>
* 对比保存
* </p>
*
* @param list list
* @return void
* @author Tophua
* @since 2020/7/15
*/
@Async
public void compareAndSave(List<DataTem> list) {
StringBuilder sb = new StringBuilder();
list.forEach(dataTem -> {
List<?> oldData = dataTem.getOldData();
List<?> newData = dataTem.getNewData();
// 按id排序
oldData.sort(Comparator.comparingLong(d -> {
try {
Method method = d.getClass().getMethod("getId");
return Long.parseLong(method.invoke(d).toString());
} catch (Exception e) {
e.printStackTrace();
}
return 0L;
}));
newData.sort(Comparator.comparingLong(d -> {
try {
Method method = d.getClass().getMethod("getId");
return Long.parseLong(method.invoke(d).toString());
} catch (Exception e) {
e.printStackTrace();
}
return 0L;
})); for (int i = ; i < oldData.size(); i++) {
final int[] finalI = {};
sameClazzDiff(oldData.get(i), newData.get(i)).forEach(r -> {
if (finalI[] == ) {
sb.append(StrUtil.LF);
sb.append(StrUtil.format("修改表:【{}】", dataTem.getTableName()));
sb.append(StrUtil.format("id:【{}】", r.getId()));
}
sb.append(StrUtil.LF);
sb.append(StrUtil.format("把字段[{}]从[{}]改为[{}]", r.getFieldName(), r.getOldValue(), r.getNewValue()));
finalI[]++;
});
}
});
if (sb.length() > ) {
sb.deleteCharAt();
}
// 存库
System.err.println(sb.toString());
} /**
* <p>
* 相同类对比
* </p>
*
* @param obj1 obj1
* @param obj2 obj2
* @return java.util.List<com.lith.datalog.handle.CompareResult>
* @author Tophua
* @since 2020/7/15
*/
private List<CompareResult> sameClazzDiff(Object obj1, Object obj2) {
List<CompareResult> results = new ArrayList<>();
Field[] obj1Fields = obj1.getClass().getDeclaredFields();
Field[] obj2Fields = obj2.getClass().getDeclaredFields();
Long id = null;
for (int i = ; i < obj1Fields.length; i++) {
obj1Fields[i].setAccessible(true);
obj2Fields[i].setAccessible(true);
Field field = obj1Fields[i];
try {
Object value1 = obj1Fields[i].get(obj1);
Object value2 = obj2Fields[i].get(obj2);
if ("id".equals(field.getName())) {
id = Long.parseLong(value1.toString());
}
if (!ObjectUtil.equal(value1, value2)) {
CompareResult r = new CompareResult();
r.setId(id);
r.setFieldName(field.getName());
// 获取注释
r.setFieldComment(field.getName());
r.setOldValue(value1);
r.setNewValue(value2);
results.add(r);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return results;
} }

SpringCloud或SpringBoot+Mybatis-Plus利用AOP+mybatis插件实现数据操作记录及更新对比的更多相关文章

  1. SpringCloud或SpringBoot+Mybatis-Plus利用mybatis插件实现数据操作记录及更新对比

    引文 本文主要介绍如何使用mybatis插件实现拦截数据库操作并根据不同需求进行数据对比分析,主要适用于系统中需要对数据操作进行记录.在更新数据时准确记录更新字段 核心:mybatis插件(拦截器). ...

  2. SpringBoot31 整合SpringJDBC、整合MyBatis、利用AOP实现多数据源

    一.整合SpringJDBC 1  JDBC JDBC(Java Data Base Connectivity,Java 数据库连接)是一种用于执行 SQL 语句的 Java API,可以为多种关系数 ...

  3. Mysql之binlog日志说明及利用binlog日志恢复数据操作记录

    众所周知,binlog日志对于mysql数据库来说是十分重要的.在数据丢失的紧急情况下,我们往往会想到用binlog日志功能进行数据恢复(定时全备份+binlog日志恢复增量数据部分),化险为夷! 一 ...

  4. 【转】Mysql之binlog日志说明及利用binlog日志恢复数据操作记录

    众所周知,binlog日志对于mysql数据库来说是十分重要的.在数据丢失的紧急情况下,我们往往会想到用binlog日志功能进行数据恢复(定时全备份+binlog日志恢复增量数据部分),化险为夷! 废 ...

  5. 利用mk-table-checksum监测Mysql主从数据一致性操作记录

    前面已经提到了mysql主从环境下数据一致性检查:mysql主从同步(3)-percona-toolkit工具(数据一致性监测.延迟监控)使用梳理今天这里再介绍另一种Mysql数据一致性自动检测工具: ...

  6. 大数据作业之利用MapRedeuce实现简单的数据操作

    Map/Reduce编程作业 现有student.txt和student_score.txt.将两个文件上传到hdfs上.使用Map/Reduce框架完成下面的题目 student.txt 20160 ...

  7. 利用org.mybatis.generator生成实体类

    springboot+maven+mybatis+mysql 利用org.mybatis.generator生成实体类 1.添加pom依赖:   2.编写generatorConfig.xml文件 ( ...

  8. Mysql利用binlog日志恢复数据操作(转)

    a.开启binlog日志:1)编辑打开mysql配置文件/etc/mys.cnf[root@vm-002 ~]# vim /etc/my.cnf在[mysqld] 区块添加 log-bin=mysql ...

  9. SpringBoot整合Mybatis多数据源 (AOP+注解)

    SpringBoot整合Mybatis多数据源 (AOP+注解) 1.pom.xml文件(开发用的JDK 10) <?xml version="1.0" encoding=& ...

随机推荐

  1. python_lesson1 数学与随机数 (math包,random包)

    math包 math包主要处理数学相关的运算.math包定义了两个常数: math.e   # 自然常数e math.pi  # 圆周率pi   此外,math包还有各种运算函数 (下面函数的功能可以 ...

  2. StringEscapeUtils防止xss攻击详解

    StringUtils和StringEscapeUtils这两个实用类. 1.转义防止xss攻击 1.转义可以分为下面的几种情况 第一用户输入特殊字符的时候,在提及的时候不做任何处理保持到数据库,当用 ...

  3. jni不通过线程c回调java的函数 --总结

    1.JNIEnv类型是一个指向全部JNI方法的指针.该指针只在创建它的线程有效,不能跨线程传递 2.JavaVM是虚拟机在JNI中的表示,一个JVM中只有一个JavaVM对象,这个对象是线程共享的. ...

  4. xeus-clickhouse: Jupyter 的 ClickHouse 内核

    在科学计算领域,Jupyter 是一个使用非常广泛的集成开发环境,它支持多种主流的编程语言比如 Python, C++, R 或者 Julia.同时,数据科学最重要的还是数据,而 SQL 是操作数据最 ...

  5. 并发04--JAVA中的锁

    1.Lock接口 Lock与Synchronized实现效果一致,通过获得锁.释放锁等操作来控制多个线程访问共享资源,但是Synchronized将获取锁固话,必须先获得锁,再执行,因此两者对比来说, ...

  6. js语法基础入门(1.2)

    1.4.查找元素的方法 1.4.1.查找元素的方法 JavaScript可以去操作html元素,要实现对html元素的操作,首选应该找到这个元素,有点类似于css中的选择器 html代码: <d ...

  7. 全栈的自我修养: 001环境搭建 (使用Vue,Spring Boot,Flask,Django 完成Vue前后端分离开发)

    全栈的自我修养: 环境搭建 Not all those who wander are lost. 彷徨者并非都迷失方向. Table of Contents @ 目录 前言 环境准备 nodejs v ...

  8. 利用oracle数据库闪回功能将oracle数据库按时间点恢复

    oracle更新脚本把原数据冲了,并且没有备份,急煞我也         解决办法:         oracle数据库有闪回功能:   select * from tab 可以查出已被删除的表    ...

  9. apply()方法和call()介绍

    我们发现apply()和call()的真正用武之地是能够扩充函数赖以运行的作用域. 1.call,apply都属于Function.prototype的一个方法,它是JavaScript引擎内在实现的 ...

  10. JavaScript基础Curry化(021)

    时候我们希望函数可以分步接受参数,并在所有参数都到位后得到执行结果.为了实现这种机制,我们先了解函数在Javascript中的应用过程: 1. 函数的“应用”(Function Application ...