SpringBoot项目使用AOP及自定义注解保存操作日志

简介: SpringBoot项目使用AOP及自定义注解保存操作日志

image.png

@[toc]

概述:

该SpringBoot项目使用AOP的环绕@Around注解及自定义注解保存操作日志到数据库,自定义注解中会配置日志模板类型logModelType字段,通过该字段去匹配是创建、删除、修改...等等功能,本案例就是为了模拟现实项目中通过AOP及自定义注解如何保存操作详情日志功能。

特色

form表单除了input输入框,也会有一些按钮【Disable/Enable】,这些按钮就需要转换数字值然后动态拼接详情日志参数,日志操作类LogAopAction中拼接参数不只是简单的一堆get、set、if else去拼接,而是根据类型logModelType字段 =》 去找枚举LogDetailEnums =》通过枚举值找常量类LogDetailConstants,常量类中定义了各种类型操作的占位符,动态拼接参数使用MessageFormat.format(),这样使用更加简单、看起来更加优雅、实现可扩展性不用写一堆代码。
image.png

使用方式

  1. 第一步:pom引入AOP
  2. 第二步:创建自定义注解、Bean实体、枚举、常量类
  3. 第三步:Controller层方法使用自定义注解标识
  4. 第四步:新建一个日志操作类LogAopAction,专门用来处理操作保存日志
  5. 第五步:postman模拟调用接口,输出AOP中ProceedingJoinPoint获取目标方法,参数,注解

注意点

  • 注意点1:日志操作类LogAopAction必须加两个注解@Aspect和@Component,其中@Aspect注解代表该类为切面,而@Component为了使该类能让spring容器扫描到
  • 注意点2:@Around注解中配置@annotation注解用来指定生效的自定义注解名字
  • 注意点3:该案例描述AOP中ProceedingJoinPoint获取目标方法,参数,注解
  • 注意点4:接收实体Bean要重新toString方法,不然无法转成json,因为未重写toString方法中用的是等号 "=" 而不是冒号 ":"
  • 注意点5:格式化常量类定义好的占位符请使用MessageFormat.format(),拼接起来更加方便

代码

第一步:pom引入AOP

<!--aop相关的依赖引入-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.78</version>
</dependency>

第二步:创建自定义注解、Bean实体、枚举、常量类

自定义注解LogAnnotation

package com.example.demo.config;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LogAnnotation {
   

    //日志模板类型
    String logModelType();
}

实体类

package com.example.demo.bean;

import lombok.Data;

/**
 * @Author 211145187
 * @Date 2022/2/23 09:32
 **/
@Data
public class OperateTeacherReq {
   
    //id
    private Integer id;
    //姓名
    private String name;
    //开关【0:Disable、1:Enable】
    private Integer pstnFlag;

    @Override
    public String toString() {
   
        return "{" +
                "id:" + id +
                ", name:'" + name + '\'' +
                ", pstnFlag:" + pstnFlag +
                '}';
    }
}

枚举类LogDetailEnums

package com.example.demo.enums;

public enum LogDetailEnums {
   
    /**
     * 开户
     */
    CREATE_ACCOUNT("CREATE_ACCOUNT", LogDetailConstants.CREATE_ACCOUNT),
    /**
     * PSTN Flag值转换
     */
    PSTN_ENABLE_FLAG("pstnFlag_1","Enable"),
    PSTN_DISABLE_FLAG("pstnFlag_0","Disable");

    private String code;

    private String message;

    LogDetailEnums(String code, String message) {
   
        this.code = code;
        this.message = message;
    }

    public String getCode() {
   
        return code;
    }

    public void setCode(String code) {
   
        this.code = code;
    }

    public String getMessage() {
   
        return message;
    }

    public void setMessage(String message) {
   
        this.message = message;
    }

    public static String getDetailByCode(String messageCode){
   
        try {
   
            for (LogDetailEnums value : LogDetailEnums.values()) {
   
                if (value.code.equals(messageCode)){
   
                    return value.message;
                }
            }
        } catch (Exception e) {
   
            e.printStackTrace();
        }
        return "";
    }

    public static LogDetailEnums getLogDetailEnum(String code){
   
        if (code==null){
   
            return null;
        }
        for (LogDetailEnums value : LogDetailEnums.values()) {
   
            if (code.equals(value.code)){
   
                return value;
            }
        }
        return null;
    }
}

常量类LogDetailConstants

package com.example.demo.enums;

public class LogDetailConstants {
   
