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

背景:

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

解决办法:

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

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

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

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

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

一、自定义注解

创建自定义注解 @AuditLog 

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;
}

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

public enum OperateEnum {
    ADD,
    DELETE,
    MODIFY,
    SAVE_OR_MODIFY,
    SELECT;
}

二、定义一个切面

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

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 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块中)按照实际需求进行配置。此示例代码中的入库部分被注释掉,您需要根据自己的需求实现相应的数据持久化逻辑。

三、记录日志的实体类

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;

}

最后:使用该注解

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

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

你可能感兴趣的:(java,开发语言)