引文
本文主要介绍如何使用Spring AOP + mybatis插件实现拦截数据库操作并根据不同需求进行数据对比分析,主要适用于系统中需要对数据操作进行记录、在更新数据时准确记录更新字段
核心:AOP、mybatis插件(拦截器)、mybatis-Plus实体规范、数据对比
mybatis插件实际上就是官方针对4层数据操作处理预留的拦截器,使用者可以根据不同的需求进行操作拦截并处理。这边笔者不做详细描述,详细介绍请到 官网 了解,这里笔者就复用官网介绍。
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 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-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;
}
本文所要讲述的就是在第一级(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实现拦截器,在拦截器中根据增、删、改操作去调用各个模块中自定义实现的处理方法来达到不同的操作处理。
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 切点注解
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 }
采用方法执行前后进行处理
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 }
经过测试,不管怎么使用数据更新操作,结果都可以进行拦截记录,完美达到预期。
小笔这里并没有将记录保存在数据库,由大家自行保存。
测试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]
本次综合前车经验,优化设计思想,改为从底层具体执行的 sql 语句入手,通过解析表名及更新条件来构造数据更新前后的查询sql,再使用Spring AOP对方法执行前后进行处理,记录更新前后的数据。最后再使用java反射机制将数据更新前后进行对比记录。
注:
使用AOP涉及到一点,就是需要保证AOP与Spring 数据库事务之间的执行顺序,如果AOP先执行然后再提交事务,那结果则是数据无变化。
在此小笔已将AOP处理级别放到最后,保证先提交事务再去查询更新后的数据,这样才能得出正确的结果。
欢迎各路大神交流意见。。。。。。
最后附上源码地址:
https://gitee.com/TopSkyhua/datalog