    public static final String CREATE_ACCOUNT = "【Add Account】id:[{0}], User Name:[{1}],pstnFlag:[{2}]";
}

第三步:Controller层方法使用自定义注解标识

package com.example.demo.controller;

import com.example.demo.bean.TeacherReq;
import com.example.demo.config.LogAnnotation;
import com.example.demo.mapper.TeacherMapper;
import com.example.demo.response.Response;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

/**
 * @Author 211145187
 * @Date 2022/5/7 11:34
 **/
@RestController
public class Controller {
   
@RequestMapping(value = "/operateLog", method = RequestMethod.POST)
    @LogAnnotation(logModelType = "CREATE_ACCOUNT")
    public Response operateLog(@RequestBody OperateTeacherReq req) {
   
        return Response.success(req);
    }
}

第四步:新建一个日志操作类LogAopAction,专门用来处理操作保存日志

package com.example.demo.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.bean.OperateTeacherReq;
import com.example.demo.enums.LogDetailEnums;
import com.example.demo.response.Response;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.text.MessageFormat;

@Aspect
@Component
public class LogAopAction {
   

    /**
     * AOP切面保存操作日志
     * @Author 211145187
     * @Around 注解描述的方法为一个环绕通知方法,在此方法中可以添加扩展业务逻辑,可以调用下一个切面对象或目标方法
     * @param point 连接点(此连接点只应用@Around描述的方法)
     * @Date 2022/5/16 14:38
     * @param point
     * @Return Response
     **/
    @Around("@annotation(com.example.demo.config.LogAnnotation) && execution(* com.example.demo.controller.*.*(..))")
    public Response logOperate(ProceedingJoinPoint point) throws NoSuchMethodException {
   
        //获取类的字节码对象,通过字节码对象获取方法信息
        Class<?> targetCls=point.getTarget().getClass();
        //获取方法签名(通过此签名获取目标方法信息)
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //获取目标方法上的注解指定的操作名称
        Method targetMethod=
                targetCls.getDeclaredMethod(
                        signature.getName(),
                        signature.getParameterTypes());
        System.out.println("获取目标方法上的注解指定的操作名称:"+targetMethod);
        LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
        String logModelType=logAnnotation.logModelType();
        System.out.println("获取自定义注解参数值logModelType:" + logModelType);
        //获取目标方法名(目标类型+方法名)
        String targetClsName=targetCls.getName();
        String targetObjectMethodName=targetClsName+"."+method.getName();
        System.out.println("获取目标方法名:" + targetObjectMethodName);
        //获取请求参数
        Object[] args = point.getArgs();
        //TODO 操作日志保存到数据库中
        String logDetailInfo = LogDetailEnums.getDetailByCode(logModelType);
        switch (LogDetailEnums.getLogDetailEnum(logModelType)) {
   
            case CREATE_ACCOUNT:
                OperateTeacherReq req = JSONObject.parseObject(JSON.toJSONString(args[0]), OperateTeacherReq.class);
                System.out.println("获取请求参数:" + req);
                JSONObject jsonObject = JSONObject.parseObject(req.toString());
                logDetailInfo = MessageFormat.format(logDetailInfo, req.getId(), req.getName(), LogDetailEnums.getDetailByCode("pstnFlag_" + jsonObject.getString("pstnFlag")));
                break;
            default:
                System.out.println("无该类型!");
                break;
        }
        return Response.success(logDetailInfo);
    }
}

第五步:postman模拟调用接口,输出AOP中ProceedingJoinPoint获取目标方法,参数,注解

常量类详情日志占位符:
public static final String CREATE_ACCOUNT = "【Add Account】id:[{0}], User Name:[{1}],pstnFlag:[{2}]";

postman调用接口

image.png

postman结果打印

image.png

控制台打印:

image.png

获取目标方法上的注解指定的操作名称:public com.example.demo.response.Response com.example.demo.controller.Controller.operateLog(com.example.demo.bean.OperateTeacherReq)
获取自定义注解参数值logModelType:CREATE_ACCOUNT
获取目标方法名:com.example.demo.controller.Controller.operateLog
获取请求参数:{
   id:1, name:'教师1', pstnFlag:1}

项目代码路径图片

image.png

