一、@interface 关键字

我们想定义一个自己的注解 需要使用 @interface 关键字来定义。

如定义一个叫 MyAnnotation 的注解:

public @interface MyAnnotation {}

二、元注解

光加上 @interface 关键字 还不够,我们还需要了解5大元注解

  1. @Retention

  2. @Target

  3. @Documented

  4. @Inherited(JDK8引入)

@Retention:定义注解的保留策略
@Retention(RetentionPolicy.SOURCE)             //注解仅存在于源码中,在class字节码文件中不包含
@Retention(RetentionPolicy.CLASS)              //默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得,
@Retention(RetentionPolicy.RUNTIME)            //注解会在class字节码文件中存在,在运行时可以通过反射获取到
-------------------------------------------------------------------------------------------
@Target:指定被修饰的Annotation可以放置的位置(被修饰的目标)
@Target(ElementType.TYPE)                      //接口、类
@Target(ElementType.FIELD)                     //属性
@Target(ElementType.METHOD)                    //方法
@Target(ElementType.PARAMETER)                 //方法参数
@Target(ElementType.CONSTRUCTOR)               //构造函数
@Target(ElementType.LOCAL_VARIABLE)            //局部变量
@Target(ElementType.ANNOTATION_TYPE)           //注解
@Target(ElementType.PACKAGE)                   //包
注:可以指定多个位置,例如:
@Target({ElementType.METHOD, ElementType.TYPE}),也就是此注解可以在方法和类上面使用
-------------------------------------------------------------------------------------------
@Inherited:指定被修饰的Annotation将具有继承性
-------------------------------------------------------------------------------------------
@Documented:指定被修饰的该Annotation可以被javadoc工具提取成文档.

三、什么是AOP

Spring AOP(面向切面编程)是Spring框架的一个重要模块,用于通过在运行时动态地织入到现有的类中,实现横切关注点的功能。横切关注点是那些会影响多个模块的功能,如日志、事务管理、安全性等。

Spring AOP提供了一种方便的方式来实现横切关注点的功能,而无需修改目标对象的源代码。他通过代理机制,在目标对象的方法执行前、执行后或发生异常时插入额外的逻辑,从而实现注入性能监控、事务管理、日志记录等功能。这样可以避免在每个需要这些功能的方法中都编写重复的代码,提高了代码的重用性和可维护性。

3.1 核心概念

  1. 切点(Pointcut):定义了在那些连接点(Join Point)上应用通知(Advice)。切点是一个表达式,用于匹配连接点。

  2. 通知(Advice):定义了在切点处执行的逻辑,比如在方法执行前后执行的代码。

  3. 连接点(Join Point):在应用程序执行过程中的某个特定点,必须方法执行时、异常抛出等,连接点是代码的具体执行位置,它代表了横切关注点可以插入的地方。

  4. 切面(Aspect):切面是切点和通知的组合,他定义了在哪里以及何时应用通知。

3.2 通知类型

通知有五种:

  1. 前置通知@Before:目标方法调用前被调用后置通知使用

  2. 最终通知@After:目标方法执行完之后执行

  3. 后置通知@AfterReturning:通知方法在目标方法返回后调用

  4. 异常通知@AfterThrowing:通知方法在目标方法抛出异常后调用

  5. 环绕通知@Around:通知包裹目标方法,在目标方法执行之前、执行之后添加自定义的行为

执行顺序:@Around => @Before => 接口中的逻辑代码 => @After => @AfterReturning

除 @Around 外,每个方法里都可以加或者不加参数 JoinPoint

@Around 参数必须为 ProceedingJoinPoint

光说肯定不如来个案例来的清晰,下面是具体的例子来介绍如何基于AOP和自定义注解实现日志记录

四、实战一

4.1 引入依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

4.2 定义注解

aop/annotation/ZkpLog

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

}

4.3 定义切面

aop/ZkpLog

@Component
@Aspect
@Slf4j
public class ZkpLogAspect {

