引文
本文主要介紹如何使用Spring AOP + mybatis插件實現攔截數據庫操作並根據不同需求進行數據對比分析,主要適用於系統中需要對數據操作進行記錄、在更新數據時准確記錄更新字段
核心:ThreadLocal、AOP、mybatis插件(攔截器)、mybatis-Plus實體規范、數據對比
實現思路
- 使用注解
DataLog
標記需要記錄操作日志的接口或方法 - 進入Aop進行數據初始化,告知mybatis攔截器該線程操作需要記錄操作記錄,並使用
ThreadLocal
進行線程隔離,防止多線程操作時引發記錄錯亂問題 - mybatis插件進行更新sql攔截並記錄更新前數據
- 數據更新完成並在接口或方法正確返回后再次查詢更新后數據
- 進行更新前后數據對比並調用上層處理接口進行數據記錄
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、實現
本文所要講述的就是在第四級(StatementHandler)進行攔截並實現數據對比記錄。
本例為公共模塊實現,然后在其它模塊中依賴此公共模塊,根據每個模塊不同的需求自定義實現不同的處理。
結構目錄
一、配置
package com.lith.datalog.config;
import com.lith.datalog.handle.BaseDataLog;
import com.lith.datalog.handle.DataUpdateInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* <p>
* Mybatis-Plus配置
* </p>
*
* @author Tophua
* @since 2020/5/7
*/
@Configuration
@EnableTransactionManagement
@MapperScan("com.lith.**.mapper")
public class MybatisPlusConfig {
/**
* <p>
* SQL執行效率插件 設置 dev test 環境開啟
* </p>
*
* @return cn.rc100.common.data.mybatis.EplusPerformanceInterceptor
* @author Tophua
* @since 2020/3/11
*/
@Bean
@Profile({"dev","test"})
public PerformanceInterceptor performanceInterceptor() {
return new PerformanceInterceptor();
}
/**
* <p>
* 數據更新操作處理
* </p>
*
* @return com.lith.datalog.handle.DataUpdateInterceptor
* @author Tophua
* @since 2020/5/11
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(BaseDataLog.class)
public DataUpdateInterceptor dataUpdateInterceptor() {
return new DataUpdateInterceptor();
}
}
二、實現攔截器
DataUpdateInterceptor
,根據官網demo實現攔截器,在攔截器中根據增、刪、改操作去調用各個模塊中自定義實現的處理方法來達到不同的操作處理。
package com.lith.datalog.handle;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.*;
import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import com.lith.datalog.annotation.IgnoreDataLog;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
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 org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionUtils;
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 {
@Override
@SneakyThrows
public Object intercept(Invocation invocation) {
// 判斷是否需要記錄日志
if (BaseDataLog.DATA_CHANGES.get() == null) {
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);
}
System.err.println("執行SQL:" + originalSql);
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())) {
// 使用mybatis-plus 工具解析sql獲取表名
Collection<String> tables = new TableNameParser(sql).tables();
if (CollectionUtils.isEmpty(tables)) {
return invocation.proceed();
}
String tableName = tables.iterator().next();
// 排除表名判斷
if (BaseDataLog.excludeTableNames.contains(tableName)) {
return invocation.proceed();
}
// 使用mybatis-plus 工具根據表名找出對應的實體類
TableInfo tableInfo = Optional.ofNullable(TableInfoHelper.getTableInfo(tableName))
.orElse(new TableInfo(null));
Class<?> entityType = tableInfo.getEntityType();
if (entityType == null || entityType.isAnnotationPresent(IgnoreDataLog.class)) {
return invocation.proceed();
}
DataChange change = new DataChange();
change.setTableName(tableName);
change.setEntityType(entityType);
// 設置sql用於執行完后查詢新數據
String selectSql = "AND " + sql.substring(sql.lastIndexOf("WHERE") + 5);
// 同表對同條數據操作多次只進行一次對比
if (BaseDataLog.DATA_CHANGES.get().stream().anyMatch(c -> tableName.equals(c.getTableName())
&& selectSql.equals(c.getWhereSql()))) {
return invocation.proceed();
}
change.setWhereSql(selectSql);
Map<String, Object> map = new HashMap<>(1);
map.put(Constants.WRAPPER, Wrappers.query().eq("1", 1).last(selectSql));
// 查詢更新前數據
SqlSessionFactory sqlSessionFactory = SqlHelper.sqlSessionFactory(entityType);
change.setSqlSessionFactory(sqlSessionFactory);
change.setSqlStatement(tableInfo.getSqlStatement(SqlMethod.SELECT_LIST.getMethod()));
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
List<?> oldData = sqlSession.selectList(change.getSqlStatement(), map);
change.setOldData(Optional.ofNullable(oldData).orElse(new ArrayList<>()));
} finally {
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
BaseDataLog.DATA_CHANGES.get().add(change);
}
// 刪除
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/27
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataLog {
/**
* sPel表達式1
*/
String sPel1() default "";
/**
* sPel表達式2
*/
String sPel2() default "";
/**
* sPel表達式3
*/
String sPel3() default "";
/**
* <p>
* 類型
* </p>
*
* @return int
* @author Tophua
* @since 2020/8/11
*/
int type() default -1;
/**
* <p>
* 標簽
* </p>
*
* @return java.lang.String
* @author Tophua
* @since 2020/8/12
*/
String tag() default "";
/**
* <p>
* 注釋
* </p>
*
* @return java.lang.String
* @author Tophua
* @since 2020/8/11
*/
String note() default "";
}
Aop切面處理
package com.lith.datalog.aspect;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.lith.datalog.annotation.DataLog;
import com.lith.datalog.handle.BaseDataLog;
import com.lith.datalog.handle.DataChange;
import lombok.AllArgsConstructor;
import org.apache.ibatis.session.SqlSession;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.mybatis.spring.SqlSessionUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;
/**
* <p>
* DataLog切面
* </p>
*
* @author Tophua
* @since 2020/7/15
*/
@Aspect
@Order(99)
@Component
@AllArgsConstructor
public class DataLogAspect {
private final BaseDataLog baseDataLog;
/**
* <p>
* 初始化
* </p>
*
* @return void
* @author Tophua
* @since 2020/10/30
*/
@PostConstruct
public void init() {
baseDataLog.setting();
}
/**
* <p>
* 切面前執行
* </p>
*
* @param dataLog dataLog
* @return void
* @author Tophua
* @since 2020/7/15
*/
@Before("@annotation(dataLog)")
public void before(JoinPoint joinPoint, DataLog dataLog) {
// 使用 ThreadLocal 記錄一次操作
BaseDataLog.DATA_CHANGES.set(new LinkedList<>());
BaseDataLog.JOIN_POINT.set(joinPoint);
BaseDataLog.DATA_LOG.set(dataLog);
if (baseDataLog.isIgnore(dataLog)) {
BaseDataLog.DATA_CHANGES.set(null);
}
}
/**
* <p>
* 切面后執行
* </p>
*
* @param dataLog dataLog
* @return void
* @author Tophua
* @since 2020/7/15
*/
@AfterReturning("@annotation(dataLog)")
public void after(DataLog dataLog) {
List<DataChange> list = BaseDataLog.DATA_CHANGES.get();
if (CollUtil.isEmpty(list)) {
return;
}
list.forEach(change -> {
List<?> oldData = change.getOldData();
if (CollUtil.isEmpty(oldData)) {
return;
}
List<Long> ids = oldData.stream()
.map(o -> ReflectUtil.invoke(o, "getId").toString())
.filter(ObjectUtil::isNotNull)
.map(Long::parseLong)
.collect(Collectors.toList());
SqlSession sqlSession = change.getSqlSessionFactory().openSession();
try {
Map<String, Object> map = new HashMap<>(1);
map.put(Constants.WRAPPER, Wrappers.query().in("id", ids));
List<?> newData = sqlSession.selectList(change.getSqlStatement(), map);
change.setNewData(Optional.ofNullable(newData).orElse(new ArrayList<>()));
} finally {
SqlSessionUtils.closeSqlSession(sqlSession, change.getSqlSessionFactory());
}
System.out.println("oldData:" + JSONUtil.toJsonStr(change.getOldData()));
System.out.println("newData:" + JSONUtil.toJsonStr(change.getNewData()));
});
// 對比調模塊
this.compareAndTransfer(list);
}
/**
* <p>
* 對比保存
* </p>
*
* @param list list
* @return void
* @author Tophua
* @since 2020/7/15
*/
public void compareAndTransfer(List<DataChange> list) {
StringBuilder sb = new StringBuilder();
StringBuilder rsb = new StringBuilder();
list.forEach(change -> {
List<?> oldData = change.getOldData();
List<?> newData = change.getNewData();
// 更新前后數據量不對必定是刪除(邏輯刪除)不做處理
if (newData == null) {
return;
}
if (oldData == null) {
return;
}
if (oldData.size() != newData.size()) {
return;
}
// 按id排序
oldData.sort(Comparator.comparingLong(d -> Long.parseLong(ReflectUtil.invoke(d, "getId").toString())));
newData.sort(Comparator.comparingLong(d -> Long.parseLong(ReflectUtil.invoke(d, "getId").toString())));
for (int i = 0; i < oldData.size(); i++) {
final int[] finalI = {0};
baseDataLog.sameClazzDiff(oldData.get(i), newData.get(i)).forEach(r -> {
String oldV = r.getOldValue() == null ? "無" : r.getOldValue().toString();
String newV = r.getNewValue() == null ? "無" : r.getNewValue().toString();
if (ObjectUtil.equal(oldV.trim(), newV.trim())) {
return;
}
if (finalI[0] == 0) {
sb.append(StrUtil.LF);
sb.append(StrUtil.format("修改表:【{}】", change.getTableName()));
sb.append(StrUtil.format("id:【{}】", r.getId()));
}
sb.append(StrUtil.LF);
rsb.append(StrUtil.LF);
sb.append(StrUtil.format("把字段[{}]從[{}]改為[{}]",
r.getFieldName(), r.getOldValue(), r.getNewValue()));
rsb.append(StrUtil.indexedFormat(baseDataLog.getLogFormat(),
r.getId(), r.getFieldName(), r.getFieldComment(),
oldV, newV));
finalI[0]++;
});
}
});
if (sb.length() > 0) {
sb.deleteCharAt(0);
rsb.deleteCharAt(0);
}
// 存庫
System.err.println(sb.toString());
BaseDataLog.DATA_CHANGES.set(list);
BaseDataLog.LOG_STR.set(rsb.toString());
baseDataLog.transfer();
}
}
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);
}
@DataLog
@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、額外功能
@IgnoreDataLog
注解可用於實體類上或某字段上以實現某表、某字段不進行數據更新記錄- 可自定義
枚舉
和數據字典
翻譯 - 可在
DataLogHandle
類的setting()
初始化方法中調用父類方法以實現設置,其中支持:setLogFormat(String logFormat)
設置已定義操作記錄文字翻譯模板addExcludeTableName(String tableName)
或setExcludeTableNames(List<String> tableNames)
設置排除某些表,與@IgnoreDataLog
類似addExcludeFieldName(String fieldName)
或setExcludeFieldNames(List<String> fieldNames)
設置排除某些字段,與@IgnoreDataLog
類似
- 重寫
isIgnore(DataLog dataLog)
方法以達到是否忽略某次操作 - 還可調用父類
protected <T> T getValueBySpEl(String spEl, Class<T> clazz)
方法解析@DataLog
注解中所使用的的Spel表達式獲取數據
4、總結
本次綜合前車經驗,優化設計思想,改為從底層具體執行的 sql 語句入手,通過解析表名及更新條件來構造數據更新前后的查詢sql,再使用Spring AOP對方法執行前后進行處理,記錄更新前后的數據。最后再使用java反射機制將數據更新前后進行對比記錄。
同時使用ThreadLocal處理多線程問題,保證一個BaseDataLog
Bean下多線程操作不會發生數據錯亂。
注:
使用AOP涉及到一點,就是需要保證AOP與Spring 數據庫事務之間的執行順序,如果AOP先執行然后再提交事務,那結果則是數據無變化。
在此小筆已將AOP處理級別放到最后,保證先提交事務再去查詢更新后的數據,這樣才能得出正確的結果。
本例采用的數據庫是Mysql、連接池采用com.alibaba.druid.pool.DruidDataSource,如使用其他數據庫或連接池出現sql截取不正常或無參數需根據mybatis-plus版本重新分析底層處理、再進行sql截取部分代碼的重新開發。
歡迎各路大神交流意見。。。。。。
最后附上源碼地址: