前言

大家好,这里是经典鸡翅,今天给大家带来一篇基于SpringAop实现的操作日志记录的解决的方案。大家可能会说,切,操作日志记录这么简单的东西,老生常谈了。不!



网上的操作日志一般就是记录操作人,操作的描述,ip等。好一点的增加了修改的数据和执行时间。那么!我这篇有什么不同呢!今天这种不仅可以记录上方所说的一切,还增加记录了操作前的数据,错误的信息,堆栈信息等。正文开始~~~~~

思路介绍

记录操作日志的操作前数据是需要思考的重点。我们以修改场景来作为探讨。当我们要完全记录数据的流向的时候,我们必然要记录修改前的数据,而前台进行提交的时候,只有修改的数据,那么如何找到修改前的数据呢。有三个大的要素,我们需要知道修改前数据的表名,表的字段主键,表主键的值。这样通过这三个属性,我们可以很容易的拼出 select * from 表名 where 主键字段 = 主键值。我们就获得了修改前的数据,转换为json之后就可以存入到数据库中了。如何获取三个属性就是重中之重了。我们采取的方案是通过提交的映射实体,在实体上打上注解,根据 Java 的反射取到值。再进一步拼装获得对象数据。那么AOP是在哪里用的呢,我们需要在记录操作日志的方法上,打上注解,再通过切面获取到切点,一切的数据都通过反射来进行获得。

定义操作日志注解

既然是基于spinrg的aop实现切面。那么必然是需要一个自定义注解的。用来作为切点。我们定义的注解,可以带一些必要的属性,例如操作的描述,操作的类型。操作的类型需要说一下,我们分为新增、修改、删除、查询。那么只有修改和删除的时候,我们需要查询一下修改前的数据。其他两种是不需要的,这个也可以用来作为判断。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog { String operation() default ""; String operateType() default ""; }

定义用于找到表和表主键的注解

表和表主键的注解打在实体上,内部有两个属性 tableName 和 idName。这两个属性的值获得后,可以进行拼接 select * from 表名 where 主键字段。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectTable { String tableName() default ""; String idName() default "";
}

定义获取主键值的注解

根据上面所说的三个元素,我们还缺最后一个元素主键值的获取,用于告诉我们,我们应该从提交的请求的那个字段,拿到其中的值。

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectPrimaryKey { }

注解的总结

有了上面的三个注解,注解的准备工作已经进行完毕。我们通过反射取到数据,可以获得一切。接下来开始实现切面,对于注解的值进行拼接处理,最终存入到我们的数据库操作日志表中。

切面的实现

对于切面来说,我们需要实现切点、数据库的插入、反射的数据获取。我们先分开进行解释,最后给出全面的实现代码。方便大家的理解和学习。

切面的定义

基于spring的aspect进行声明这是一个切面。

@Aspect
@Component
public class OperateLogAspect {
}

切点的定义

切点就是对所有的打上OperateLog的注解的请求进行拦截和加强。我们使用annotation进行拦截。

	@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
private void operateLogPointCut(){
}

获取请求ip的共用方法

	private String getIp(HttpServletRequest request){
String ip = request.getHeader("X-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}

数据库的日志插入操作

我们将插入数据库的日志操作进行单独的抽取。

private void insertIntoLogTable(OperateLogInfo operateLogInfo){
operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
String sql="insert into log values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),
operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),
operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),
operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),
operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),
operateLogInfo.getModule(),operateLogInfo.getOperateType());
}

环绕通知的实现

日志的实体类实现

@TableName("operate_log")
@Data
public class OperateLogInfo { //主键id
@TableId
private String id;
//操作人id
private String userId;
//操作人名称
private String userName;
//操作内容
private String operation;
//操作方法名称
private String method;
//操作后的数据
private String modifiedData;
//操作前数据
private String preModifiedData;
//操作是否成功
private String result;
//报错信息
private String errorMessage;
//报错堆栈信息
private String errorStackTrace;
//开始执行时间
private Date executeTime;
//执行持续时间
private Long duration;
//ip
private String ip;
//操作类型
private String operateType; }

准备工作全部完成。接下来的重点是对环绕通知的实现。思路分为数据处理、异常捕获、finally执行数据库插入操作。环绕通知的重点类就是ProceedingJoinPoint ,我们通过它的getSignature方法可以获取到打在方法上注解的值。例如下方。