    //切点,表示在注解切入
    @Pointcut("@annotation(com.zkp.shortlink.project.annotation.ZkpLog)")
    public void zkpLogAspect() {}

    //表示在方法执行前执行
    @Before("zkpLogAspect()")
    public void beforeZkpLog(JoinPoint joinPoint){
        ServletRequestAttributes requestAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();

        String methodName = joinPoint.getSignature().getName();
        System.out.println("========================================= Method " + methodName + "() begin=========================================");
        // 执行时间
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date d= new Date();
        String time = sdf.format(d);
        System.out.println("Time           : " + time);
        // 打印请求 URL
        System.out.println("URL :            " + request.getRequestURL());
        // 打印 请求方法
        System.out.println("HTTP Method:     " + request.getMethod());
        // 打印controller 的全路径以及执行方法
        System.out.println("Class Method   : " + joinPoint.getSignature().getDeclaringTypeName() + "." + methodName);
        // 打印请求的 IP
        System.out.println("IP             : " + request.getRemoteHost());
        // 打印请求入参
        System.out.println("Request Args   : " + JSON.toJSONString(joinPoint.getArgs()));
        System.out.println("=======================================================================================================");
    }

    //表示在方法执行后执行
    @After("zkpLogAspect()")
    public void afterZkpLog(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("========================================= Method " + methodName + "() end =========================================");
    }

}

4.4 使用

@ZkpLog
@PostMapping("/api/short-link/v1/create")
public Result<ShortLinkCreateRespDTO> createShortLink(@RequestBody ShortLinkCreateReqDTO requestParam){
    ShortLinkCreateRespDTO result = shortLinkService.createShortLink(requestParam);
    return Results.success(result);
}

4.5 效果

到现在就基本实现了日志记录的功能啦!上面那个枚举是没参数的,下面再来一个有参数的来加深一下大家对自定义注解的认识

五、实战二

5.1 定义注解

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

    String type() default "";

}

5.2 定义切面

@Aspect
@Component
@Slf4j
public class AttendanceOperationLogAop {

    private final static String TYPE_ADD = "新增";
    private final static String TYPE_APPROVAL = "审批";

    //切点
   @Pointcut("@annotation(com.zkp.attendance.domain.aop.annotation.OperationLogAnnotation)")
    public void loggableMethods() {}

    /**
     * @param joinPoint 切点
     * @Description 记录审批操作日志(通过,拒绝,退回)
     * @author zhengkangpan
     * @date 2024/5/9 11:24
     **/ 
    @AfterReturning(value = "loggableMethods()")
    public void approvalOperationLog(JoinPoint joinPoint) {
        //获取方法签名
        String type = getType(joinPoint);
        if (!StrUtil.isEmpty(type) && (TYPE_APPROVAL).equals(type)) {
            Object[] args = joinPoint.getArgs();
            //获取第一个和第二个参数
            String status = (String) args[0];
            AttendanceApplicationDetailsVo attendanceApplicationDetailsVo = (AttendanceApplicationDetailsVo) args[1];
        }
        //具体逻辑,例如记录日志到数据库
        xxxxxxx
    }

    @NotNull
    private String getType(JoinPoint joinPoint) {
        //获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //通过方法签名获取方法对象
        Method method = signature.getMethod();
        //通过方法对象  获取方法对象注解
        OperationLogAnnotation annotation = method.getAnnotation(OperationLogAnnotation.class);
        //获取类型(新增,编辑,提交)
        return annotation.type();
    }    

5.3 使用

@OperationLogAnnotation(type = "审批")
public void approval(String type, AttendanceApplicationDetailsVo detailsVo) {
    //方法具体逻辑
}

尽管已经实现了基本功能,但还是觉得不是很美观,比如@OperationLogAnnotation(type = "审批"),"审批"即为静态常量,而且也不支持多个type,后面就想到了可以用枚举改造

六、实战三(枚举版)

6.1 创建枚举

/**
 * @Description 审计日志操作事件
 * @author zhengkangpan
 * @date 2024/8/6 15:13
 * @return
 **/
@Getter
@AllArgsConstructor
public enum AopOperationEventTypeEnum {

