1. 背景
应甲方的要求,系统中涉及到一些核心业务操作需要记录操作日志。
日志格式示例如下:
操作类型 | 操作内容 | 操作人员 |
---|---|---|
更新 | 表单ID【669306331893190656】 姓名 由【张三】改成【李四】 年龄 由【17岁】改成【18岁】 性别 由【男】改成【女】 地址 由【地球上的某个部落】改成【火星上的某个部落】 更新时间【2024-12-20 18:28:58】 | 管理员-飞哥 |
新增 | 表单ID【669306331893190656】 姓名 由【】改成【张三】 年龄 由【】改成【17岁】 性别 由【】改成【男】 地址 由【】改成【地球上的某个部落】 创建时间【2024-12-20 08:08:08】 | 管理员-云哥 |
为了可以提高整体开发效率,不在记录日志这块浪费太多时间,最好是只需通过配置或者加注解的方式就可以便捷记录操作日志。
在 Mybatis 中,插件机制提供了非常强大的扩展能力,在 sql 最终执行之前,提供了四个拦截点,支持不同场景的功能扩展
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
这里简单介绍这四种拦截点的区别:
- Executor:代表执行器,由它调度 StatementHandler、ParameterHandler、ResultSetHandler 等来执行对应的 SQL,其中 StatementHandler 是最重要的。
- StatementHandler:作用是使用数据库的 Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
- ParameterHandler:是用来处理 SQL 参数的。
- ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的,它非常的复杂,好在不常用。
借用网上的一张 mybatis 执行过程来辅助说明
原文 blog.csdn.net/weixin_3949...
切点的选择,主要与它的功能强相关,这里主要是通过拦截mapper类相关方法,选择对Executor进行拦截。此外,操作日志的记录还涉及到一些字段名称、字典值的转换等等,因此还需要增加一张日志映射表,大致的思路是在拦截器里面获取当前操作表更新后的新数据,并调用特定方法获取操作前的旧数据进行比对,再根据日志映射表配置,对实体对象中有修改过的字段进行转换,按指定格式组转出日志内容。
以下是方案的具体实现:
2. 日志映射表
2.1 表结构
字段名 | 类型 | 注释 |
---|---|---|
id | bidint | 主键,自增 |
module | varchar | 日志模块 |
field | varchar | 字段 |
field_name | varchar | 字段显示名称 |
convert_type | char | 转换类型,目前支持的类型有:00无需转换,01json,02sql,03英文逗号分隔,04日期 |
convert_resource | text | 转换资源 |
log_format | char | 日志格式 |
ord | int | 序号,数字越小优先级越高 |
2.2 字段简介
module: 日志模块,注解LogEnabled需要用到
field: 表字段,支持下划线、驼峰格式
fieldName: 字段名称
convert_type: 字段转换类型,目前支持以下5种类型
- 00 :无需转换,原样展示
- 01 :JSON,配置示例:
[{"code":"01","value":"Individual"},{"code":"02","value":"Company"}]
json的key固定必须有code和value,系统会用实体对象的具体字段和json的code进行比较,相同则取出对应value - 02 :SQL,SQL示例:
select id as code,name as value from link_ep_car_type t where t.id = :originValue
参数名固定是originValue,匹配规则类似01 - 03 :英文逗号隔开,示例:
[{"code":"00","value":"小米"},{"code":"01","value":"腾讯"},{"code":"02","value":"字节跳动"},{"code":"03","value":"阿里巴巴"},{"code":"04","value":"其他"}]
json的key固定必须有code和value。例如字段值=01,02,会根据配置的json array匹配映射得到 腾讯,字节跳动 - 04:日期格式,针对日期字段可指定日期格式化方式。没有指定的情况下,如果实体字段是date类型,格式化方式:yyyy-MM-dd HH:mm:ss。如果是LocalDate类型,格式化方式:yyyy-MM-dd,如果是LocalTime类型,格式化方式:HH:mm:ss
log_format: 日志格式
00:%s 由 【%s】改成【%s】,共用3个占位符,第一个占位符会用字段名替换,第二个占位符会用旧值替换,第三个占位符会用新值替换,效果示例:住址 由【福建省厦门市】 改成 【福建省莆田市】
10:%s 【%s】,共用2个占位符,第一个占位符会用字段名替换,第二个占位符会用新值替换,效果示例:表单ID 【203929302】
ord: 序号,值越小优先级越高,越先展示
3.注解
less
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Inherited//可继承
public @interface LogEnabled {
/**
* 事件ID对应的字段
* 最好是主键
*
* @return
*/
String eventIdFiled();
/**
* 记录日志模块
* 对应link_log_operation_reflect.module
*
* @return
*/
String module();
/**
* 用来查询旧数据的自定义mapper方法名
* 参数需是map类型,并且需通过paramFields声明用来获取旧数据的字段
* <p>
* 如果没有指定将默认使用selectByPrimaryKey方法获取
*
* @return
*/
String selectOldEntityMapperMethod() default "";
/**
* 用来获取旧数据的字段
* 如果有指定查询旧数据的自定义mapper方法名则需要声明该参数
*
* @return
*/
String[] paramFields() default {};
/**
* 是否保存到日志表
* 默认保存
*
* @return
*/
boolean persistent() default true;
}
4.Mybatis拦截器代码
ini
package mcr.interceptor;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import mcr.ApplicationContextHolder;
import mcr.annotation.LogEnabled;
import mcr.constants.LogConstants;
import mcr.service.log.LinkLogOperationRecordService;
import mcr.utils.ReflectionUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.scripting.xmltags.ForEachSqlNode;
import org.apache.ibatis.session.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author: 飞哥
* @desc: 操作日志拦截器
* @date: 2023/8/7
*/
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
)
})
public class OperationLogInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(OperationLogInterceptor.class);
/**
* 日志记录逻辑说明
* <p>
* 需要记录操作日志的实体需加注解LogEnabled,声明对应的module,以及eventIdFiled
* LogEnabled注解说明:
* (1)module:日志模块,会据此从link_log_operation_reflect读取日志字段映射配置,目前支持字段转换的方式有json、SQL
* json固定格式,示例:[{"code":"01","value":"個人"},{"code":"02","value":"公司"}]
* sql查询字段需含code/value字段,查询条件用:originValue,示例: select id as `code`,name as `value` from `link_ep_car_type` t where t.id = :originValue
* <p>
* (2)paramFields:用来获取老数据的字段,默认会执行 mapper中名叫 selectOldEntity的方法获取老数据,注意此方法入参类型是Map<String, Object>
* (3)eventIdFiled:用来记录事件ID的字段,最好是实体对象的主键
* <p>
* update操作会比对旧数据和新数据,数据一致或新数据为null的不记录
* insert操作只记录新数据
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
OperationLog operationLog = OperationLog.empty();
try {
Object[] args = invocation.getArgs();
//获取绑定的参数对象
Object parameterObject = args[1];
if (parameterObject == null) {
return invocation.proceed();
}
//获取日志注解
LogEnabled logEnabled = null;
if (parameterObject instanceof HashMap) {
//批量insert操作
List<LogEnabled> annotationList = (List<LogEnabled>) ((HashMap) parameterObject).values().stream().map(list -> {
if (!(list instanceof List)) {
return null;
}
return ((List) list).stream().filter(obj -> {
return obj.getClass().getAnnotation(LogEnabled.class) != null;
}).map(obj -> obj.getClass().getAnnotation(LogEnabled.class)).findFirst().orElse(null);
}).collect(Collectors.toList());
logEnabled = CollectionUtils.isEmpty(annotationList) ? null : annotationList.get(0);
} else {
logEnabled = parameterObject.getClass().getAnnotation(LogEnabled.class);
}
if (logEnabled == null) {
return invocation.proceed();
}
MappedStatement mappedStatement = (MappedStatement) args[0];
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
LogConstants.OPTION option;
if (SqlCommandType.INSERT.equals(sqlCommandType)) {
option = LogConstants.OPTION.ADD;
} else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
option = LogConstants.OPTION.UPDATE;
} else {
// option = LogConstants.OPTION.DELETE;
return invocation.proceed();
}
//获取SQL语句和参数信息封装对象
BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
//获取参数映射列表
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
Configuration configuration = mappedStatement.getConfiguration();
MetaObject metaObject = configuration.newMetaObject(parameterObject);
//新的实体类
List<Object> newEntityList = parameterObject instanceof HashMap ? (List<Object>) ((HashMap) parameterObject).get("list") : new ArrayList<Object>() {{
add(parameterObject);
}};
//旧的实体类
List<Object> oldEntityList = this.getOldEntityList(option, logEnabled, invocation, metaObject, mappedStatement, parameterMappings, parameterObject);
operationLog = ApplicationContextHolder.getBean(LinkLogOperationRecordService.class).operationLogRecord(logEnabled.module(), option, logEnabled.persistent(), newEntityList, oldEntityList, logEnabled.eventIdFiled());
} catch (Exception e) {
log.error("记录操作日志拦截器出现异常:", e);
}
Object result = invocation.proceed();
ApplicationContextHolder.getBean(LinkLogOperationRecordService.class).storeLogReportAndSaveLog(result, operationLog);
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 获取mapper名字
*
* @param mappedStatementId 映射语句ID
* @return
*/
private String extractMapperName(String mappedStatementId) {
// 从 mappedStatementId 中提取出 Mapper 的名称
int lastIndex = mappedStatementId.lastIndexOf(".");
return mappedStatementId.substring(0, lastIndex);
}
/**
* 获取根据主键查询DO对象的MappedStatement
*
* @param configuration 配置对象
* @param mappedStatementId 映射语句ID
* @return
*/
private MappedStatement getSelectByPrimaryKeyMappedStatement(Configuration configuration, String mappedStatementId) {
String selectByPrimaryKeyMapperId = mappedStatementId.replace(mappedStatementId.substring(mappedStatementId.lastIndexOf(CommonConstants.DOT) + 1), "selectByPrimaryKey");
return configuration.getMappedStatement(selectByPrimaryKeyMapperId);
}
/**
* 更新和删除场景获取旧数据
*
* @param option 操作日志类型
* @param logEnabled 操作日志注解
* @param invocation 调用对象
* @param metaObject 当前提交的参数封装对象
* @param mappedStatement 映射语句
* @param parameterMappings 当前提交参数映射列表
* @param parameterObject 参数对象
* @return
*/
private List<Object> getOldEntityList(LogConstants.OPTION option, LogEnabled logEnabled, Invocation invocation, MetaObject metaObject, MappedStatement mappedStatement, List<ParameterMapping> parameterMappings, Object parameterObject) {
if (LogConstants.OPTION.UPDATE != option && LogConstants.OPTION.DELETE != option) {
return Lists.newArrayList();
}
if (!StringUtils.hasText(logEnabled.selectOldEntityMapperMethod())) {
return this.getOldEntityList(invocation, mappedStatement, parameterObject);
}
return this.getOldEntityList(metaObject, mappedStatement, parameterMappings, logEnabled.selectOldEntityMapperMethod(), logEnabled.paramFields());
}
/**
* 默认用selectByPrimaryKeyMappedStatement获取旧数据
*
* @param invocation 拦截器调用对象
* @param mappedStatement 映射语句
* @param parameterObject 当前提交参数映射列表
* @return
*/
private List<Object> getOldEntityList(Invocation invocation, MappedStatement mappedStatement, Object parameterObject) {
Executor executor = (Executor) invocation.getTarget();
MappedStatement selectByPrimaryKeyMappedStatement = this.getSelectByPrimaryKeyMappedStatement(mappedStatement.getConfiguration(), mappedStatement.getId());
List<Object> oldEntityList = Lists.newArrayList();
try {
//单个实体操作
List<Object> resultList = executor.query(selectByPrimaryKeyMappedStatement, parameterObject, RowBounds.DEFAULT, null);
oldEntityList.addAll(resultList);
} catch (Exception e) {
log.error("获取旧实体对象出现异常:", e);
}
return oldEntityList;
}
/**
* 用当前提交的实体对象中的部分字段查询更新之前的旧数据
* 实体对应的MAPPER需有selectOldEntity方法
*
* @param metaObject 当前提交的参数封装对象
* @param mappedStatement 映射语句
* @param parameterMappings 当前提交参数映射列表
* @param selectOldEntityMapperMethod 用来查询旧数据的mapper方法名
* @param paramFields 用来获取旧数据的字段
* @return
*/
private List<Object> getOldEntityList(MetaObject metaObject, MappedStatement mappedStatement, List<ParameterMapping> parameterMappings, String selectOldEntityMapperMethod, String[] paramFields) {
List<Object> oldEntityList = Lists.newArrayList();
try {
if (metaObject.getOriginalObject() instanceof HashMap) {
//批量操作
ArrayList objectList = ((ArrayList) metaObject.getValue("list"));
for (int index = 0; index < objectList.size(); index++) {
Object object = objectList.get(index);
Class clz = Class.forName(this.extractMapperName(mappedStatement.getId()));
Method method = clz.getMethod(selectOldEntityMapperMethod, Map.class);
Object bean = ApplicationContextHolder.getBean(clz);
oldEntityList.add(method.invoke(bean, this.fillParams(object, index, parameterMappings, paramFields)));
}
} else {
//单个实体操作
Class clz = Class.forName(this.extractMapperName(mappedStatement.getId()));
Method method = clz.getMethod(selectOldEntityMapperMethod, Map.class);
Object bean = ApplicationContextHolder.getBean(clz);
oldEntityList.add(method.invoke(bean, this.fillParams(metaObject, parameterMappings, paramFields)));
}
} catch (Exception e) {
log.error("获取旧实体对象出现异常:{}", mappedStatement.getId(), e);
}
return oldEntityList;
}
/**
* 填充查询参数
*
* @param metaObject 当前提交的参数封装对象
* @param parameterMappings 当前提交参数映射列表
* @param paramFields 用来获取旧数据的字段
* @return
*/
private Map<String, Object> fillParams(MetaObject metaObject, List<ParameterMapping> parameterMappings, String[] paramFields) {
List<String> list = Lists.newArrayList(paramFields);
Map<String, Object> params = Maps.newHashMap();
parameterMappings.forEach(p -> {
String key = p.getProperty();
if (list.contains(key)) {
Object value = metaObject.getValue(key);
params.put(key, value);
}
});
return params;
}
/**
* 填充查询参数
* 适用于批量操作场景
*
* @param object 当前提交的参数封装对象
* @param index 批量操作参数索引
* @param parameterMappings 当前提交参数映射列表
* @param paramFields 用来获取旧数据的字段
* @return
*/
private Map<String, Object> fillParams(Object object, int index, List<ParameterMapping> parameterMappings, String[] paramFields) {
List<String> list = Lists.newArrayList(paramFields);
Map<String, Object> params = Maps.newHashMap();
parameterMappings.forEach(p -> {
try {
//批量操作的时候property格式是__frch_record_0.id
String key = p.getProperty().replaceAll(ForEachSqlNode.ITEM_PREFIX + "(.*)_" + index + "\.", "");
if (list.contains(key)) {
Field field = ReflectionUtils.getAccessibleField(object, ApplicationContextHolder.getBean(LinkLogOperationRecordService.class).getCamelCaseString(key));
Object value = field.get(object);
params.put(key, value);
}
} catch (Exception e) {
log.error("填充查询参数出现异常:{}", p, e);
}
});
return params;
}
}
5.实体对象字段转换代码
kotlin
/**
* @author: 飞哥
* @desc:
* @date: 2023/8/8
*/
@RefreshScope
@Service
public class LinkLogOperationRecordService extends BaseService<LinkLogOperationRecordMapper, LinkLogOperationRecordDO> {
private static final Logger log = LoggerFactory.getLogger(LinkLogOperationRecordService.class);
@Autowired
private I18nMessageSource i18nMessageSource;
@Autowired
private LinkLogOperationReflectService logOperationReflectService;
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public LinkLogOperationRecordService(@Qualifier("defaultDataSource") DruidDataSource druidDataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(druidDataSource);
}
/**
* 保存日志
*
* @param recordId
* @param eventType
* @param content
*/
private void saveLog(String recordId, String eventType, String content) {
try {
LinkLogOperationRecordDO record = new LinkLogOperationRecordDO();
record.setEventId(recordId);
record.setEventType(eventType);
record.setContent(content);
UserDTO user = WebUtils.getUser();
record.setOperatorBy(user == null ? CommonConstants.SYSTEM : user.getUserId());
record.setOperatorByName(user == null ? CommonConstants.SYSTEM_NAME : user.getUserName());
this.save(record);
} catch (Exception e) {
log.error("日志入库失败:{} {}", recordId, eventType, e);
}
}
/**
* 暂存即将上报的操作日志
*
* @param recordId
* @param content
*/
private void storeLogReport(String recordId, String content) {
try {
LogReportInfo logReportInfo = new LogReportInfo();
logReportInfo.setRecordId(recordId);
logReportInfo.setContent(content);
logReportInfo.setOperateTime(new Date());
WebUtils.setLogReport(logReportInfo);
} catch (Exception e) {
log.error("日志暂存失败:{}", recordId, e);
}
}
/**
* 暂存即将上报的操作日志+日志入库
*
* @param invocationResult 更新记录数 >0才记录日志
* @param operationLog 操作日志对象
*/
public void storeLogReportAndSaveLog(Object invocationResult, OperationLog operationLog) {
try {
if (!operationLog.isEmpty() && Integer.parseInt(invocationResult.toString()) > 0) {
this.storeLogReportAndSaveLog(operationLog.getRecordId(), operationLog.getOption().getCode(), operationLog.getContent(), operationLog.getPersistent());
} else {
log.warn("没有日志内容或没有更新记录,不记录操作日志:{} {}", invocationResult, JSON.toJSONString(operationLog));
}
} catch (Exception e) {
log.error("记录日志失败:", e);
}
}
/**
* 暂存即将上报的操作日志+日志入库
*
* @param recordId
* @param eventType
* @param content
* @param persistent
*/
public void storeLogReportAndSaveLog(String recordId, String eventType, String content, boolean persistent) {
this.storeLogReport(recordId, content);
if (persistent) {
this.saveLog(recordId, eventType, content);
}
}
/**
* 查詢操作日志
*
* @param eventId
* @return
*/
public List<LinkLogOperationRecordDO> listAll(String eventId) {
Example example = this.getExample();
example.and().andEqualTo("eventId", eventId);
example.setOrderByClause("id desc");
return this.selectByExample(example);
}
/**
* 记录操作日志
*
* @param module 慕课
* @param option 操作类型
* @param persistent 是否持久化到数据库
* @param newEntityList 准备更新的数据对象集合
* @param oldEntityList 历史数据对象集合
* @param eventIdField 实体对象中作为eventId的字段
* @param <T>
*/
public <T> OperationLog operationLogRecord(String module, LogConstants.OPTION option, boolean persistent, List<T> newEntityList, List<T> oldEntityList, String eventIdField) {
try {
List<LinkLogOperationReflectDO> reflectList = this.logOperationReflectService.listLogOperationReflect(module);
for (int i = 0; i < newEntityList.size(); i++) {
List<String> contentList = Lists.newArrayList();
T newEntity = newEntityList.get(i);
T oldEntity = oldEntityList.size() > i ? oldEntityList.get(i) : null;
reflectList.forEach(o -> {
try {
//字段
String column = o.getField();
//字段对应名称,默认翻译成英文
String name = o.getFieldName();
ValueInfo valueInfo = this.getValueInfo(column, option, newEntity, oldEntity);
if (valueInfo == null) {
return;
}
Object newValue = valueInfo.newValue;
Object oldValue = valueInfo.oldValue;
if (newValue == null) {
//更新后的属性值=null,忽略
return;
}
LogConstants.LOG_FORMAT logFormat = LogConstants.LOG_FORMAT.codeOf(o.getLogFormat());
if (LogConstants.LOG_FORMAT.MAJOR_KEY == logFormat) {
String newValueConvert = this.convertFieldValue(newValue, o.getConvertType(), o.getConvertResource());
contentList.add(String.format(logFormat.getFormat(), name, newValueConvert));
} else {
if (newValue.equals(oldValue)) {
//属性值不变
return;
}
String oldValueConvert = this.convertFieldValue(oldValue, o.getConvertType(), o.getConvertResource());
String newValueConvert = this.convertFieldValue(newValue, o.getConvertType(), o.getConvertResource());
contentList.add(String.format(logFormat.getFormat(), name, oldValueConvert, newValueConvert));
}
} catch (Exception e) {
log.error("组装操作日志内容出现异常:{} {} ", module, JSON.toJSONString(o), e);
}
});
Field field = ReflectionUtils.getAccessibleField(newEntity, this.getCamelCaseString(eventIdField));
String fieldValue = Objects.toString(field.get(newEntity), "");
log.info("日志内容:{}", contentList);
if (CollectionUtils.isEmpty(contentList)) {
return OperationLog.empty();
}
String content = Joiner.on("\r").join(contentList);
return OperationLog.Builder
.create()
.withRecordId(fieldValue)
.withPersistent(persistent)
.withOption(option)
.withContent(content)
.build();
}
} catch (Exception e) {
log.error("记录操作日志出现异常:{},{},{}", module, option, eventIdField, e);
}
return OperationLog.empty();
}
/**
* 记录操作日志
*
* @param module 慕课
* @param option 操作类型
* @param persistent 是否持久化到数据库
* @param newEntity 准备更新的数据对象
* @param oldEntity 历史数据对象
* @param eventIdField 实体对象中作为eventId的字段
* @param <T>
*/
public <T> void operationLogRecord(String module, LogConstants.OPTION option, boolean persistent, T newEntity, T oldEntity, String eventIdField) {
OperationLog operationLog = this.operationLogRecord(module, option, persistent, Arrays.asList(newEntity), Arrays.asList(oldEntity), eventIdField);
if (!operationLog.isEmpty()) {
this.storeLogReportAndSaveLog(operationLog.getRecordId(), operationLog.getOption().getCode(), operationLog.getContent(), operationLog.getPersistent());
}
}
/**
* 获取新旧value
*
* @param column
* @param option
* @param newEntity
* @param oldEntity
* @param <T>
* @return
*/
private <T> ValueInfo getValueInfo(String column, LogConstants.OPTION option, T newEntity, T oldEntity) {
try {
// 获取属性对象
Field field = ReflectionUtils.getAccessibleField(newEntity, this.getCamelCaseString(column));
//获取getter方法
Method method = ReflectionUtils.getAccessibleMethod(newEntity, "get" + StringUtils.capitalize(this.getCamelCaseString(column)));
if (field == null && method == null) {
log.error("没有这个字段,也没有getter方法,直接跳过:{}", column);
return null;
}
if (field != null) {
// 设置访问权限(可选)
field.setAccessible(true);
// 读取属性值
Object newValue = field.get(newEntity);
Object oldValue = oldEntity == null ? null : (LogConstants.OPTION.UPDATE == option ? field.get(oldEntity) : null);
return new ValueInfo(newValue, oldValue);
} else {
//读取getter方法
Object newValue = method.invoke(newEntity);
Object oldValue = oldEntity == null ? null : (LogConstants.OPTION.UPDATE == option ? method.invoke(oldEntity) : null);
return new ValueInfo(newValue, oldValue);
}
} catch (Exception e) {
log.error("获取新旧value出现异常:{}", column, e);
}
return null;
}
/**
* 转换字段值
* convertTypeString转换规则说明:
* 默认 00,表示不需要转换,直接返回原始值
* 01 JSON,表示convertResource是一个jsonArray,原始值会从convertResource中匹配,固定格式:[{code:x1, value:y1},{code:x2,value:y2}]
* 02 SQL,表示convertResource是一个SQL,执行结果格式固定为[{code:x1, value:y1},{code:x2,value:y2}],SQL有一个固定查询参数,名字为originValue,SQL示例:select id as `code`,name as `value` from `link_district` t where t.id = :originValue;
* 03 英文逗号分隔,主要针对存储值为01,02,03类型的字段,会先用英文逗号分隔,然后再从convertResource匹配,convertResource固定格式:[{code:x1, value:y1},{code:x2,value:y2}]
* 04 日期,主要针对一些需要转换为特定格式的日期字段,例如可以填yyyy-MM-dd,当如果不特殊指定,也就是convertTypeString=00时也可以转换日期,只是格式默认是yyyy-MM-dd HH:mm:ss
*
* @param originValue 原始数据
* @param convertTypeString 转换类型
* @param convertResource 转换资源,JSON、SQL
* @return
*/
public String convertFieldValue(Object originValue, String convertTypeString, String convertResource) {
String originValueString = Objects.toString(originValue, "");
List<ConvertInfo> list = Lists.newArrayList();
LogConstants.CONVERT_TYPE translateType = LogConstants.CONVERT_TYPE.codeOf(convertTypeString);
if (translateType != null) {
switch (translateType) {
case IGNORE:
return this.formatDate(originValue, originValueString, null);
case JSON:
list = JSONArray.parseArray(convertResource, ConvertInfo.class);
break;
case SQL:
list = this.namedParameterJdbcTemplate.query(convertResource, ImmutableMap.of("originValue", originValueString), new BeanPropertyRowMapper<>(ConvertInfo.class));
break;
case SPLIT:
List<String> originValueList = Lists.newArrayList(originValueString.split(","));
List<ConvertInfo> convertInfoList = JSONArray.parseArray(convertResource, ConvertInfo.class);
return Joiner.on(",").join(originValueList.stream().map(value -> {
return convertInfoList.stream().filter(o -> value.equals(o.getCode())).findFirst().map(ConvertInfo::getValue).orElse(value);
}).collect(Collectors.toList()));
case DATE:
return this.formatDate(originValue, originValueString, convertResource);
default:
log.error("暂不支持的转换类型:{}", convertTypeString);
break;
}
} else {
log.error("暂不支持的转换类型:{}", convertTypeString);
}
//找不到翻译则返回原始值
return list.stream().filter(o -> originValueString.equals(o.getCode())).findFirst().map(ConvertInfo::getValue).orElse(originValueString);
}
/**
* 格式化日期字段
*
* @param originValue
* @param originValueString
* @param format
* @return
*/
private String formatDate(Object originValue, String originValueString, String format) {
if (originValue instanceof Date) {
return DateUtil.convertDate2LocalDateTime((Date) originValue).format(!StringUtils.hasLength(format) ? DateUtil.DTF_YMDHMS_2 : DateTimeFormatter.ofPattern(format));
}
if (originValue instanceof LocalDate) {
return ((LocalDate) originValue).format(!StringUtils.hasLength(format) ? DateUtil.DTF_YYYYMMDD_2 : DateTimeFormatter.ofPattern(format));
}
if (originValue instanceof LocalTime) {
return ((LocalTime) originValue).format(!StringUtils.hasLength(format) ? DateUtil.DTF_HHMMSS : DateTimeFormatter.ofPattern(format));
}
if (originValue instanceof Long) {
LocalDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli((Long) originValue), ZoneId.systemDefault()).toLocalDateTime();
return dateTime.format(!StringUtils.hasLength(format) ? DateUtil.DTF_YMDHMS_2 : DateTimeFormatter.ofPattern(format));
}
return originValueString;
}
/**
* 下划线字段转为驼峰
* 例如back_end会转换成backEnd
*
* @param underScoreField
* @return
*/
public String getCamelCaseString(String underScoreField) {
return underScoreField.contains(CommonConstants.UNDERLINE) ? CaseUtils.toCamelCase(underScoreField, Boolean.FALSE, '_') : underScoreField;
}
public class ValueInfo {
private Object newValue;
private Object oldValue;
public ValueInfo(Object newValue, Object oldValue) {
this.newValue = newValue;
this.oldValue = oldValue;
}
public Object getNewValue() {
return newValue;
}
public void setNewValue(Object newValue) {
this.newValue = newValue;
}
public Object getOldValue() {
return oldValue;
}
public void setOldValue(Object oldValue) {
this.oldValue = oldValue;
}
}
}
6.添加Mybatis拦截器
ini
@Configuration
public class DbConfig {
@Bean
public SqlSessionFactory buildSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
// 创建 ResourcePatternResolver 对象
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 使用 ResourcePatternResolver 获取 Mapper 文件对应的 Resource 对象
Resource[] resources = resolver.getResources(mapperLocations);
sessionFactoryBean.setMapperLocations(resources);
//Properties properties = new Properties();
SqlSessionFactory sqlSessionFactory = sessionFactoryBean.getObject();
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
configuration.setMapUnderscoreToCamelCase(true);
//记录操作日志
configuration.addInterceptor(new OperationLogInterceptor());
return sqlSessionFactory;
}
}
7.业务实践
至此,底层基础工作已经完成,接下来后续需要记录操作日志的表对应DO只需要加上刚才声明的注解,并在表里配置字段映射即可自动记录操作日志,如下所示:
arduino
@LogEnabled(eventIdFiled = "userId", module = "user_info")
@Table(name = "user_info")
public class UserInfoDO {
/**
* 主鍵
*/
@Id
private String userId;
/**
* 姓名
*/
private String name;
/**
* 性别
*/
private String sex;
/**
* 年龄
*/
private Integer age;
/**
* 地址
*/
private String address;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
...getter/setter
}
映射表配置示例:
module | field | field_name | convert_type | convert_source | log_format | ord |
---|---|---|---|---|---|---|
user_info | userId | 用户ID | 00 | 10 | 10 | |
user_info | name | 姓名 | 00 | 00 | 20 | |
user_info | sex | 性别 | 01 | [{"code":"01","value":"男"},{"code":"02","value":"女"}] | 00 | 30 |
user_info | age | 年龄 | 02 | select :originValue as code , concat(:originValue,'岁') |
00 | 40 |
user_info | address | 地址 | 00 | 00 | 50 | |
user_info | createTime | 创建时间 | 04 | 00 | 60 | |
user_info | updateTime | 更新时间 | 04 | 00 | 70 |
这样后续对UserInfoDO所在mapper进行insert或者update就会自动生成开头一样的操作日志。
贴出来的代码可能不是很全,但大致的思路可以供大家参考