Spring Event与AOP结合:优雅记录日志的艺术

在构建现代化的应用中,日志记录是不可或缺的一环。

Spring 框架为我们提供了强大的事件机制(Spring Event)和切面编程(AOP),结合使用可以实现优雅的日志记录,使得代码更加模块化和可维护。

本文将介绍如何结合Spring Event和AOP,以及如何在不同场景下应用这两个强大的特性。

代码地址:

https://github.com/bangbangzhou/spring-boot-event-log-demo.git

1.Spring Event 与 AOP 简介

1.1. Spring Event

Spring Event是Spring的事件通知机制,可以将相互耦合的代码解耦,从而方便功能的修改与添加。Spring Event是监听者模式的一个具体实现。

监听者模式包含了监听者Listener、事件Event、事件发布者EventPublish,过程就是EventPublish发布一个事件,被监听者捕获到,然后执行事件相应的方法。

1.2. AOP

AOP(Aspect-Oriented Programming)是一种编程范式,它允许我们通过切面(Aspect)将横切关注点(Cross-Cutting Concerns)模块化。切面是一个模块,它定义了在程序中的何处执行横切关注点逻辑。

AOP作用:在不修改原始代码的基础上对其进行增强

应用场景

  • 事务处理
  • 日志记录
  • 用户权限
  • ……
图片[1]-Spring Event与AOP结合:优雅记录日志的艺术-不念博客

SpringBoot-自定义配置类-实现日志记录Spring Event和AOP,我们可以实现在系统关键操作发生时记录日志的功能。

这使得日志记录变得更加灵活和可配置,而不需要在每个业务方法中硬编码日志逻辑。

2. 代码实现

项目结构如下:

图片[2]-Spring Event与AOP结合:优雅记录日志的艺术-不念博客

【步骤0】:创建maven工程spring-boot-event-log-demo并配置pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.7.15</version>
    </parent>


    <groupId>com.zbbmeta</groupId>
    <artifactId>spring-boot-event-log-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--        aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--        fastjson2-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.35</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.20</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

【步骤一】:配置application.yml

配置项目信息

server:
  port: 8890

【步骤二】:创建OptLogDTO类,用于封装操作日志信息

com.zbbmeta.dto包下创建OptLogDTO

@Data
@Accessors(chain = true)
public class OptLogDTO implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 日志类型
     */
    private String type;

    /**
     * 日志标题
     */
    private String title;
    /**
     * 操作内容
     */
    private String operation;
    /**
     * 执行方法
     */

    private String method;

    /**
     * 请求路径
     */
    private String url;
    /**
     * 参数
     */
    private String params;
    /**
     * ip地址
     */
    private String ip;
    /**
     * 耗时
     */
    private Long executeTime;
    /**
     * 地区
     */
    private String location;
    /**
     * 创建人
     */
    private String createBy;

    /**
     * 创建时间
     */
    private Date startTime;
    /**
     * 更新时间
     */
    private Date endTime;


    /**
     * 异常信息
     */

    private String exception;
}

【步骤三】:定义事件类

com.zbbmeta.event包下创建事件类SysLogEvent

/**
 * 定义系统日志事件
 */
public class SysLogEvent extends ApplicationEvent {
    public SysLogEvent(OptLogDTO optLogDTO) {
        super(optLogDTO);
    }
}

【步骤四】:定义事件监听器

com.zbbmeta.listener包下创建监听器类SysLogListener

在监听器中可以将日志输出到数据库

/**
 * 异步监听日志事件
 */
@Component
public class SysLogListener {
    @Async//异步处理
    @EventListener(SysLogEvent.class)
    public void saveSysLog(SysLogEvent event) {
        OptLogDTO sysLog = (OptLogDTO) event.getSource();
        long id = Thread.currentThread().getId();
        //TODO 可以输出日志到数据库
        System.out.println("监听到日志操作事件:" + sysLog + " 线程id:" + id);
        //将日志信息保存到数据库...
    }
}

【步骤五】:定义切面

定义切入点表达式、配置切面(绑定切入点与通知关系),用于记录每次发送请求时方法名,参数,时间等信息

com.zbbmeta.aspect包下创建LogAspect

@Slf4j
@Aspect
@Component
public class LogAspect {

    @Autowired
    private final ApplicationContext applicationContext;