MethodSignature signature = (MethodSignature) pjp.getSignature();
OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class);
operateLogInfo.setOperation(declaredAnnotation.operation());
operateLogInfo.setModule(declaredAnnotation.module());
operateLogInfo.setOperateType(declaredAnnotation.operateType());
//获取执行的方法
String method = signature.getDeclaringType().getName() + "." + signature.getName();
operateLogInfo.setMethod(method);
String operateType = declaredAnnotation.operateType();

获取请求的数据,也是通过这个类来实现,这里有一点是需要注意的,就是我们要约定参数的传递必须是第一个参数。这样才能保证我们取到的数据是提交的数据。

if(pjp.getArgs().length>0){
Object args = pjp.getArgs()[0];
operateLogInfo.setModifiedData(new Gson().toJson(args));
}

接下来的一步就是对修改前的数据进行拼接。之前我们提到过如果是修改和删除,我们才会进行数据的拼接获取,主要是通过类来判断书否存在注解,如果存在注解,那么就要判断注解上的值是否是控制或者,非空才能正确的进行拼接。取field的值的时候,要注意私有的变量需要通过setAccessible(true)才可以进行访问。

if(GlobalStaticParas.OPERATE_MOD.equals(operateType) ||
GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
String tableName = "";
String idName = "";
String selectPrimaryKey = "";
if(pjp.getArgs().length>0){
Object args = pjp.getArgs()[0];
//获取操作前的数据
boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable.class);
if(selectTableFlag){
tableName = args.getClass().getAnnotation(SelectTable.class).tableName();
idName = args.getClass().getAnnotation(SelectTable.class).idName();
}else {
throw new RuntimeException("操作日志类型为修改或删除,实体类必须指定表面和主键注解!");
}
Field[] fields = args.getClass().getDeclaredFields();
Field[] fieldsCopy = fields;
boolean isFindField = false;
int fieldLength = fields.length;
for(int i = 0; i < fieldLength; ++i) {
Field field = fieldsCopy[i];
boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class);
if (hasPrimaryField) {
isFindField = true;
field.setAccessible(true);
selectPrimaryKey = (String)field.get(args);
}
}
if(!isFindField){
throw new RuntimeException("实体类必须指定主键属性!");
}
}
if(StringUtils.isNotEmpty(tableName) &&
StringUtils.isNotEmpty(idName)&&
StringUtils.isNotEmpty(selectPrimaryKey)){
StringBuffer sb = new StringBuffer();
sb.append(" select * from ");
sb.append(tableName);
sb.append(" where ");
sb.append(idName);
sb.append(" = ? ");
String sql = sb.toString();
try{
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey);
if(maps!=null){
operateLogInfo.setPreModifiedData(new Gson().toJson(maps));
}
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("查询操作前数据出错!");
}
}else {
throw new RuntimeException("表名、主键名或主键值 存在空值情况,请核实!");
}
}else{
operateLogInfo.setPreModifiedData("");
}

切面的完整实现代码

