引文
本文主要介绍如何使用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;
}
}
上面的插件将会拦截在 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;
/**
*
* 用户表
*
*
* @author Tophua
* @since 2020/5/7
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends Model {
/**
* 主键id
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String name;
private Integer age;
private String email;
}
2、实现
本文所要讲述的就是在第一级(Executor)进行拦截并实现数据对比记录。
本例为公共模块实现,然后在其它模块中依赖此公共模块,根据每个模块不同的需求自定义实现不同的处理。
结构目录
一、配置
1 packagecom.lith.datalog.config;
2
3 importcom.lith.datalog.handle.DataUpdateInterceptor;
4 importorg.mybatis.spring.annotation.MapperScan;
5 importorg.springframework.context.annotation.Bean;
6 importorg.springframework.context.annotation.Configuration;
7 importorg.springframework.context.annotation.Profile;
8 importorg.springframework.transaction.annotation.EnableTransactionManagement;
9
10 importjavax.sql.DataSource;
11
12 /**
13 *
14 * Mybatis-Plus配置
15 *
16 *
17 * @author Tophua
18 * @since 2020/5/7
19 */
20 @Configuration
21 @EnableTransactionManagement
22 @MapperScan("com.lith.**.mapper")
23 public classMybatisPlusConfig {
24
25 /**
26 *
27 * SQL执行效率插件 设置 dev test 环境开启
28 *
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 publicPerformanceInterceptor performanceInterceptor() {
37 return newPerformanceInterceptor();
38 }
39
40 /**
41 *
42 * 数据更新操作处理
43 *
44 *
45 * @return com.lith.datalog.handle.DataUpdateInterceptor
46 * @author Tophua
47 * @since 2020/5/11
48 */
49 @Bean
50 @Profile({"dev","test"})
51 publicDataUpdateInterceptor dataUpdateInterceptor(DataSource dataSource) {
52 return newDataUpdateInterceptor(dataSource);
53 }
54 }
二、实现拦截器
DataUpdateInterceptor,根据官网demo实现拦截器,在拦截器中根据增、删、改操作去调用各个模块中自定义实现的处理方法来达到不同的操作处理。
1 packagecom.lith.datalog.handle;2
3 importcn.hutool.db.Db;4 importcom.baomidou.mybatisplus.core.metadata.TableInfo;5 importcom.baomidou.mybatisplus.core.metadata.TableInfoHelper;6 importcom.baomidou.mybatisplus.core.toolkit.CollectionUtils;7 importcom.baomidou.mybatisplus.core.toolkit.PluginUtils;8 importcom.baomidou.mybatisplus.core.toolkit.StringPool;9 importcom.baomidou.mybatisplus.core.toolkit.TableNameParser;10 importcom.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;11 importcom.lith.datalog.aspect.DataLogAspect;12 importcom.lith.datalog.aspect.DataTem;13 importlombok.AllArgsConstructor;14 importlombok.extern.slf4j.Slf4j;15 importorg.apache.ibatis.executor.statement.StatementHandler;16 importorg.apache.ibatis.mapping.MappedStatement;17 importorg.apache.ibatis.mapping.SqlCommandType;18 importorg.apache.ibatis.plugin.Interceptor;19 importorg.apache.ibatis.plugin.Intercepts;20 importorg.apache.ibatis.plugin.Invocation;21 importorg.apache.ibatis.plugin.Signature;22 importorg.apache.ibatis.reflection.MetaObject;23 importorg.apache.ibatis.reflection.SystemMetaObject;24
25 importjavax.sql.DataSource;26 importjava.lang.reflect.Proxy;27 importjava.sql.Statement;28 import java.util.*;29
30 /**
31 *
32 * 数据更新拦截器33 *
34 *35 *@authorTophua36 *@since2020/5/1137 */38 @Slf4j39 @AllArgsConstructor40 @Intercepts({@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})})41 public class DataUpdateInterceptor extends AbstractSqlParserHandler implementsInterceptor {42 private finalDataSource dataSource;43
44 @Override45 public Object intercept(Invocation invocation) throwsThrowable {46 //获取线程名,使用线程名作为同一次操作记录
47 String threadName =Thread.currentThread().getName();48 //判断是否需要记录日志
49 if (!DataLogAspect.hasThread(threadName)) {50 returninvocation.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 tables = newTableNameParser(sql).tables();96 if(CollectionUtils.isEmpty(tables)) {97 returninvocation.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 = newDataTem();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 returninvocation.proceed();122 }123
124 /**
125 * 获取sql语句开头部分126 *127 *@paramsql ignore128 *@returnignore129 */
130 private intindexOfSqlStart(String sql) {131 String upperCaseSql =sql.toUpperCase();132 Set 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 list = new ArrayList<>(set);142 list.sort(Comparator.naturalOrder());143 return list.get(0);144 }145 }
二、AOP
使用AOP主要是考虑到一个方法中会出现多次数据库操作,而这些操作在记录中只能算作用户的一次操作,故使用AOP进行操作隔离,将一个方法内的所有数据库操作合并为一次记录。
此外AOP还代表着是否需要记录日志,有切点才会进行记录。
AOP 切点注解
1 packagecom.lith.datalog.annotation;2
3 importjava.lang.annotation.ElementType;4 importjava.lang.annotation.Retention;5 importjava.lang.annotation.RetentionPolicy;6 importjava.lang.annotation.Target;7
8 /**
9 *
10 * 数据日志11 *
12 *13 *@authorTophua14 *@since2020/7/1515 */16 @Target(ElementType.METHOD)17 @Retention(RetentionPolicy.RUNTIME)18 public @interfaceDataLog {19 }
三、AOP切面实现
采用方法执行前后进行处理
1 packagecom.lith.datalog.aspect;2
3 importcn.hutool.core.collection.CollUtil;4 importcn.hutool.core.util.ObjectUtil;5 importcn.hutool.core.util.StrUtil;6 importcn.hutool.db.Db;7 importcn.hutool.json.JSONUtil;8 importcom.lith.datalog.annotation.DataLog;9 importcom.lith.datalog.handle.CompareResult;10 importlombok.AllArgsConstructor;11 importlombok.SneakyThrows;12 importorg.aspectj.lang.annotation.After;13 importorg.aspectj.lang.annotation.Aspect;14 importorg.aspectj.lang.annotation.Before;15 importorg.springframework.core.annotation.Order;16 importorg.springframework.scheduling.annotation.Async;17 importorg.springframework.stereotype.Component;18
19 importjavax.sql.DataSource;20 importjava.lang.reflect.Field;21 importjava.lang.reflect.Method;22 importjava.sql.SQLException;23 import java.util.*;24 importjava.util.concurrent.ConcurrentHashMap;25 importjava.util.stream.Collectors;26
27 /**
28 *
29 * DataLog切面30 *
31 *32 *@authorTophua33 *@since2020/7/1534 */35 @Aspect36 @Order(99)37 @Component38 @AllArgsConstructor39 public classDataLogAspect {40
41 private finalDataSource dataSource;42
43 private static final Map> TEM_MAP = new ConcurrentHashMap<>();44
45 /**
46 *
47 * 判断线程是否需要记录日志48 *
49 *50 *@paramthreadName threadName51 *@returnboolean52 *@authorTophua53 *@since2020/7/1554 */55 public static booleanhasThread(String threadName) {56 returnTEM_MAP.containsKey(threadName);57 }58
59 /**
60 *
61 * 增加线程数据库操作62 *
63 *64 *@paramthreadName threadName65 *@paramdataTem dataTem66 *@returnvoid67 *@authorTophua68 *@since2020/7/1569 */70 public static voidput(String threadName, DataTem dataTem) {71 if(TEM_MAP.containsKey(threadName)) {72 TEM_MAP.get(threadName).add(dataTem);73 }74 }75
76 /**
77 *
78 * 切面前执行79 *
80 *81 *@paramdataLog dataLog82 *@returnvoid83 *@authorTophua84 *@since2020/7/1585 */86 @SneakyThrows87 @Before("@annotation(dataLog)")88 public voidbefore(DataLog dataLog) {89 //获取线程名,使用线程名作为同一次操作记录
90 String threadName =Thread.currentThread().getName();91 TEM_MAP.put(threadName, new LinkedList<>());92 }93
94 /**
95 *
96 * 切面后执行97 *
98 *99 *@paramdataLog dataLog100 *@returnvoid101 *@authorTophua102 *@since2020/7/15103 */104 @SneakyThrows105 @After("@annotation(dataLog)")106 public voidafter(DataLog dataLog) {107 //获取线程名,使用线程名作为同一次操作记录
108 String threadName =Thread.currentThread().getName();109 List 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 returnmethod.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 *
144 * 对比保存145 *
146 *147 *@paramlist list148 *@returnvoid149 *@authorTophua150 *@since2020/7/15151 */152 @Async153 public void compareAndSave(Listlist) {154 StringBuilder sb = newStringBuilder();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 returnLong.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 returnLong.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 *
201 * 相同类对比202 *
203 *204 *@paramobj1 obj1205 *@paramobj2 obj2206 *@returnjava.util.List207 *@authorTophua208 *@since2020/7/15209 */210 private ListsameClazzDiff(Object obj1, Object obj2) {211 List 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 = newCompareResult();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 returnresults;240 }241
242 }
3、测试及结果
经过测试,不管怎么使用数据更新操作,结果都可以进行拦截记录,完美达到预期。
小笔这里并没有将记录保存在数据库,由大家自行保存。
测试demo
1 packagecom.lith.datalog.controller;2
3 importcom.baomidou.mybatisplus.core.toolkit.Wrappers;4 importcom.lith.datalog.annotation.DataLog;5 importcom.lith.datalog.entity.User;6 importcom.lith.datalog.mapper.UserMapper;7 importcom.lith.datalog.service.UserService;8 importlombok.AllArgsConstructor;9 importorg.springframework.transaction.annotation.Transactional;10 import org.springframework.web.bind.annotation.*;11
12 /**
13 *
14 * UserController15 *
16 *17 *@authorTophua18 *@since2020/5/719 */20 @RestController21 @AllArgsConstructor22 @RequestMapping("/user")23 public classUserController {24
25 private finalUserService userService;26 private finalUserMapper userMapper;27
28 @GetMapping("{id}")29 publicUser getById(@PathVariable Integer id) {30 returnuserService.getById(id);31 }32
33 @DataLog34 @PostMapping35 publicBoolean save(@RequestBody User user) {36 returnuserService.save(user);37 }38
39 @DataLog40 @PutMapping41 @Transactional(rollbackFor = Exception.class)42 publicBoolean updateById(@RequestBody User user) {43 User nUser = newUser();44 nUser.setId(2);45 nUser.setName("代码更新");46 nUser.updateById();47 userService.update(Wrappers.lambdaUpdate()48 .set(User::getName, "批量")49 .in(User::getId, 3, 4));50 userMapper.updateTest();51 returnuserService.updateById(user);52 }53
54 @DeleteMapping("{id}")55 publicBoolean removeById(@PathVariable Integer id) {56 returnuserService.removeById(id);57 }58 }
结果显示:
Time:2 ms -ID:com.lith.datalog.mapper.UserMapper.updateById
Execute SQL:UPDATE user SET name='代码更新' WHERE id=2Time: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=1oldData:[{"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