手上运维着一个MES系统,产线上用户瞎操作后,会告诉我们,系统用不了了,最后反馈到我们开发手上就是:你们系统BUG啊,快解决,花费大量时间的找到原因,多为用户操作不规范导致.于是就有了一个这样的需求,记录用户干了啥.
方案 1.手动写在需要记录的功能处手动插入日志或者是写数据库,后续可以用类似于日志分析系统对相应数据进行解析
- 公司没有日志分析系统,所有的日志都是基于单独系统的各种乱七八糟的日志,没法做解析,而且没有资源给我们自己做一个类似的系统
- 手动插入很繁琐,而且基本上是重复的工作
简单点说就是:设置一个切点,在方法前后对方法进行拦截操作
2.1简单依赖
org.springframework.boot spring-boot-starter-aop
定义一个注解
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.METHOD })
public @interface DateLog {
String operModul() default ""; // 操作模块
String operType() default ""; // 操作类型
String operDesc() default ""; // 操作说明
}
定义切点
@Aspect
@Component
public class DateLogAspect {
@Pointcut(value = "@annotation(com.leiyuee.manage.Audit.DataLog)" )
public void pointcut(){}
//前置
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint){
}
//后置
@After("pointcut()")
public void doAfter(JoinPoint joinPoint){
// 使用joinPoint得到方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("调用的方法名:"+methodName);
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
System.out.println(method.toString());
//获取操作
Audit audit = method.getAnnotation(Audit.class);
if (audit != null) {
System.out.println(audit.operModul()+"---"+audit.operType()+"---"+audit.operDesc());
}
}
**使用:**在需要使用的方法上添加注解 @Audit
//@DataLog
@DateLog(operModul = "角色管理",operType = "修改",operDesc = "修改角色")
@PutMapping("/{id}")
public R update(@RequestBody SysRole sysRole){
roleService.updateById(sysRole);
return R.ok(200,"修改成功",null);
}
结果
调用的方法名:save public com.leiyuee.manage.util.R com.leiyuee.manage.web.controller.manage.SysRoleController.save(com.leiyuee.manage.web.entity.manage.SysRole) 角色管理---新增---新增角色
可以直到用户什么时候是操作了什么方法,可以写到数据库表里面
2.2 增强上面的简单方法只能记录到用户操作了哪些方法,传了哪些数据,无法记录到数据的变化.Mybatis允许在已映射语句执行过程中的某一点进行拦截调用,通过实现Interceptor接口,来指定拦截方法的签名.
修改注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataLog {
String sPel1() default "";
String sPel2() default "";
String sPel3() default "";
int type() default -1;
String tag() default "";
String note() default "";
修改切面
@Aspect
@Order(99)
@Component
@AllArgsConstructor
public class DataLogAspect {
private final BaseDataLog baseDataLog;
@PostConstruct
public void init() {
baseDataLog.setting();
}
@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);
}
}
@AfterReturning("@annotation(dataLog)")
public void after(DataLog dataLog) {
List list = BaseDataLog.DATA_CHANGES.get();
if (CollUtil.isEmpty(list)) {
return;
}
list.forEach(change -> {
List> oldData = change.getOldData();
if (CollUtil.isEmpty(oldData)) {
return;
}
List 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 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);
}
public void compareAndTransfer(List 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("1:"+sb.toString());
BaseDataLog.DATA_CHANGES.set(list);
BaseDataLog.LOG_STR.set(rsb.toString());
baseDataLog.transfer();
}
}
定义拦截器
@Slf4j
@AllArgsConstructor
@Intercepts({@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})})
public class DataUpdateInterceptor implements Interceptor {
//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 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 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();
}
private int indexOfSqlStart(String sql) {
String upperCaseSql = sql.toUpperCase();
Set 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 list = new ArrayList<>(set);
list.sort(Comparator.naturalOrder());
return list.get(0);
}
}
对象类
@Data
public class CompareResult {
private Long id;
private String fieldName;
private String fieldComment;
private Object oldValue;
private Object newValue;
}
@Data
public class DataChange {
private SqlSessionFactory sqlSessionFactory;
private String sqlStatement;
private String tableName;
private String whereSql;
private Class> entityType;
private List> oldData;
private List> newData;
}
@Data
public class LogData {
private List dataChanges;
private List compareResults;
private String logStr;
}
public abstract class BaseDataLog {
public static final ThreadLocal DATA_LOG = new ThreadLocal<>();
public static final ThreadLocal JOIN_POINT = new ThreadLocal<>();
public static final ThreadLocal> DATA_CHANGES = new ThreadLocal<>();
public static final ThreadLocal> COMPARE_RESULTS = ThreadLocal.withInitial(ArrayList::new);
public static final ThreadLocal LOG_STR = new ThreadLocal<>();
@Setter
@Getter
private String logFormat = "将[{2}]由{3}修改为{4}";
@Setter
@Getter
public static List excludeTableNames = new ArrayList<>();
@Setter
@Getter
private List excludeFieldNames = new ArrayList<>();
private final SpelExpressionParser parser = new SpelExpressionParser();;
private final DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
public void transfer() {
LogData data = new LogData();
data.setDataChanges(DATA_CHANGES.get());
data.setCompareResults(COMPARE_RESULTS.get());
data.setLogStr(LOG_STR.get());
this.change(DATA_LOG.get(), data);
DATA_CHANGES.remove();
COMPARE_RESULTS.remove();
LOG_STR.remove();
DATA_LOG.remove();
JOIN_POINT.remove();
}
public boolean isIgnore(DataLog dataLog) {
return false;
}
public BaseDataLog addExcludeTableName(String tableName) {
excludeTableNames.add(tableName);
return this;
}
public BaseDataLog addExcludeFieldName(String fieldName) {
this.excludeFieldNames.add(fieldName);
return this;
}
public abstract void setting();
public abstract void change(DataLog dataLog,LogData data);
public List sameClazzDiff(Object obj1, Object obj2) {
List results = new ArrayList<>();
Field[] obj1Fields = obj1.getClass().getDeclaredFields();
Field[] obj2Fields = obj2.getClass().getDeclaredFields();
Long id = null;
for (int i = 0; i < obj1Fields.length; i++) {
obj1Fields[i].setAccessible(true);
obj2Fields[i].setAccessible(true);
Field field = obj1Fields[i];
if (this.excludeFieldNames.contains(field.getName()) || field.isAnnotationPresent(IgnoreDataLog.class)) {
continue;
}
try {
Object value1 = obj1Fields[i].get(obj1);
Object value2 = obj2Fields[i].get(obj2);
if ("id".equals(field.getName())) {
id = Long.parseLong(value1.toString());
}
if (value1 == null || StrUtil.isBlank(value1.toString())) {
value1 = null;
}
if (value2 == null || StrUtil.isBlank(value2.toString())) {
value2 = null;
}
if (!ObjectUtil.equal(value1, value2)) {
if (value1 != null && value2 != null && ObjectUtil.equal(value1.toString().trim(), value2.toString().trim())) {
continue;
}
CompareResult r = new CompareResult();
r.setId(id);
r.setFieldName(field.getName());
// 获取注释
r.setFieldComment(field.getName());
// ApiModelProperty property = field.getAnnotation(ApiModelProperty.class);
// if (property != null && StrUtil.isNotBlank(property.value())) {
// r.setFieldComment(property.value());
// }
r.setOldValue(value1);
r.setNewValue(value2);
results.add(r);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
COMPARE_RESULTS.get().addAll(results);
return results;
}
protected T getValueBySpEl(String spEl, Class clazz) {
if (!spEl.contains("#")) {
//注解的值非SPEL表达式,直接解析就好
return JSONUtil.toBean(spEl, clazz);
}
JoinPoint joinPoint = JOIN_POINT.get();
// 通过joinPoint获取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 使用spring的DefaultParameterNameDiscoverer获取方法形参名数组
String[] paramNames = nameDiscoverer.getParameterNames(method);
if (paramNames == null || paramNames.length == 0) {
return null;
}
// 解析过后的Spring表达式对象
Expression expression = parser.parseExpression(spEl);
// spring的表达式上下文对象
EvaluationContext context = new StandardEvaluationContext(joinPoint);
// 通过joinPoint获取被注解方法的形参
Object[] args = joinPoint.getArgs();
// 给上下文赋值
for(int i = 0 ; i < args.length ; i++) {
context.setVariable(paramNames[i], args[i]);
}
// 表达式从上下文中计算出实际参数值
return expression.getValue(context, clazz);
}
}
使用: 在需要记录的位置使用@DataLog注解
@DataLog
@PutMapping("/{id}")
public R update(@RequestBody SysRole sysRole){
roleService.updateById(sysRole);
return R.ok(200,"修改成功",null);
}
结果
oldData:[{"dataScope":"1","delFlag":"0","roleSort":11,"deptCheckStrictly":true,"createBy":"","updateBy":"","menuCheckStrictly":true,"roleName":"打工人11","roleKey":"coder11","id":1540213822982119425,"status":"1"}]
newData:[{"dataScope":"1","delFlag":"0","roleSort":11,"deptCheckStrictly":true,"createBy":"","updateBy":"","menuCheckStrictly":true,"roleName":"打工人","roleKey":"coder","id":1540213822982119425,"status":"1"}]
Time:138 ms - ID:com.leiyuee.manage.web.mapper.manage.SysRoleMapper.updateById
Execute SQL:UPDATE sys_role SET role_name='打工人', role_key='coder', role_sort=11, data_scope='1', menu_check_strictly=1, dept_check_strictly=1, status='1', del_flag='0', create_by='', update_by='' WHERe id=1540213822982119425
Time:0 ms - ID:com.leiyuee.manage.web.mapper.manage.SysRoleMapper.selectList
Execute SQL:SELECT id,role_name,role_key,role_sort,data_scope,menu_check_strictly,dept_check_strictly,status,del_flag,create_by,create_time,update_by,update_time,remark FROM sys_role WHERe (id IN (1540213822982119425))
可以将相应的修改数据写入数据库,最终实现记录用户详细操作
附上项目地址: manage https://gitee.com/leiyuee/manage.git



