SpringBoot AspectJ做AOP日志管理

AspectJ原理:

  • AspectJ是一个代码生成工具(Code Generator)。
  • AspectJ语法就是用来定义代码生成规则的语法。您如果使用过Java Compiler Compiler (JavaCC),您会发现,两者的代码生成规则的理念惊人相似。
  • AspectJ有自己的语法编译工具,编译的结果是Java Class文件,运行的时候,classpath需要包含AspectJ的一个jar文件(Runtime lib)。

1:引入maven依赖

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

2:Service层(被切面的业务逻辑层)
这里简单写一个业务逻辑,内容不是重点,重点是怎么切面。
BusinessService:

@Service
public class BusinessService {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public User getIpInfo(HttpServletRequest request, int type) {
        logger.info("进入getIpInfo方法。");
        User user = new User();
        user.setName(request.getContextPath());
        user.setId(request.getContentLengthLong());
        return user;
    }

    public Integer getTestInfo(int type) {
        logger.info("进入getTestInfo方法。");
        return type;
    }
}

Aspect切面
有了上面的Service,可以直接使用@Component@Aspect来写切面逻辑。
OperateLogAspect:

package com.springboot.study.aspjectj;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * @Author: guodong
 * @Date: 2021/6/29 16:13
 * @Version: 1.0
 * @Description:
 */
@Component
@Aspect
public class OperateLogAspect {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Pointcut("execution(* com.springboot.study.service.BusinessService.*(..)) && !execution(* com.springboot.study.service.BusinessService.getTest*(..)) ")
    private void log() {
    }

    @Before("log()")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        joinPoint.getArgs();
        logger.info("方法为:" + methodName + ", 这是一个前置测试 ");
    }

    @After("execution(* com.springboot.study.service.BusinessService.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("方法为:" + methodName + ", 这是一个后置测试 ");
    }

    @Around("execution(* com.springboot.study.service.BusinessService.getTestInfo(..))")
    public void test(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        logger.info("方法为:" + methodName + ", 测水水水水水水水水水水 ");
    }

    @AfterReturning(value = "log()", returning = "result")
    public void afterReturning(JoinPoint point, Object result) {
        String methodName = point.getSignature().getName();
        logger.info("方法为:" + methodName + ",目标方法执行结果为:" + result);
    }

    @Around("log()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        logger.info("请求参数:{}", joinPoint.getArgs());
        Object[] arr = joinPoint.getArgs();
        Integer type = (Integer) arr[1];
        logger.info("请求类型:{}", type);
        long startTime = System.currentTimeMillis();
        Object obj = joinPoint.proceed();
        long timeTaken = System.currentTimeMillis() - startTime;
        logger.info("执行时间:{}", timeTaken);
        return obj;
    }
}

参数详解如下:

  • @Pointcut可以定义切点,定义之后,其他地方使用同样的切点就不需要再写execution,直接写切点的方法名即可。
  • @Before是在被切方法调用前执行,可以获取到被切方法的参数。
  • @After是在被切方法执行后执行,无法获取到被切方法的返回值。
  • @AfterReturning是被被切方法执行后执行,可以获取到被切方法的返回值。
  • @Around是环绕被切方法,记住需要调用joinPoint.proceed()执行被切方法,如果有返回值,注意return 返回值。
  • @AfterThrowing: 异常通知, 在方法抛出异常之后执行。
  • 如果切面方法外有事务,切面执行的方法也是会回滚的。

运行结果
测试中,我调用BusinessService的getIpInfo方法。
切面测试结果:

2021-06-29 16:17:07.450  INFO 13176 --- [nio-8080-exec-1] c.s.study.aspjectj.OperateLogAspect      : 请求参数:org.apache.catalina.connector.RequestFacade@71268e04
2021-06-29 16:17:07.452  INFO 13176 --- [nio-8080-exec-1] c.s.study.aspjectj.OperateLogAspect      : 请求类型:0
2021-06-29 16:17:07.454  INFO 13176 --- [nio-8080-exec-1] c.s.study.aspjectj.OperateLogAspect      : 方法为:getIpInfo, 这是一个前置测试 
2021-06-29 16:17:07.462  INFO 13176 --- [nio-8080-exec-1] c.s.study.service.BusinessService        : 进入getIpInfo方法。
2021-06-29 16:17:07.463  INFO 13176 --- [nio-8080-exec-1] c.s.study.aspjectj.OperateLogAspect      : 执行时间:11
2021-06-29 16:17:07.463  INFO 13176 --- [nio-8080-exec-1] c.s.study.aspjectj.OperateLogAspect      : 方法为:getIpInfo, 这是一个后置测试 
2021-06-29 16:17:07.463  INFO 13176 --- [nio-8080-exec-1] c.s.study.aspjectj.OperateLogAspect      : 方法为:getIpInfo,目标方法执行结果为:User(a=0, id=-1, name=, age=null, result=null)

切面表达式浅谈
execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?) 除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。
?表示可省略,
修饰符模式如public、protected等;
返回类型模式表示方法返回类型;
方法名模式表示类+方法;
参数模式表示参数;
异常模式表示抛出的异常;
表达式中,可以使用*来代表任意字符,用..来表示任意个参数。
示例:

execution(* com.cff.springbootwork.aspectj.service.BusinessService.*(..))

  • 切面表达式是从execution开始的,()内是表达式主体。
  • 第一个号:表示返回类型,号表示所有的类型。
  • com.cff.springbootwork.aspectj.service.BusinessService.*表示匹配该类的所有方法。
  • (..)表示参数个数任意。
  • 多个execution可以合并,可以使用||、&&等进行连接。

最后补充一点小知识:
AspectJ支持5种类型的通知注解
1)@Before:前置通知:在方法执行之前执行的通知。
2)@After:后置通知,在方法执行之后执行,即方法返回结果或者抛出异常的时候, 下面的后置通知记录了方法的终止。
3)@AfterRunning:返回通知,在方法返回结果之后执行。ps:无论方法是正常返回还是抛出异常,后置通知都会执行。如果只想在方法返回的时候记录日志,应使用返回通知代替后置通知。
4)@AfterThrowing:异常通知,在方法抛出异常之后。
5)@Around:环绕通知,围绕着方法执行(即方法前后都有执行)。环绕通知是所有通知类型中功能最为强大的, 能够全面地控制连接点,甚至可以控制是否执行连接点。

参考博客:
https://blog.csdn.net/zhao9tian/article/details/37762389
https://blog.csdn.net/u014338530/article/details/88778158
https://cloud.tencent.com/developer/article/1690944

郭慕荣博客园
原文地址:https://www.cnblogs.com/jelly12345/p/14950731.html