@Aspect
@Component
public class OperateLogAspect { @Autowired
private JdbcTemplate jdbcTemplate; @Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
private void operateLogPointCut(){
} @Around("operateLogPointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object responseObj = null;
OperateLogInfo operateLogInfo = new OperateLogInfo();
String flag = "success";
try{
HttpServletRequest request = SpringContextUtil.getHttpServletRequest();
DomainUserDetails currentUser = SecurityUtils.getCurrentUser();
if(currentUser!=null){
operateLogInfo.setUserId(currentUser.getId());
operateLogInfo.setUserName(currentUser.getUsername());
}
MethodSignature signature = (MethodSignature) pjp.getSignature();
OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class);
operateLogInfo.setOperation(declaredAnnotation.operation());
operateLogInfo.setModule(declaredAnnotation.module());
operateLogInfo.setOperateType(declaredAnnotation.operateType());
//获取执行的方法
String method = signature.getDeclaringType().getName() + "." + signature.getName();
operateLogInfo.setMethod(method);
String operateType = declaredAnnotation.operateType();
if(pjp.getArgs().length>0){
Object args = pjp.getArgs()[0];
operateLogInfo.setModifiedData(new Gson().toJson(args));
}
if(GlobalStaticParas.OPERATE_MOD.equals(operateType) ||
GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
String tableName = "";
String idName = "";
String selectPrimaryKey = "";
if(pjp.getArgs().length>0){
Object args = pjp.getArgs()[0];
//获取操作前的数据
boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable.class);
if(selectTableFlag){
tableName = args.getClass().getAnnotation(SelectTable.class).tableName();
idName = args.getClass().getAnnotation(SelectTable.class).idName();
}else {
throw new RuntimeException("操作日志类型为修改或删除,实体类必须指定表面和主键注解!");
}
Field[] fields = args.getClass().getDeclaredFields();
Field[] fieldsCopy = fields;
boolean isFindField = false;
int fieldLength = fields.length;
for(int i = 0; i < fieldLength; ++i) {
Field field = fieldsCopy[i];
boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class);
if (hasPrimaryField) {
isFindField = true;
field.setAccessible(true);
selectPrimaryKey = (String)field.get(args);
}
}
if(!isFindField){
throw new RuntimeException("实体类必须指定主键属性!");
}
}
if(StringUtils.isNotEmpty(tableName) &&
StringUtils.isNotEmpty(idName)&&
StringUtils.isNotEmpty(selectPrimaryKey)){
StringBuffer sb = new StringBuffer();
sb.append(" select * from ");
sb.append(tableName);
sb.append(" where ");
sb.append(idName);
sb.append(" = ? ");
String sql = sb.toString();
try{
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey);
if(maps!=null){
operateLogInfo.setPreModifiedData(new Gson().toJson(maps));
}
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("查询操作前数据出错!");
}
}else {
throw new RuntimeException("表名、主键名或主键值 存在空值情况,请核实!");
}
}else{
operateLogInfo.setPreModifiedData("");
}
//操作时间
Date beforeDate = new Date();
Long startTime = beforeDate.getTime();
operateLogInfo.setExecuteTime(beforeDate);
responseObj = pjp.proceed();
Date afterDate = new Date();
Long endTime = afterDate.getTime();
Long duration = endTime - startTime;
operateLogInfo.setDuration(duration);
operateLogInfo.setIp(getIp(request));
operateLogInfo.setResult(flag);
}catch (RuntimeException e){
throw new RuntimeException(e);
}catch (Exception e){
flag = "fail";
operateLogInfo.setResult(flag);
operateLogInfo.setErrorMessage(e.getMessage());
operateLogInfo.setErrorStackTrace(e.getStackTrace().toString());
e.printStackTrace();
}finally {
insertIntoLogTable(operateLogInfo);
}
return responseObj;
} private void insertIntoLogTable(OperateLogInfo operateLogInfo){
operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
String sql="insert into energy_log values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),
operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),
operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),
operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),
operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),
operateLogInfo.getModule(),operateLogInfo.getOperateType());
} private String getIp(HttpServletRequest request){
String ip = request.getHeader("X-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}

示例的使用方式

针对于示例来说我们要在controller上面打上操作日志的注解。

    @PostMapping("/updateInfo")
@OperateLog(operation = "修改信息",operateType = GlobalStaticParas.OPERATE_MOD)
public void updateInfo(@RequestBody Info info) {
service.updateInfo(info);
}

针对于Info的实体类,我们则要对其中的字段和表名进行标识。

@Data
@SelectTable(tableName = "info",idName = "id")
public class Info { @SelectPrimaryKey
private String id; private String name; }

总结

文章写到这,也就结束了,文中难免有不足,欢迎大家批评指正,另外可以关注我的公众号,进群交流哦。

一文带你学会基于SpringAop实现操作日志的记录的更多相关文章

  1. 一文带你学会java的jvm精华知识点

    前言 本文分为20多个问题,通过问题的方式,来逐渐理解jvm,由浅及深.希望帮助到大家. 1. Java类实例化时,JVM执行顺序? 正确的顺序如下: 1父类静态代码块 2父类静态变量 3子类静态代码 ...

  2. 一文带你学会使用YOLO及Opencv完成图像及视频流目标检测(上)|附源码

    计算机视觉领域中,目标检测一直是工业应用上比较热门且成熟的应用领域,比如人脸识别.行人检测等,国内的旷视科技.商汤科技等公司在该领域占据行业领先地位.相对于图像分类任务而言,目标检测会更加复杂一些,不 ...

  3. 一文带你学会国产加密算法SM4的java实现方案

    前言 今天给大家带来一个国产SM4加密解密算法的java后端解决方案,代码完整,可以直接使用,希望给大家带来帮助,尤其是做政府系统的开发人员,可以直接应用到项目中进行加密解密. 画重点!是SM4哦,不 ...

  4. 一文带你学会国产加密算法SM4的vue实现方案

    前言 上篇文章我们介绍了国产SM4加密算法的后端java实现方案.没有看过的小伙伴可以看一下这篇文章. https://www.cnblogs.com/jichi/p/12907453.html 本篇 ...

  5. 一文带你学会AQS和并发工具类的关系

    1. 存在的意义   AQS(AbstractQueuedSynchronizer)是JAVA中众多锁以及并发工具的基础,其底层采用乐观锁,大量使用了CAS操作, 并且在冲突时,采用自旋方式重试,以实 ...

  6. 一文带你学会AQS和并发工具类的关系2

    1.创建公平锁 1.使用方式 Lock reentrantLock = new ReentrantLock(true); reentrantLock.lock(); //加锁 try{ // todo ...

  7. springAOP实现操作日志记录,并记录请求参数与编辑前后字段的具体改变

    本文为博主原创,未经允许不得转载: 在项目开发已经完成多半的情况下,需要开发进行操作日志功能的开发,由于操作的重要性,需要记录下操作前的参数和请求时的参数, 在网上找了很多,没找到可行的方法.由于操作 ...

  8. [原创]基于SpringAOP开发的方法调用链分析框架

    新人熟悉项目必备工具!基于SpringAOP开发的一款方法调用链分析插件,简单到只需要一个注解,异步非阻塞,完美嵌入Spring Cloud.Dubbo项目!再也不用担心搞不懂项目! 很多新人进入一家 ...

  9. Istio是啥?一文带你彻底了解!

    原标题:Istio是啥?一文带你彻底了解! " 如果你比较关注新兴技术的话,那么很可能在不同的地方听说过 Istio,并且知道它和 Service Mesh 有着牵扯. 这篇文章可以作为了解 ...

随机推荐

  1. Kafka平滑滚动升级2.4.0指南

    今天测试了下kafka从2.0.0滚动升级至2.4.0,下面做一下记录.这个链接是Kafka官网对升级2.4.0的指南,可以参考  http://kafka.apache.org/24/documen ...

  2. 细说 PEP 468: Preserving Keyword Argument Order

    细说 PEP 468: Preserving Keyword Argument Order Python 3.6.0 版本对字典做了优化,新的字典速度更快,占用内存更少,非常神奇.从网上找了资料来看, ...

  3. Python高级编程-Python一切皆对象

    Python高级编程-Python一切皆对象 Python3高级核心技术97讲 笔记 1. Python一切皆对象 1.1 函数和类也是对象,属于Python的一等公民 ""&qu ...

  4. 初识Java和JDK下载安装

    故事:Java帝国的诞生 对手: C&C++ ◆1972年C诞生 ◆贴近硬件,运行极快,效率极高. ◆操作系统,编译器,数据库,网络系统等 ◆指针和内存管理 ◆1982年C++诞生 ◆面向对象 ...

  5. MATLAB矩阵处理—特殊矩阵

    需要掌握 MATLAB语言中特殊矩阵 MATLAB语言中矩阵的变幻 MATLAB语言矩阵如何求值 MATLAB语言中特征值与特征向量 MATLAB语言中稀疏矩阵 2.1  特殊矩阵 如何建立矩阵? 逐 ...

  6. 【Hadoop离线基础总结】HDFS的API操作

    HDFS的API操作 创建maven工程并导入jar包 注意 由于cdh版本的所有的软件涉及版权的问题,所以并没有将所有的jar包托管到maven仓库当中去,而是托管在了CDH自己的服务器上面,所以我 ...

  7. C:简单实现BaseCode64编码

    What is Base64? 前言 目前来看遇到过Base 16.Base 32.Base 64的编解码,这种编码格式是二进制和文本编码转化,是对称并且可逆的转化.Base 64总共有64个ASCI ...

  8. 【源码】RingBuffer(二)——消费者

    消费者如何读取数据? 前一篇是生产者的处理,这一篇讲消费者的处理 我们都知道,消费者无非就是不停地从队列中读取数据,处理数据.但是与BlockedQueue不同的是,RingBuffer的消费者不会对 ...

  9. 初级PLC

    SMB2接收到一个数据即产生一次中断,必须在中断处理程序中将数据从SMB2中读出,依次填表.这是一种效率极低的通讯处理方法,通讯字节多了会影响其它程序的运行. M 是位地址.比如M0.0,M0.1等. ...

  10. [codeforces-543-D div1]树型DP

    题意:给一棵树的边标上0或1,求以节点i为源点,其它点到i的唯一路径上的1的边数不超过1条的方案数,输出所有i的答案. 思路:令f[i]表示以节点i为源点,只考虑子树i时的方案数,ans[i]为最后答 ...