JAVA自定义注解记录操作日志

背景:

系统的操作日志、审计日志。在日常的管理还是维护中都会起到很大的作用。

解决办法:

可以在需要的方法中对日志进行保存操作,但是对业务代码入侵性大。

或者使用切面针对控制类进行处理,但是灵活度不高。

==》因此决定使用自定义注解 + 切面来针对方法进行日志记录。

目前日志主要记录的有三方面:

  1. 请求的入参,出参
  2. 关于业务上的操作
  3. 异常日常日志的打印

一、自定义注解

创建自定义注解 @AuditLog

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuditLog {

    // 1.操作描述
    String action() default "";

    // 2.操作类型(增删改查)
    OperateEnum type() default OperateEnum.MODIFY;

    // 3.是否记录参数
    boolean isRecord() default true;
}

操作类型枚举类:新增、删除、修改、查询。

java 复制代码
public enum OperateEnum {
    ADD,
    DELETE,
    MODIFY,
    SAVE_OR_MODIFY,
    SELECT;
}

二、定义一个切面

创建一个切面类AuditAspect ,用于捕获带有**@AuditLog**注解的方法调用并记录日志:

java 复制代码
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;

/**
 * 日志记录切面
 */
@Slf4j
@Aspect
@Component
public class AuditAspect {

    @Pointcut(value = "@annotation(com.bocloud.devops.eaas.api.log.AuditLog)")
    public void logMethodPointCut() {
    }

    @Around("logMethodPointCut()")
    public Object recordSysLog(ProceedingJoinPoint point) throws Throwable {
        UserLogRecordDO userLogRecordDO = bulidRequestParams(point);

        //先执行业务

        try {
            Object result = point.proceed();
            if (result != null) {
                userLogRecordDO.setResultMsg(JSONUtil.toJsonStr(result));
            }
            return result;
        } catch (BusinessException e) {
            userLogRecordDO.setResultCode(e.getCode());
            userLogRecordDO.setResultMsg(e.getMessage());
            throw e;
        } catch (Exception e) {
            userLogRecordDO.setResultCode("");
            userLogRecordDO.setResultMsg("后端未知异常");
            throw e;
        } finally {
            //操作日志入库
            try {
                //XXXService.save(userLogRecordDO);
            } catch (Exception e) {
                log.warn("记录用户操作异常:{}", e.getMessage());
            }

        }
    }


    /**
     * 记录方法和方法入参
     * @param point
     * @return
     */
    protected UserLogRecordDO bulidRequestParams(ProceedingJoinPoint point) {
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();
        AuditLog annotation = method.getAnnotation(AuditLog.class);

        UserLogRecordDO userLogRecordDO = new UserLogRecordDO();
        userLogRecordDO.setAction(annotation.action());
        userLogRecordDO.setType(annotation.type().name());
        userLogRecordDO.setMethodName(method.getDeclaringClass().getSimpleName() + "." + method.getName());

        try {
            // 处理入参
            Parameter[] parameters = methodSignature.getMethod().getParameters();
            HashMap<String, Object> paramMap = new HashMap<>();
            Object[] args = point.getArgs();
            for (int i = 0; i < parameters.length; i++) {
                AuditLog auditLog = parameters[i].getAnnotation(AuditLog.class);
                if (auditLog != null) {
                    continue;
                }

                Class<?> type = parameters[i].getType();
                if (ServletResponse.class.isAssignableFrom(type) || ServletRequest.class.isAssignableFrom(type)) {
                    continue;
                }
                String name = parameters[i].getName();
                paramMap.put(name, args[i]);
            }

            userLogRecordDO.setMethodParams(JSONUtil.toJsonStr(paramMap));
            return userLogRecordDO;
        } catch (Exception e) {
            log.warn("构建入参异常:{}", e.getMessage());
        }
        return userLogRecordDO;
    }
}
  • logMethodPointCut(): 这个方法定义了一个切入点(pointcut),它使用@annotation注解来匹配带有com.bocloud.devops.eaas.api.log.AuditLog注解的方法。这意味着切面将在这些被标记为@AuditLog的方法执行前后生效。

  • recordSysLog(ProceedingJoinPoint point): 这是一个环绕通知方法,用于包围切入点方法的执行。它负责记录系统日志和审计信息。具体的操作包括:

    • 构建请求参数并创建一个UserLogRecordDO对象,其中包括操作描述、操作类型、方法名等信息。
    • 调用point.proceed()来执行切入点方法,捕获方法的执行结果,将结果信息记录到UserLogRecordDO中。
    • 处理可能抛出的BusinessException异常,记录异常信息。
    • finally块中尝试将操作日志入库,但在此示例中,入库的部分被注释掉。
  • bulidRequestParams(ProceedingJoinPoint point): 这个方法用于构建方法的参数信息,并将其记录到UserLogRecordDO对象中。它包括方法名、操作描述、操作类型以及方法的入参信息。此方法通过反射分析方法参数和参数上的AuditLog注解,以构建参数信息的JSON表示。

此切面的主要目的是在标记了@AuditLog注解的方法执行前后记录操作日志信息。在实际应用中,您需要确保数据库操作以及记录日志的部分(在finally块中)按照实际需求进行配置。此示例代码中的入库部分被注释掉,您需要根据自己的需求实现相应的数据持久化逻辑。

三、记录日志的实体类

java 复制代码
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import lombok.Data;

/**
 * @program: eaas-center
 * @description:
 * @author: yanchao
 * @create: 2023-10-10 23:09
 **/
@Data
@ApiOperation("操作日志记录表")
public class UserLogRecordDO {


    @ApiModelProperty("主键")
    private String id;

    @ApiModelProperty("行为描述")
    private String action;

    @ApiModelProperty("执行方法")
    private String methodName;

    @ApiModelProperty("执行入参")
    private String methodParams;

    @ApiModelProperty("操作类型")
    private String type;

    @ApiModelProperty("响应编码")
    private String resultCode;

    @ApiModelProperty("结果描述")
    private String resultMsg;

}

最后:使用该注解

java 复制代码
    /**
     * 通过id查询中心维护表
     *
     * @param id 主键
     * @return 单条数据
     */
    @GetMapping("{id}")
    @AuditLog(action = "通过id查询中心维护表", type = OperateEnum.SELECT)
    public Result<EaasCentralMaintainQueryDetailRes> detail(@PathVariable("id") Integer id{
        return Result.success(this.eaasCentralMaintainService.detail(id));
    }

这样即可在数据库查看相应的操作日志记录。

相关推荐
考虑考虑21 小时前
Jpa使用union all
java·spring boot·后端
用户37215742613521 小时前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊1 天前
Java学习第22天 - 云原生与容器化
java
渣哥1 天前
原来 Java 里线程安全集合有这么多种
java
间彧1 天前
Spring Boot集成Spring Security完整指南
java
间彧1 天前
Spring Secutiy基本原理及工作流程
java
Java水解1 天前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆1 天前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学1 天前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole1 天前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端