相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
目录
相关文章
|
5月前
|
XML 安全 Java
使用 Spring 的 @Aspect 和 @Pointcut 注解简化面向方面的编程 (AOP)
面向方面编程(AOP)通过分离横切关注点,如日志、安全和事务,提升代码模块化与可维护性。Spring 提供了对 AOP 的强大支持,核心注解 `@Aspect` 和 `@Pointcut` 使得定义切面与切入点变得简洁直观。`@Aspect` 标记切面类,集中处理通用逻辑;`@Pointcut` 则通过表达式定义通知的应用位置,提高代码可读性与复用性。二者结合,使开发者能清晰划分业务逻辑与辅助功能,简化维护并提升系统灵活性。Spring AOP 借助代理机制实现运行时织入,与 Spring 容器无缝集成,支持依赖注入与声明式配置,是构建清晰、高内聚应用的理想选择。
606 0
|
Java API 数据安全/隐私保护
(工作经验)优雅实现接口权限校验控制:基于自定义注解、AOP与@ConditionalOnProperty配置开关的通用解决方案
(工作经验)优雅实现接口权限校验控制:基于自定义注解、AOP与@ConditionalOnProperty配置开关的通用解决方案
651 1
|
存储 安全 Java
Spring Boot 3 集成Spring AOP实现系统日志记录
本文介绍了如何在Spring Boot 3中集成Spring AOP实现系统日志记录功能。通过定义`SysLog`注解和配置相应的AOP切面,可以在方法执行前后自动记录日志信息,包括操作的开始时间、结束时间、请求参数、返回结果、异常信息等,并将这些信息保存到数据库中。此外,还使用了`ThreadLocal`变量来存储每个线程独立的日志数据,确保线程安全。文中还展示了项目实战中的部分代码片段,以及基于Spring Boot 3 + Vue 3构建的快速开发框架的简介与内置功能列表。此框架结合了当前主流技术栈,提供了用户管理、权限控制、接口文档自动生成等多项实用特性。
1012 8
|
XML Java 数据格式
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
本文介绍了如何使用Spring框架的注解方式实现AOP(面向切面编程)。当目标对象没有实现接口时,Spring会自动采用CGLIB库进行动态代理。文中详细解释了常用的AOP注解,如`@Aspect`、`@Pointcut`、`@Before`等,并提供了完整的示例代码,包括业务逻辑类`User`、配置类`SpringConfiguration`、切面类`LoggingAspect`以及测试类`TestAnnotationConfig`。通过这些示例,展示了如何在方法执行前后添加日志记录等切面逻辑。
1435 2
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
4313 31
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
9月前
|
监控 容灾 算法
阿里云 SLS 多云日志接入最佳实践:链路、成本与高可用性优化
本文探讨了如何高效、经济且可靠地将海外应用与基础设施日志统一采集至阿里云日志服务(SLS),解决全球化业务扩展中的关键挑战。重点介绍了高性能日志采集Agent(iLogtail/LoongCollector)在海外场景的应用,推荐使用LoongCollector以获得更优的稳定性和网络容错能力。同时分析了多种网络接入方案,包括公网直连、全球加速优化、阿里云内网及专线/CEN/VPN接入等,并提供了成本优化策略和多目标发送配置指导,帮助企业构建稳定、低成本、高可用的全球日志系统。
954 54
|
监控 安全 Apache
什么是Apache日志?为什么Apache日志分析很重要?
Apache是全球广泛使用的Web服务器软件,支持超过30%的活跃网站。它通过接收和处理HTTP请求,与后端服务器通信,返回响应并记录日志,确保网页请求的快速准确处理。Apache日志分为访问日志和错误日志,对提升用户体验、保障安全及优化性能至关重要。EventLog Analyzer等工具可有效管理和分析这些日志,增强Web服务的安全性和可靠性。
434 9
|
存储 SQL 关系型数据库
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log、原理、写入过程;binlog与redolog区别、update语句的执行流程、两阶段提交、主从复制、三种日志的使用场景;查询日志、慢查询日志、错误日志等其他几类日志
926 35
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log
|
12月前
|
存储 缓存 关系型数据库
图解MySQL【日志】——Redo Log
Redo Log(重做日志)是数据库中用于记录数据页修改的物理日志,确保事务的持久性和一致性。其主要作用包括崩溃恢复、提高性能和保证事务一致性。Redo Log 通过先写日志的方式,在内存中缓存修改操作,并在适当时候刷入磁盘,减少随机写入带来的性能损耗。WAL(Write-Ahead Logging)技术的核心思想是先将修改操作记录到日志文件中,再择机写入磁盘,从而实现高效且安全的数据持久化。Redo Log 的持久化过程涉及 Redo Log Buffer 和不同刷盘时机的控制参数(如 `innodb_flush_log_at_trx_commit`),以平衡性能与数据安全性。
666 5
图解MySQL【日志】——Redo Log