SpringCloud或SpringBoot+Mybatis-Plus利用AOP+mybatis插件实现数据操作记录及更新对比
引文
本文主要介绍如何使用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、实现
一、配置
- 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实现拦截器,在拦截器中根据增、删、改操作去调用各个模块中自定义实现的处理方法来达到不同的操作处理。
- 1 package com.lith.datalog.handle;
- 2
- 3 import cn.hutool.db.Db;
- 4 import com.baomidou.mybatisplus.core.metadata.TableInfo;
- 5 import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
- 6 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
- 7 import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
- 8 import com.baomidou.mybatisplus.core.toolkit.StringPool;
- 9 import com.baomidou.mybatisplus.core.toolkit.TableNameParser;
- 10 import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
- 11 import com.lith.datalog.aspect.DataLogAspect;
- 12 import com.lith.datalog.aspect.DataTem;
- 13 import lombok.AllArgsConstructor;
- 14 import lombok.extern.slf4j.Slf4j;
- 15 import org.apache.ibatis.executor.statement.StatementHandler;
- 16 import org.apache.ibatis.mapping.MappedStatement;
- 17 import org.apache.ibatis.mapping.SqlCommandType;
- 18 import org.apache.ibatis.plugin.Interceptor;
- 19 import org.apache.ibatis.plugin.Intercepts;
- 20 import org.apache.ibatis.plugin.Invocation;
- 21 import org.apache.ibatis.plugin.Signature;
- 22 import org.apache.ibatis.reflection.MetaObject;
- 23 import org.apache.ibatis.reflection.SystemMetaObject;
- 24
- 25 import javax.sql.DataSource;
- 26 import java.lang.reflect.Proxy;
- 27 import java.sql.Statement;
- 28 import java.util.*;
- 29
- 30 /**
- 31 * <p>
- 32 * 数据更新拦截器
- 33 * </p>
- 34 *
- 35 * @author Tophua
- 36 * @since 2020/5/11
- 37 */
- 38 @Slf4j
- 39 @AllArgsConstructor
- 40 @Intercepts({@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})})
- 41 public class DataUpdateInterceptor extends AbstractSqlParserHandler implements Interceptor {
- 42 private final DataSource dataSource;
- 43
- 44 @Override
- 45 public Object intercept(Invocation invocation) throws Throwable {
- 46 // 获取线程名,使用线程名作为同一次操作记录
- 47 String threadName = Thread.currentThread().getName();
- 48 // 判断是否需要记录日志
- 49 if (!DataLogAspect.hasThread(threadName)) {
- 50 return invocation.proceed();
- 51 }
- 52 Statement statement;
- 53 Object firstArg = invocation.getArgs()[0];
- 54 if (Proxy.isProxyClass(firstArg.getClass())) {
- 55 statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
- 56 } else {
- 57 statement = (Statement) firstArg;
- 58 }
- 59 MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);
- 60 try {
- 61 statement = (Statement) stmtMetaObj.getValue("stmt.statement");
- 62 } catch (Exception e) {
- 63 // do nothing
- 64 }
- 65 if (stmtMetaObj.hasGetter("delegate")) {
- 66 //Hikari
- 67 try {
- 68 statement = (Statement) stmtMetaObj.getValue("delegate");
- 69 } catch (Exception ignored) {
- 70
- 71 }
- 72 }
- 73
- 74 String originalSql = statement.toString();
- 75 originalSql = originalSql.replaceAll("[\\s]+", StringPool.SPACE);
- 76 int index = indexOfSqlStart(originalSql);
- 77 if (index > 0) {
- 78 originalSql = originalSql.substring(index);
- 79 }
- 80
- 81 StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
- 82 MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
- 83 this.sqlParser(metaObject);
- 84 MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
- 85
- 86 // 获取执行Sql
- 87 String sql = originalSql.replace("where", "WHERE");
- 88 // 插入
- 89 if (SqlCommandType.INSERT.equals(mappedStatement.getSqlCommandType())) {
- 90 }
- 91 // 更新
- 92 if (SqlCommandType.UPDATE.equals(mappedStatement.getSqlCommandType())) {
- 93 try {
- 94 // 使用mybatis-plus 工具解析sql获取表名
- 95 Collection<String> tables = new TableNameParser(sql).tables();
- 96 if (CollectionUtils.isEmpty(tables)) {
- 97 return invocation.proceed();
- 98 }
- 99 String tableName = tables.iterator().next();
- 100 // 使用mybatis-plus 工具根据表名找出对应的实体类
- 101 Class<?> entityType = TableInfoHelper.getTableInfos().stream().filter(t -> t.getTableName().equals(tableName))
- 102 .findFirst().orElse(new TableInfo(null)).getEntityType();
- 103
- 104 DataTem dataTem = new DataTem();
- 105 dataTem.setTableName(tableName);
- 106 dataTem.setEntityType(entityType);
- 107 // 设置sql用于执行完后查询新数据
- 108 dataTem.setSql("SELECT * FROM " + tableName + " WHERE id in ");
- 109 String selectSql = "SELECT * FROM " + tableName + " " + sql.substring(sql.lastIndexOf("WHERE"));
- 110 // 查询更新前数据
- 111 List<?> oldData = Db.use(dataSource).query(selectSql, entityType);
- 112 dataTem.setOldData(oldData);
- 113 DataLogAspect.put(threadName, dataTem);
- 114 } catch (Exception e) {
- 115 e.printStackTrace();
- 116 }
- 117 }
- 118 // 删除
- 119 if (SqlCommandType.DELETE.equals(mappedStatement.getSqlCommandType())) {
- 120 }
- 121 return invocation.proceed();
- 122 }
- 123
- 124 /**
- 125 * 获取sql语句开头部分
- 126 *
- 127 * @param sql ignore
- 128 * @return ignore
- 129 */
- 130 private int indexOfSqlStart(String sql) {
- 131 String upperCaseSql = sql.toUpperCase();
- 132 Set<Integer> set = new HashSet<>();
- 133 set.add(upperCaseSql.indexOf("SELECT "));
- 134 set.add(upperCaseSql.indexOf("UPDATE "));
- 135 set.add(upperCaseSql.indexOf("INSERT "));
- 136 set.add(upperCaseSql.indexOf("DELETE "));
- 137 set.remove(-1);
- 138 if (CollectionUtils.isEmpty(set)) {
- 139 return -1;
- 140 }
- 141 List<Integer> list = new ArrayList<>(set);
- 142 list.sort(Comparator.naturalOrder());
- 143 return list.get(0);
- 144 }
- 145 }
二、AOP
使用AOP主要是考虑到一个方法中会出现多次数据库操作,而这些操作在记录中只能算作用户的一次操作,故使用AOP进行操作隔离,将一个方法内的所有数据库操作合并为一次记录。
此外AOP还代表着是否需要记录日志,有切点才会进行记录。
AOP 切点注解
- 1 package com.lith.datalog.annotation;
- 2
- 3 import java.lang.annotation.ElementType;
- 4 import java.lang.annotation.Retention;
- 5 import java.lang.annotation.RetentionPolicy;
- 6 import java.lang.annotation.Target;
- 7
- 8 /**
- 9 * <p>
- 10 * 数据日志
- 11 * </p>
- 12 *
- 13 * @author Tophua
- 14 * @since 2020/7/15
- 15 */
- 16 @Target(ElementType.METHOD)
- 17 @Retention(RetentionPolicy.RUNTIME)
- 18 public @interface DataLog {
- 19 }
三、AOP切面实现
采用方法执行前后进行处理
- 1 package com.lith.datalog.aspect;
- 2
- 3 import cn.hutool.core.collection.CollUtil;
- 4 import cn.hutool.core.util.ObjectUtil;
- 5 import cn.hutool.core.util.StrUtil;
- 6 import cn.hutool.db.Db;
- 7 import cn.hutool.json.JSONUtil;
- 8 import com.lith.datalog.annotation.DataLog;
- 9 import com.lith.datalog.handle.CompareResult;
- 10 import lombok.AllArgsConstructor;
- 11 import lombok.SneakyThrows;
- 12 import org.aspectj.lang.annotation.After;
- 13 import org.aspectj.lang.annotation.Aspect;
- 14 import org.aspectj.lang.annotation.Before;
- 15 import org.springframework.core.annotation.Order;
- 16 import org.springframework.scheduling.annotation.Async;
- 17 import org.springframework.stereotype.Component;
- 18
- 19 import javax.sql.DataSource;
- 20 import java.lang.reflect.Field;
- 21 import java.lang.reflect.Method;
- 22 import java.sql.SQLException;
- 23 import java.util.*;
- 24 import java.util.concurrent.ConcurrentHashMap;
- 25 import java.util.stream.Collectors;
- 26
- 27 /**
- 28 * <p>
- 29 * DataLog切面
- 30 * </p>
- 31 *
- 32 * @author Tophua
- 33 * @since 2020/7/15
- 34 */
- 35 @Aspect
- 36 @Order(99)
- 37 @Component
- 38 @AllArgsConstructor
- 39 public class DataLogAspect {
- 40
- 41 private final DataSource dataSource;
- 42
- 43 private static final Map<String, List<DataTem>> TEM_MAP = new ConcurrentHashMap<>();
- 44
- 45 /**
- 46 * <p>
- 47 * 判断线程是否需要记录日志
- 48 * </p>
- 49 *
- 50 * @param threadName threadName
- 51 * @return boolean
- 52 * @author Tophua
- 53 * @since 2020/7/15
- 54 */
- 55 public static boolean hasThread(String threadName) {
- 56 return TEM_MAP.containsKey(threadName);
- 57 }
- 58
- 59 /**
- 60 * <p>
- 61 * 增加线程数据库操作
- 62 * </p>
- 63 *
- 64 * @param threadName threadName
- 65 * @param dataTem dataTem
- 66 * @return void
- 67 * @author Tophua
- 68 * @since 2020/7/15
- 69 */
- 70 public static void put(String threadName, DataTem dataTem) {
- 71 if (TEM_MAP.containsKey(threadName)) {
- 72 TEM_MAP.get(threadName).add(dataTem);
- 73 }
- 74 }
- 75
- 76 /**
- 77 * <p>
- 78 * 切面前执行
- 79 * </p>
- 80 *
- 81 * @param dataLog dataLog
- 82 * @return void
- 83 * @author Tophua
- 84 * @since 2020/7/15
- 85 */
- 86 @SneakyThrows
- 87 @Before("@annotation(dataLog)")
- 88 public void before(DataLog dataLog) {
- 89 // 获取线程名,使用线程名作为同一次操作记录
- 90 String threadName = Thread.currentThread().getName();
- 91 TEM_MAP.put(threadName, new LinkedList<>());
- 92 }
- 93
- 94 /**
- 95 * <p>
- 96 * 切面后执行
- 97 * </p>
- 98 *
- 99 * @param dataLog dataLog
- 100 * @return void
- 101 * @author Tophua
- 102 * @since 2020/7/15
- 103 */
- 104 @SneakyThrows
- 105 @After("@annotation(dataLog)")
- 106 public void after(DataLog dataLog) {
- 107 // 获取线程名,使用线程名作为同一次操作记录
- 108 String threadName = Thread.currentThread().getName();
- 109 List<DataTem> list = TEM_MAP.get(threadName);
- 110 if (CollUtil.isEmpty(list)) {
- 111 return;
- 112 }
- 113 list.forEach(dataTem -> {
- 114 List<?> oldData = dataTem.getOldData();
- 115 if (CollUtil.isEmpty(oldData)) {
- 116 return;
- 117 }
- 118 String ids = oldData.stream().map(o -> {
- 119 try {
- 120 Method method = o.getClass().getMethod("getId");
- 121 return method.invoke(o).toString();
- 122 } catch (Exception e) {
- 123 e.printStackTrace();
- 124 return null;
- 125 }
- 126 }).filter(ObjectUtil::isNotNull).collect(Collectors.joining(","));
- 127 String sql = dataTem.getSql() + "(" + ids + ")";
- 128 try {
- 129 List<?> newData = Db.use(dataSource).query(sql, dataTem.getEntityType());
- 130 dataTem.setNewData(newData);
- 131 System.out.println("oldData:" + JSONUtil.toJsonStr(dataTem.getOldData()));
- 132 System.out.println("newData:" + JSONUtil.toJsonStr(dataTem.getNewData()));
- 133
- 134 } catch (SQLException e) {
- 135 e.printStackTrace();
- 136 }
- 137 });
- 138 // 异步对比存库
- 139 this.compareAndSave(list);
- 140 }
- 141
- 142 /**
- 143 * <p>
- 144 * 对比保存
- 145 * </p>
- 146 *
- 147 * @param list list
- 148 * @return void
- 149 * @author Tophua
- 150 * @since 2020/7/15
- 151 */
- 152 @Async
- 153 public void compareAndSave(List<DataTem> list) {
- 154 StringBuilder sb = new StringBuilder();
- 155 list.forEach(dataTem -> {
- 156 List<?> oldData = dataTem.getOldData();
- 157 List<?> newData = dataTem.getNewData();
- 158 // 按id排序
- 159 oldData.sort(Comparator.comparingLong(d -> {
- 160 try {
- 161 Method method = d.getClass().getMethod("getId");
- 162 return Long.parseLong(method.invoke(d).toString());
- 163 } catch (Exception e) {
- 164 e.printStackTrace();
- 165 }
- 166 return 0L;
- 167 }));
- 168 newData.sort(Comparator.comparingLong(d -> {
- 169 try {
- 170 Method method = d.getClass().getMethod("getId");
- 171 return Long.parseLong(method.invoke(d).toString());
- 172 } catch (Exception e) {
- 173 e.printStackTrace();
- 174 }
- 175 return 0L;
- 176 }));
- 177
- 178 for (int i = 0; i < oldData.size(); i++) {
- 179 final int[] finalI = {0};
- 180 sameClazzDiff(oldData.get(i), newData.get(i)).forEach(r -> {
- 181 if (finalI[0] == 0) {
- 182 sb.append(StrUtil.LF);
- 183 sb.append(StrUtil.format("修改表:【{}】", dataTem.getTableName()));
- 184 sb.append(StrUtil.format("id:【{}】", r.getId()));
- 185 }
- 186 sb.append(StrUtil.LF);
- 187 sb.append(StrUtil.format("把字段[{}]从[{}]改为[{}]", r.getFieldName(), r.getOldValue(), r.getNewValue()));
- 188 finalI[0]++;
- 189 });
- 190 }
- 191 });
- 192 if (sb.length() > 0) {
- 193 sb.deleteCharAt(0);
- 194 }
- 195 // 存库
- 196 System.err.println(sb.toString());
- 197 }
- 198
- 199 /**
- 200 * <p>
- 201 * 相同类对比
- 202 * </p>
- 203 *
- 204 * @param obj1 obj1
- 205 * @param obj2 obj2
- 206 * @return java.util.List<com.lith.datalog.handle.CompareResult>
- 207 * @author Tophua
- 208 * @since 2020/7/15
- 209 */
- 210 private List<CompareResult> sameClazzDiff(Object obj1, Object obj2) {
- 211 List<CompareResult> results = new ArrayList<>();
- 212 Field[] obj1Fields = obj1.getClass().getDeclaredFields();
- 213 Field[] obj2Fields = obj2.getClass().getDeclaredFields();
- 214 Long id = null;
- 215 for (int i = 0; i < obj1Fields.length; i++) {
- 216 obj1Fields[i].setAccessible(true);
- 217 obj2Fields[i].setAccessible(true);
- 218 Field field = obj1Fields[i];
- 219 try {
- 220 Object value1 = obj1Fields[i].get(obj1);
- 221 Object value2 = obj2Fields[i].get(obj2);
- 222 if ("id".equals(field.getName())) {
- 223 id = Long.parseLong(value1.toString());
- 224 }
- 225 if (!ObjectUtil.equal(value1, value2)) {
- 226 CompareResult r = new CompareResult();
- 227 r.setId(id);
- 228 r.setFieldName(field.getName());
- 229 // 获取注释
- 230 r.setFieldComment(field.getName());
- 231 r.setOldValue(value1);
- 232 r.setNewValue(value2);
- 233 results.add(r);
- 234 }
- 235 } catch (IllegalAccessException e) {
- 236 e.printStackTrace();
- 237 }
- 238 }
- 239 return results;
- 240 }
- 241
- 242 }
3、测试及结果
经过测试,不管怎么使用数据更新操作,结果都可以进行拦截记录,完美达到预期。
小笔这里并没有将记录保存在数据库,由大家自行保存。
测试demo
- 1 package com.lith.datalog.controller;
- 2
- 3 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
- 4 import com.lith.datalog.annotation.DataLog;
- 5 import com.lith.datalog.entity.User;
- 6 import com.lith.datalog.mapper.UserMapper;
- 7 import com.lith.datalog.service.UserService;
- 8 import lombok.AllArgsConstructor;
- 9 import org.springframework.transaction.annotation.Transactional;
- 10 import org.springframework.web.bind.annotation.*;
- 11
- 12 /**
- 13 * <p>
- 14 * UserController
- 15 * </p>
- 16 *
- 17 * @author Tophua
- 18 * @since 2020/5/7
- 19 */
- 20 @RestController
- 21 @AllArgsConstructor
- 22 @RequestMapping("/user")
- 23 public class UserController {
- 24
- 25 private final UserService userService;
- 26 private final UserMapper userMapper;
- 27
- 28 @GetMapping("{id}")
- 29 public User getById(@PathVariable Integer id) {
- 30 return userService.getById(id);
- 31 }
- 32
- 33 @DataLog
- 34 @PostMapping
- 35 public Boolean save(@RequestBody User user) {
- 36 return userService.save(user);
- 37 }
- 38
- 39 @DataLog
- 40 @PutMapping
- 41 @Transactional(rollbackFor = Exception.class)
- 42 public Boolean updateById(@RequestBody User user) {
- 43 User nUser = new User();
- 44 nUser.setId(2);
- 45 nUser.setName("代码更新");
- 46 nUser.updateById();
- 47 userService.update(Wrappers.<User>lambdaUpdate()
- 48 .set(User::getName, "批量")
- 49 .in(User::getId, 3, 4));
- 50 userMapper.updateTest();
- 51 return userService.updateById(user);
- 52 }
- 53
- 54 @DeleteMapping("{id}")
- 55 public Boolean removeById(@PathVariable Integer id) {
- 56 return userService.removeById(id);
- 57 }
- 58 }
结果显示:
- 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(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;
}
}