本文记录通过spring aop实现记录mybatis-plus mapper接口,在执行删除及修改数据时操作日志,自动比对历史数据. 分析字段数据变化. 可以作为实现审计日志功能实现基础技术
MybatisPlusMethodInterceptor 实现MethodInterceptor接口,作为方法增强的入口.
public class MybatisPlusMethodInterceptor extends DataLogAspectSupport implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 非BaseMapper类型跳过增强
if (!(invocation.getThis() instanceof BaseMapper)) {
return invocation.proceed();
}
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
return invoke(invocation.getThis(), targetClass, invocation.getMethod(), invocation.getArguments(), invocation::proceed);
}
}
DataLogAspectSupport封装实现日志记录逻辑
由于mybatis-plus的mapper接口都是实现BaseMapper
public interface AccountMapper extends BaseMapper<Account> {
}
利用这个特点可以实现统一逻辑处理. update和delete操作,结合反射获取mybatis-plus注解@TableId
@TableName("account")
public class Account {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String username;
//setter/getter省略
}
即可获取当前记录的主键,从而自动查询操作前数据,生成比对信息
public class DataLogAspectSupport {
private static final Logger LOGGER = LoggerFactory.getLogger(DataLogAspectSupport.class);
public Object invoke(Object target, Class<?> targetClass, Method method, Object[] args,
final InvocationCallback invocation) throws Throwable {
LogInfo logInfo = this.recordLog(target, targetClass, method, args);
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = invocation.proceedWithLog();
} catch (Throwable t) {
if (logInfo != null) {
logInfo.setException(ExceptionUtils.getRootCause(t).getMessage());
}
} finally {
long timeout = System.currentTimeMillis() - startTime;
logInfo.setTimeout(timeout);
}
LOGGER.info("[easy-log]{}:\n{}", logInfo.getMethod(), JSON.toJSONString(logInfo, true));
return result;
}
private LogInfo recordLog(Object target, Class<?> targetClass, Method method, Object[] args) {
LogInfo logInfo = new LogInfo();
BaseMapper baseMapper = (BaseMapper) target;
String methodName = ((Class)targetClass.getGenericInterfaces()[0]).getSimpleName() + "." + method.getName();
logInfo.setMethod(methodName);
// TODO 后续支持更多方法
if (methodName.contains("updateById") || methodName.contains("deleteById")) {
LOGGER.debug("[easy-log][{}] 执行数据变化分析--开始", methodName);
Serializable primaryKey = this.getPrimaryKey(args[0]);
LOGGER.debug("[easy-log][{}] key:[{}] current:{}", methodName, primaryKey, JSON.toJSONString(args[0]));
Object result = baseMapper.selectById(primaryKey);
LOGGER.debug("[easy-log][{}] key:[{}] history:{}", methodName, primaryKey, JSON.toJSONString(result));
if (methodName.contains("updateById")){
try {
List<CompareResult> compareResultList = this.compareTowObject(result, args[0]);
logInfo.setDataSnapshot(JSON.toJSONString(compareResultList));
LOGGER.debug("[easy-log][{}] key:[{}] compareResult:" + JSON.toJSONString(compareResultList), methodName, primaryKey);
for (CompareResult compareResult : compareResultList) {
String report = compareResult.getFieldName() + "【" + compareResult.getFieldComment() + "】值:" + compareResult.getOldValue() + " => " + compareResult.getNewValue();
LOGGER.debug(report);
}
LOGGER.debug("[easy-log][{}] 执行数据变化分析--结束", methodName);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} else {
logInfo.setDataSnapshot(JSON.toJSONString(result));
}
}
return logInfo;
}
/**
* 对比两个对象
*
* @param oldObj 旧对象
* @param newObj 新对象
*/
protected List<CompareResult> compareTowObject(Object oldObj, Object newObj) throws IllegalAccessException {
List<CompareResult> list = new ArrayList<>();
//获取对象的class
Class<?> clazz1 = oldObj.getClass();
Class<?> clazz2 = newObj.getClass();
//获取对象的属性列表
Field[] field1 = clazz1.getDeclaredFields();
Field[] field2 = clazz2.getDeclaredFields();
//遍历属性列表field1
for (int i = 0; i < field1.length; i++) {
//遍历属性列表field2
for (int j = 0; j < field2.length; j++) {
//如果field1[i]属性名与field2[j]属性名内容相同
if (field1[i].getName().equals(field2[j].getName())) {
field1[i].setAccessible(true);
field2[j].setAccessible(true);
if (field2[j].get(newObj) == null) {
continue;
}
//如果field1[i]属性值与field2[j]属性值内容不相同
if (!compareTwo(field1[i].get(oldObj), field2[j].get(newObj))) {
CompareResult r = new CompareResult();
r.setFieldName(field1[i].getName());
r.setOldValue(field1[i].get(oldObj));
r.setNewValue(field2[j].get(newObj));
// TODO 获取属性名称功能暴露出去
// ApiModelProperty apiModelProperty = field1[i].getAnnotation(ApiModelProperty.class);
// if (apiModelProperty != null) {
// r.setFieldComment(apiModelProperty.value());
// }
list.add(r);
}
break;
}
}
}
return list;
}
@FunctionalInterface
protected interface InvocationCallback {
@Nullable
Object proceedWithLog() throws Throwable;
}
private Serializable getPrimaryKey(Object et) {
// 反射获取实体类
Class<?> clazz = et.getClass();
// 不含有表名的实体就默认通过
if (!clazz.isAnnotationPresent(TableName.class)) {
return (Serializable) et;
}
// 获取表名
TableName tableName = clazz.getAnnotation(TableName.class);
String tbName = tableName.value();
if (StringUtils.isBlank(tbName)) {
return null;
}
String pkName = null;
String pkValue = null;
// 获取实体所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 设置些属性是可以访问的
field.setAccessible(true);
if (field.isAnnotationPresent(TableId.class)) {
// 获取主键
pkName = field.getName();
try {
// 获取主键值
pkValue = field.get(et).toString();
} catch (Exception e) {
pkValue = null;
}
}
}
return pkValue;
}
/**
* 对比两个数据是否内容相同
*
* @param object1,object2
* @return boolean类型
*/
private boolean compareTwo(Object object1, Object object2) {
if (object1 == null && object2 == null) {
return true;
}
if (object1 == null && object2 != null) {
return false;
}
if (object1.equals(object2)) {
return true;
}
return false;
}
}
public class LogInfo {
/** 日志主键*/
// @TableId(type = IdType.UUID)
private String logId;
/** 日志类型*/
private String type;
/** 日志标题*/
private String title;
/** 日志摘要*/
private String description;
/** 请求IP*/
private String ip;
/** URI*/
private String requestUri;
/** 请求方式*/
private String method;
/** 提交参数*/
private String params;
/** 异常*/
private String exception;
/** 操作时间*/
private Date operateDate;
/** 请求时长*/
private Long timeout;
/** 用户登入名*/
private String loginName;
/** requestID*/
private String requestId;
/** 历史数据*/
private String dataSnapshot;
/** 日志状态*/
private Integer status;
//setter/getter省略
}
@ConditionalOnClass(BaseMapper.class)
@Configuration
public class MybatisPlusDataLogConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(MybatisPlusDataLogConfiguration.class);
@Bean
public AspectJExpressionPointcutAdvisor mybatisPlusMethodAdvisor(MybatisPlusMethodInterceptor interceptor) {
AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
advisor.setExpression("execution(* com.easycode8.easylog.sample.mapper.*.*(..))");
advisor.setAdvice(interceptor);
LOGGER.info("[easy-log]启动mybatis-plus操作数据比对");
return advisor;
}
@Bean
public MybatisPlusMethodInterceptor mybatisPlusMethodInterceptor() {
return new MybatisPlusMethodInterceptor();
}
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.3.2.RELEASEversion>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.2version>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.8.1version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.79version>
dependency>
记录操作前数据,可以有多种实现做法,mybatis的拦截器也可以实现类似逻辑.
public class RecordInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameterObject = args[1];
BoundSql boundSql = ms.getBoundSql(parameterObject);
String sql = boundSql.getSql();
SqlCommandType sqlCommandType = ms.getSqlCommandType();
if (sqlCommandType == SqlCommandType.DELETE || sqlCommandType == SqlCommandType.UPDATE) {
// 记录信息的逻辑
String tableName = getTableNameFromSql(sql);
// 获取删除或修改前的记录信息
List<Map<String, Object>> originalRecords = getOriginalRecords(tableName, parameterObject);
// 将信息记录到日志文件或数据库中
recordLog(tableName, originalRecords);
}
return invocation.proceed();
}
private String getTableNameFromSql(String sql) {
// 根据 SQL 语句获取表名
// ...
}
private List<Map<String, Object>> getOriginalRecords(String tableName, Object parameterObject) {
// 获取删除或修改前的记录信息
// ...
}
private void recordLog(String tableName, List<Map<String, Object>> originalRecords) {
// 将信息记录到日志文件或数据库中
// ...
}
}