    OUT_USER_REGISTER("外部用户注册"),
    OUT_USER_LOGIN("外部用户登录"),
    CHANGE_PASSWORD("修改密码"),
    FORGET_PASSWORD("忘记密码"),
    OUT_USER_EXAMINE("外部用户审核"),
    ADD_OUT_USER("新增外部用户"),
    EDIT_OUT_USER("编辑外部用户"),
    DELETE_OUT_USER("删除外部用户"),
    ADD_CERTIFICATE("新增证书"),
    EDIT_CERTIFICATE("编辑证书"),
    DELETE_CERTIFICATE("删除证书"),
    CHANGE_SAFE_CONFIG("修改安全配置");

    private String description;
}

6.2 定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLogAnnotation {
    AopOperationEventTypeEnum[] value();
}

6.3 定义切面

/**
 * @Description Aop记录审计日志
 * @Author zhengkangpan
 * @Date 2024/8/6 15:49
 */
@Aspect
@Component
@Slf4j
//@Order(1)
public class AuditLogAop {

    @Autowired
    AuditLogService auditLogService;

    @Autowired
    AuditLogRepository auditLogRepository;

    @Autowired
    UserDDDService userDDDService;

    @Autowired
    OutUserBackRepository userBackRepository;

    @Autowired
    CertificateRepository certificateRepository;

    /**
     * @Description 切点表达式
     * @author zhengkangpan
     * @date 2024/8/6 15:51
     **/
    @Pointcut("@annotation(com.haiyisec.cms.auditLog.domain.aop.annotation.OperationLogAnnotation)")
    public void loggableMethods() {
    }

    @AfterReturning(value = "loggableMethods()", returning = "result")
    public void saveAuditLog(JoinPoint joinPoint, Object result) {
        // 操作成功
        handleAuditLog(joinPoint, EventResultEnum.SUCCESS.getCode(), result, null);
    }

//    @AfterThrowing(value = "loggableMethods()", throwing = "ex")
//    public void handleFailure(JoinPoint joinPoint, Exception ex) {
//        // 操作失败
//        handleAuditLog(joinPoint, EventResultEnum.FAIL.getCode(), ex);
//    }

    /**
     * @Description 获取Type
     * @author zhengkangpan
     * @date 2024/5/9 16:09
     * @param joinPoint 切点
     **/
    private AopOperationEventTypeEnum[] getType(JoinPoint joinPoint) {
        //获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //通过方法签名获取方法对象
        Method method = signature.getMethod();
        //通过方法对象  获取方法对象注解
        OperationLogAnnotation annotation = method.getAnnotation(OperationLogAnnotation.class);
        //获取类型(保存,提交,审批)
        return annotation.value();
    }