    public LogAspect(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Pointcut("execution(* *..*Controller.*(..))")
    public void pointcut() {
    }

    /**
     * 环绕通知,使用Pointcut()上注册的切入点
     * @param point
     * @return
     */
    @Around("pointcut()")
    public Object recordLog(ProceedingJoinPoint point) throws Throwable {
        Object result = new Object();

        // 获取request
        HttpServletRequest request = RequestHolder.getHttpServletRequest();


        // 判断为空则直接跳过执行
        if (ObjectUtils.isEmpty(request)){
            return point.proceed();
        }
        // 获取注解里的value值
        Method targetMethod = resolveMethod(point);
        // 打印执行时间
        Date now = DateUtil.date();
        // 请求方法
        String method = request.getMethod();
        String url = request.getRequestURI();

        // 获取IP和地区
        String ip = RequestHolder.getHttpServletRequestIpAddress();
        String region = IPUtil.getCityInfo(ip);

        //获取请求参数
        // 参数
        Object[] args = point.getArgs();
        String requestParam = getArgs(args, request);
        Date end = null;
        // 计算耗时
        long tookTime = 0L;
        try {
            result = point.proceed();
        } finally {
            end = DateUtil.date();

            tookTime = DateUtil.between(now, end, DateUnit.SECOND);
        }
        // 如果是登录请求,则不获取用户信息
        String userName = "springboot葵花宝典";
        // 封装optLogDTO
        OptLogDTO optLogDTO = new OptLogDTO();
        optLogDTO.setIp(ip)
                .setCreateBy(userName)
                .setMethod(method)
                .setUrl(url)
                .setStartTime(now)
                .setEndTime(end)
                .setType("1")
                .setOperation(String.valueOf(result))
                .setLocation(StrUtil.isEmpty(region) ? "本地" : region)
                .setExecuteTime(tookTime)
                .setParams(JSON.toJSONString(requestParam));


        ApplicationEvent event = new SysLogEvent(optLogDTO);

        //发布事件
        applicationContext.publishEvent(event);

        long id = Thread.currentThread().getId();
        System.out.println("发布事件,线程id:" + id);


        return result;
    }

    /**
     * 配置异常通知
     *
     * @param point join point for advice
     * @param e exception
     */
    @AfterThrowing(pointcut = "pointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint point, Throwable e) {
        // 打印执行时间
        long startTime = System.nanoTime();

        Date now = DateUtil.date();

        OptLogDTO optLogDTO = new OptLogDTO();

        // 获取IP和地区
        String ip = RequestHolder.getHttpServletRequestIpAddress();
        String region = IPUtil.getCityInfo(ip);


        // 获取request
        HttpServletRequest request = RequestHolder.getHttpServletRequest();

        // 请求方法
        String method = request.getMethod();
        String url = request.getRequestURI();

        // 获取注解里的value值
        Method targetMethod = resolveMethod((ProceedingJoinPoint) point);

        optLogDTO.setExecuteTime( DateUtil.between(now, DateUtil.date(), DateUnit.SECOND))
                .setIp(ip)
                .setLocation(region)
                .setMethod(method)
                .setUrl(url)
                .setType("2")
                .setException(getStackTrace(e));
        // 发布事件
        log.info("Error Result: {}", optLogDTO);
        ApplicationEvent event = new SysLogEvent(optLogDTO);

        //发布事件
        applicationContext.publishEvent(event);

        long id = Thread.currentThread().getId();
        System.out.println("发布事件,线程id:" + id);
    }

    private Method resolveMethod(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class<?> targetClass = point.getTarget().getClass();

        Method method = getDeclaredMethod(targetClass, signature.getName(),
                signature.getMethod().getParameterTypes());
        if (method == null) {
            throw new IllegalStateException("无法解析目标方法: " + signature.getMethod().getName());
        }
        return method;
    }

    /**
     * 获取堆栈信息
     */
    public static String getStackTrace(Throwable throwable) {
        StringWriter sw = new StringWriter();
        try (PrintWriter pw = new PrintWriter(sw)) {
            throwable.printStackTrace(pw);
            return sw.toString();
        }
    }

    private Method getDeclaredMethod(Class<?> clazz, String name, Class<?>... parameterTypes) {
        try {
            return clazz.getDeclaredMethod(name, parameterTypes);
        } catch (NoSuchMethodException e) {
            Class<?> superClass = clazz.getSuperclass();
            if (superClass != null) {
                return getDeclaredMethod(superClass, name, parameterTypes);
            }
        }
        return null;
    }

    /**
     * 获取请求参数
     * @param args
     * @param request
     * @return
     */
    private String getArgs(Object[] args, HttpServletRequest request) {
        String strArgs = StrUtil.EMPTY;

        try {
            if (!request.getContentType().contains("multipart/form-data")) {
                strArgs = JSONObject.toJSONString(args);
            }
        } catch (Exception e) {
            try {
                strArgs = Arrays.toString(args);
            } catch (Exception ex) {
                log.warn("解析参数异常", ex);
            }
        }
        return strArgs;
    }
}

注意:指令使用到了IPUtil和RequestHolder工具类,就不具体实现了,可以带代码仓获取代码进行查看

【步骤六】:创建Controller

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private ApplicationContext applicationContext;
    @GetMapping("/getUser")
    public String getUser(){
        return "OK";
    }

    @GetMapping("/name")
    public String getName(String name){
        return "OK";
    }
}

【步骤七】:创建启动类

@SpringBootApplication
@EnableAsync//启用异步处理
public class EventListenerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EventListenerApplication.class,args);
    }
}

3.测试

启动项目并访问Controller可以发现监听器触发了使用postman发送请求:http://localhost:8890/user/name?name="张三"

图片[3]-Spring Event与AOP结合:优雅记录日志的艺术-不念博客

在控制台显示如下信息,也可以自己将日志输出到你想输出的地方,比如mysql

图片[4]-Spring Event与AOP结合:优雅记录日志的艺术-不念博客
https://github.com/bangbangzhou/spring-boot-event-log-demo.git
© 版权声明
THE END