一文带你学会基于SpringAop实现操作日志的记录

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 大家好,这里是经典鸡翅,今天给大家带来一篇基于SpringAop实现的操作日志记录的解决的方案。大家可能会说,切,操作日志记录这么简单的东西,老生常谈了。不!

前言

大家好,这里是经典鸡翅,今天给大家带来一篇基于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;

}

总结

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

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
20天前
|
Java Maven Spring
超实用的SpringAOP实战之日志记录
【11月更文挑战第11天】本文介绍了如何使用 Spring AOP 实现日志记录功能。首先概述了日志记录的重要性及 Spring AOP 的优势,然后详细讲解了搭建 Spring AOP 环境、定义日志切面、优化日志内容和格式的方法,最后通过测试验证日志记录功能的准确性和完整性。通过这些步骤,可以有效提升系统的可维护性和可追踪性。
|
5月前
|
SQL Java Serverless
实时计算 Flink版操作报错合集之在写入SLS(Serverless Log Service)时出现报错,该如何排查
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
|
6月前
|
弹性计算 Serverless 应用服务中间件
Serverless 应用引擎操作报错合集之集成sls时出现报错,是什么导致的
Serverless 应用引擎(SAE)是阿里云提供的Serverless PaaS平台,支持Spring Cloud、Dubbo、HSF等主流微服务框架,简化应用的部署、运维和弹性伸缩。在使用SAE过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
7月前
|
分布式计算 DataWorks 关系型数据库
DataWorks操作报错合集之在DataWorks中设置了一个任务节点的调度时间,并将其发布到生产环境,但到了指定时间(例如17:30)却没有产生运行实例和相关日志如何解决
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
105 0
|
5月前
|
Java Serverless 应用服务中间件
函数计算操作报错合集之JVM启动时找不到指定的日志目录,该如何解决
Serverless 应用引擎(SAE)是阿里云提供的Serverless PaaS平台,支持Spring Cloud、Dubbo、HSF等主流微服务框架,简化应用的部署、运维和弹性伸缩。在使用SAE过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
5月前
|
监控 数据管理 关系型数据库
数据管理DMS使用问题之是否支持将操作日志导出至阿里云日志服务(SLS)
阿里云数据管理DMS提供了全面的数据管理、数据库运维、数据安全、数据迁移与同步等功能,助力企业高效、安全地进行数据库管理和运维工作。以下是DMS产品使用合集的详细介绍。
|
6月前
|
监控 数据库
neo4j数据插入操作有日志吗
【6月更文挑战第29天】neo4j数据插入操作有日志吗
93 1
|
6月前
|
存储 运维 Java
Spring运维之boot项目开发关键之日志操作以及用文件记录日志
Spring运维之boot项目开发关键之日志操作以及用文件记录日志
68 2
|
6月前
|
Java 数据库连接 数据库
Spring日志完结篇,MyBatis操作数据库(入门)
Spring日志完结篇,MyBatis操作数据库(入门)
|
6月前
|
SQL DataWorks Oracle
DataWorks产品使用合集之datax解析oracle增量log日志该如何操作
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
62 0