    private void handleAuditLog(JoinPoint joinPoint, String eventResult, Object result, Exception ex) {
        AopOperationEventTypeEnum[] types = getType(joinPoint);

        //提取方法参数中的信息,需要的话再从args拿出来强转即可
        Object[] args = joinPoint.getArgs();

        //操作时间
        Date operationTime = new Date();
        //操作者
        String operatorName = "";
        //操作对象
        String operationObject = null;
        //操作内容
        String operationContent = null;
        //操作事件
        String description = null;
        //操作结果
        String operationResult = eventResult;

        for (AopOperationEventTypeEnum type : types) {
            switch (type) {
                case OUT_USER_LOGIN:
                    String errorMsg = ((CommResponseVo<?>) result).getErrorMsg();

                    CommRequestVo<Map<String, Object>> loginRequest = (CommRequestVo<Map<String, Object>>) args[0];
                    String identifier = (String) loginRequest.getData().get("identifier");
                    String address = (String) loginRequest.getData().get("address");

                    //构造日志实体
                    operatorName = identifier;
                    operationObject = identifier;
                    description = AopOperationEventTypeEnum.OUT_USER_LOGIN.getDescription();
                    if (StrUtil.isEmpty(errorMsg)) {
                        //登录成功
                        operationContent = "外部用户登录,源IP地址为【" + address + "】";
                        saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                        break;
                    } else {
                        //登录失败
                        operationContent = "外部用户登录,源IP地址为【" + address + "】,认证失败,原因为【" + errorMsg + "】";
                        saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                        break;
                    }

                case OUT_USER_REGISTER:
                    String registerErrorMsg = ((CommResponseVo<?>) result).getErrorMsg();
                    if (StrUtil.isEmpty(registerErrorMsg)) {
                        //获取方法的第二个参数
                        CommRequestVo<Map<String, Object>> registerRequest = (CommRequestVo<Map<String, Object>>) args[1];
                        String email = (String) registerRequest.getData().get("email");
                        List<String> phoneList = (List<String>) registerRequest.getData().get("phoneList");
                        String realName = (String) registerRequest.getData().get("realName");
                        String businessName = (String) registerRequest.getData().get("businessName");
                        operatorName = email + "_" + realName;
                        description = AopOperationEventTypeEnum.OUT_USER_REGISTER.getDescription();
                        operationObject = email;
                        operationContent = "外部用户注册信息,最终为姓名【" + realName + "】、公司名称【" + businessName + "】、邮箱号码【" + email + "】、手机号码【" + phoneList.get(1) + "】";
                        saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                        break;
                    }else{
                        //不记录失败日志
                        break;
                    }
                case CHANGE_PASSWORD:
                    String changePasswordErrorMsg = ((CommResponseVo<?>) result).getErrorMsg();
                    if (StrUtil.isEmpty(changePasswordErrorMsg)) {
                        //获取方法的第一个参数
                        CommRequestVo<Map<String, Object>> changeRequest = (CommRequestVo<Map<String, Object>>) args[0];
                        if ((String) changeRequest.getData().get("userId") == null) {
                            break;
                        } else {
                            OutUserBackPo backPo = outUserBackRepository.getOne((String) changeRequest.getData().get("userId"));
                            if (backPo == null) {
                                throw new AppException(null, "该用户不存在");
                            }
                            operatorName = backPo.getEmail() + "_" + backPo.getRealName();
                            description = AopOperationEventTypeEnum.CHANGE_PASSWORD.getDescription();
                            operationObject = backPo.getEmail() + "_" + backPo.getRealName();
                            operationContent = "外部用户修改了密码";
                            saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                            break;
                        }
                    }else{
                        //不记录失败日志
                        break;
                    }
                case FORGET_PASSWORD:
                    String forgetPasswordErrorMsg = ((CommResponseVo<?>) result).getErrorMsg();
                    if (StrUtil.isEmpty(forgetPasswordErrorMsg)) {
                        //获取方法的第一个参数
                        CommRequestVo<Map<String, Object>> forgetRequest = (CommRequestVo<Map<String, Object>>) args[0];
                        if ((String) forgetRequest.getData().get("userId") != null) {
                            break;
                        } else {
                            OutUserBackPo backPo = outUserBackRepository.findByEmail((String) forgetRequest.getData().get("email"));
                            if (backPo == null) {
                                throw new AppException(null, "该用户不存在");
                            }
                            operatorName = backPo.getRealName();
                            description = AopOperationEventTypeEnum.FORGET_PASSWORD.getDescription();
                            operationObject = backPo.getEmail();
                            operationContent = "通过邮箱找回密码并改密";
                            saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                            break;
                        }
                    }else{
                        //不记录失败日志
                        break;
                    }
                case OUT_USER_EXAMINE:
                    //获取方法的第三个参数
                    OutUserExamineVo queryVo = (OutUserExamineVo) args[2];
                    operatorName = getOperatorName();
                    operationObject = queryVo.getEmail();
                    String examine = queryVo.getExamineStatus().equals(ExamineStatusEnum.PASS.getCode()) ? ExamineStatusEnum.PASS.getDescription() : ExamineStatusEnum.REFUSE.getDescription();
                    operationContent = "审核了【" + queryVo.getRealName() + "】的注册信息,审核结果为【" + examine + "】";
                    description = AopOperationEventTypeEnum.OUT_USER_EXAMINE.getDescription();
                    saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                    break;
                case ADD_OUT_USER:
                    //获取方法的第一个参数
                    OutUserVo addOutUserVo = (OutUserVo) args[0];
                    List<String> addPhoneList = addOutUserVo.getPhoneList();
                    operatorName = getOperatorName();
                    operationContent = "新增外部用户【" + addOutUserVo.getRealName() + "】,最终为姓名【" + addOutUserVo.getRealName() + "】、用户类型【" + PersonTypeEnum.getMsgByCode(addOutUserVo.getPersonType()) + "】、关联公司名称【" + addOutUserVo.getAssociateBusinessName() + "】、邮箱号码【" + addOutUserVo.getEmail() + "】、手机号码【" + addPhoneList.get(0) + " " + addPhoneList.get(1) + "】";
                    description = AopOperationEventTypeEnum.ADD_OUT_USER.getDescription();
                    saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                    break;
                case EDIT_OUT_USER:
                    //获取方法的第一个参数
                    OutUserVo editOutUserVo = (OutUserVo) args[0];
                    operatorName = getOperatorName();
                    operationObject = "外部用户【" + editOutUserVo.getOldName() + "】";
                    operationContent = "编辑外部用户名称【" + editOutUserVo.getOldName() + "】,最终为姓名【" + editOutUserVo.getRealName() + "】、用户类型【" + PersonTypeEnum.getMsgByCode(editOutUserVo.getPersonType()) + "】、关联公司名称【" + editOutUserVo.getAssociateBusinessName() + "】、邮箱号码【" + editOutUserVo.getEmail() + "】、手机号码【" + editOutUserVo.getPhoneList().get(0) + " " + editOutUserVo.getPhoneList().get(1) + "】";
                    description = AopOperationEventTypeEnum.EDIT_OUT_USER.getDescription();
                    saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                    break;
                case DELETE_OUT_USER:
                    AuditDeleteReturnVo auditDeleteReturnVo1 = (AuditDeleteReturnVo) result;
                    OutUserBackPo userBackPo = auditDeleteReturnVo1.getOutUserBackPo();
                    operatorName = getOperatorName();
                    description = AopOperationEventTypeEnum.DELETE_OUT_USER.getDescription();
                    operationObject = "外部用户【" + userBackPo.getEmail() + "_" + userBackPo.getRealName() + "】";
                    operationContent = "删除外部用户【" + userBackPo.getRealName() + "】";
                    saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                    break;
                case DELETE_CERTIFICATE:
                    AuditDeleteReturnVo auditDeleteReturnVo2 = (AuditDeleteReturnVo) result;
                    CertificatePo certificatePo = auditDeleteReturnVo2.getCertificatePo();
                    operatorName = getOperatorName();
                    description = AopOperationEventTypeEnum.DELETE_CERTIFICATE.getDescription();
                    operationObject = certificatePo.getCertificateNumber();
                    operationContent = "删除了证书";
                    saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                    break;
                case CHANGE_SAFE_CONFIG:
                    //获取方法的第一个参数
                    SafeConfigVo safeVo = (SafeConfigVo) args[0];
                    operatorName = getOperatorName();
                    description = AopOperationEventTypeEnum.CHANGE_SAFE_CONFIG.getDescription();
                    operationContent = "修改安全配置,最终安全配置10分钟内最多尝试错误密码【" + safeVo.getMaxTryPassword() + "】次,密码有效期【" + safeVo.getPasswordValidity() + "】天,禁止使用历史密码个数【" + safeVo.getForbiddenPasswordsQuantity() + "】个,同个ip一天内最多注册次数【" + safeVo.getDailyIpMaxRegister() + "】次";
                    saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                    break;
                case ADD_CERTIFICATE:
                    //获取参数,判断是编辑还是新增
                    Boolean isEdit = (Boolean) args[2];
                    if (!isEdit) {
                        //获取返回结果
                        CertificatePo certificatePoAdd = (CertificatePo) result;
                        operatorName = getOperatorName();
                        description = AopOperationEventTypeEnum.ADD_CERTIFICATE.getDescription();
                        operationObject = certificatePoAdd.getCertificateNumber();
                        OutUserBackPo outUserBackPo = userBackRepository.getOne(certificatePoAdd.getUserId());
                        operationContent = String.format("新增了证书,最终证书编号【%s】、证书类型【%s】、产品类型【%s】、姓名【%s】、公司名称【%s】、有效期至【%s】", certificatePoAdd.getCertificateNumber(), CertificateTypeEnum.getMsgByCode(certificatePoAdd.getCertificateType()), ProductType.getMsgByCode(certificatePoAdd.getProductType()), certificatePoAdd.getShowUserName(), outUserBackPo.getAssociateBusinessName(), DateUtil.format(certificatePoAdd.getGraduationTime(), "yyyy-MM-dd"));
                    } else {
                        //获取返回结果
                        CertificatePo certificatePoEdit = (CertificatePo) result;
                        operatorName = getOperatorName();
                        description = AopOperationEventTypeEnum.EDIT_CERTIFICATE.getDescription();
                        operationObject = certificatePoEdit.getCertificateNumber();
                        OutUserBackPo backPo = userBackRepository.getOne(certificatePoEdit.getUserId());
                        operationContent = String.format("编辑了证书,最终证书编号【%s】、证书类型【%s】、产品类型【%s】、姓名【%s】、公司名称【%s】、有效期至【%s】", certificatePoEdit.getCertificateNumber(), CertificateTypeEnum.getMsgByCode(certificatePoEdit.getCertificateType()), ProductType.getMsgByCode(certificatePoEdit.getProductType()), certificatePoEdit.getShowUserName(), backPo.getAssociateBusinessName(), DateUtil.format(certificatePoEdit.getGraduationTime(), "yyyy-MM-dd"));
                    }
                    saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
                    break;
                default:
                    log.error("无对应的操作事件!");
                    break;
            }
        }
    }


    /**
     * @Description 获取登录子用户
     * @author zhengkangpan
     * @date 2024/9/9 14:21
     * @return String
     **/
    private String getOperatorName() {
        String operatorName = "";
        if (UserUtil.getLoginUser() != null) {
            operatorName = UserUtil.getLoginUser().getName();
        } else {
            SysUserVo sysUserVo = userDDDService.getById(UserUtil.getCurUserId());
            if (sysUserVo == null) {
                throw new AppException("ERR_CRM80001", "无该对象操作权限或对象不存在!");
            }
            operatorName = sysUserVo.getLoginName();
        }
        return operatorName;
    }

}

6.4 使用

@OperationLogAnnotation(AopOperationEventTypeEnum.EDIT_OUT_USER)
public void editExternalUser(OutUserVo outUserVo, CheckResult cr) {
    //方法逻辑
}

@Transactional(rollbackFor = Exception.class)
@OperationLogAnnotation({AopOperationEventTypeEnum.CHANGE_PASSWORD, AopOperationEventTypeEnum.FORGET_PASSWORD})
public CommResponseVo<Map<String, Object>> changePassword(CommRequestVo<Map<String, Object>> requestVo) {
    //方法逻辑
}

相关文章

敏感数据脱敏实现

基于自定义注解+AOP实现分布式锁防重复提交