(一)AOP 概要

1. 什么是 AOP ?

AOP是Aspect Oriented Programming的缩写,意为面向切面编程
这是一种通过预编译方式和运行期间动态代理实现程序功能统一维护的技术。
相比传统的面向对象编程(OOP),AOP更关注于将系统中的公共功能(如日志记录、安全控制、事务处理、异常处理等)从业务逻辑中分离出来,形成独立的模块,以便在不影响业务逻辑代码的情况下,对这些公共功能进行集中管理和维护。

2. 为什么要用 AOP?

  1. 降低耦合度:通过将公共功能从业务逻辑中分离出来,AOP可以显著降低系统各模块之间的耦合度,提高系统的可维护性和可扩展性。
  2. 提高代码复用性:公共功能的独立化使得这些功能可以在多个地方重复使用,而无需在每个业务逻辑中都重复编写相同的代码。
  3. 便于集中管理:AOP允许开发者将系统中的公共功能集中管理,便于统一维护和升级。
  4. 提高开发效率:通过使用AOP,开发者可以更加专注于业务逻辑的实现,而无需花费过多时间在公共功能的编写和维护上。

3. AOP一般用来干什么?

  1. 日志记录:在方法调用前后记录日志信息,帮助开发者进行性能分析和故障排查。
  2. 安全控制:在方法调用前进行权限检查,确保只有具有相应权限的用户才能执行该方法。
  3. 事务管理:在方法调用前后管理事务的开启、提交和回滚,确保数据的一致性和完整性。
  4. 异常处理:在方法调用过程中捕获并处理异常,提供友好的错误消息给用户。
  5. 性能监控:对方法调用的性能进行监控和分析,帮助开发者优化系统性能。
  6. 缓存优化:通过缓存方法调用的结果来提高系统性能,减少不必要的计算和资源消耗。

4. AOP 的核心概念

名词概念理解
通知(Advice)拦截到连接点之后所要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类我们要实现的功能,如日志记录,性能统计,安全控制,事务处理,异常处理等等,说明什么时候要干什么
连接点(Joint Point)被拦截到的点,如被拦截的方法、对类成员的访问以及异常处理程序块的执行等等,自身还能嵌套其他的 Joint PointSpring 允许你用通知的地方,方法有关的前前后后(包括抛出异常)
切入点(Pointcut)对连接点进行拦截的定义指定通知到哪个方法,说明在哪干
切面(Aspect)切面类的定义,里面包含了切入点(Pointcut)和通知(Advice)的定义切面就是通知和切入点的结合
目标对象(Target Object)切入点选择的对象,也就是需要被通知的对象;由于 Spring AOP 通过代理模式实现,所以该对象永远是被代理对象业务逻辑本身
织入(Weaving)把切面应用到目标对象从而创建出 AOP 代理对象的过程。织入可以在编译期、类装载期、运行期进行,而 Spring 采用在运行期完成切点定义了哪些连接点会得到通知
引入(Introduction )可以在运行期为类动态添加方法和字段,Spring 允许引入新的接口到所有目标对象引入就是在一个接口/类的基础上引入新的接口增强功能
AOP 代理(AOP Proxy )Spring AOP 可以使用 JDK 动态代理或者 CGLIB 代理,前者基于接口,后者基于类通过代理来对目标对象应用切面

(二)Spring AOP

1. 简述

AOP 是 Spring 框架中的一个核心内容。在 Spring 中,AOP 代理可以用 JDK 动态代理或者 CGLIB 代理 CglibAopProxy 实现。Spring 中 AOP 代理由 Spring 的 IOC 容器负责生成和管理,其依赖关系也由 IOC 容器负责管理。

2. 相关注解

注解说明
@Aspect将一个 java 类定义为切面类
@Pointcut定义一个切入点,可以是一个规则表达式,比如下例中某个 package 下的所有函数,
也可以是一个注解等
@Before在切入点开始处切入内容
@After在切入点结尾处切入内容
@AfterReturning在切入点 return 内容之后处理逻辑
@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
1. 只要开启了这个注解, 其中的point.proceed(); 就不能删除掉,不然会导致所有关于切点的捕获(@Before、@Afte、@AfterReturning、@AfterThrowing)无法被触发
2. point.proceed();出现的异常不能被捕获,就算捕获了也得将其抛出
@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑
@Order(100)AOP 切面执行顺序, @Before 数值越小越先执行,@After 和 @AfterReturning 数值越大越先执行

其中 @Before、@After、@AfterReturning、@Around、@AfterThrowing 都属于通知(Advice)。

3. 执行顺序 (细节)

只要开启了@Around这个注解, 其中的point.proceed(); 就不能删除掉,不然会导致关于切点的捕获(@Before、@Afte、@AfterReturning、@AfterThrowing)无法被触发

  • 正常情况下: @Around -> @Before -> 目标方法 -> @AfterReturning -> @After -> @Around
  • 异常情况下: @Around -> @Before -> 目标方法 -> @AfterThrowing-> @After -> @Around


为什么会存在@Around在前又在后?
原因是被 Object result = proceedingJoinPoint.proceed() 这一段代码所影响的;
这段代码上面输出是在@Before之前,下面的输出是在@After之后

4. @Pointcut 切入点的不同表达式 示例

当然,以下是一些不同类型的切入点表达式(Pointcut Expressions)的示例,这些示例通常用于AOP(面向切面编程)框架中,如Spring AOP。

1. execution 表达式

这是最常用的切入点表达式,用于匹配方法执行的连接点。

// 匹配com.example.service包及其子包中所有类的所有方法
execution(* com.example.service..*.*(..))

// 匹配com.example.service.UserService类中所有的public方法
execution(public * com.example.service.UserService.*(..))

// 匹配所有返回类型为String,且方法名以find开头的public方法
execution(public String com.example..*.find*(..))

2. within 表达式

用于匹配连接点所在的Java类或包。

// 匹配com.example.service包及其子包中所有类的所有方法
within(com.example.service..*)

// 精确匹配com.example.service.UserService类中的所有方法
within(com.example.service.UserService)

注意:within表达式通常用于类型匹配,而不是方法签名匹配。

3. this 和 target 表达式

thistarget表达式用于匹配代理对象或目标对象。它们通常用于基于对象类型的过滤,而不是方法签名。

// 匹配代理对象实现了MyInterface接口的所有连接点
this(com.example.MyInterface)

// 匹配目标对象实现了MyInterface接口的所有连接点
target(com.example.MyInterface)

注意:这些表达式在Spring AOP中可能不直接支持,因为Spring AOP是基于代理的,并且thistarget的区分在JDK动态代理和CGLIB代理中可能有所不同。但在AspectJ等更强大的AOP框架中,这些表达式是支持的。

4. args 表达式

args表达式用于匹配方法参数。

// 匹配所有第一个参数为String类型的方法
args(String, ..)

// 匹配所有参数中包含至少一个String类型的方法
args(.., String, ..)

// 精确匹配第一个参数为特定类型的方法
args(com.example.MyType, ..)

注意:args表达式中的参数类型是按顺序匹配的,但可以使用..来匹配任意数量的额外参数。

5. @annotation、@within、@target 和 @args 表达式

这些表达式基于注解来匹配连接点。

// 匹配所有被@Transactional注解标注的方法
@annotation(org.springframework.transaction.annotation.Transactional)

// 匹配所有在类级别被@Transactional注解标注的类中的方法
@within(org.springframework.transaction.annotation.Transactional)

// 匹配所有目标对象(不是代理对象)被@Service注解标注的类中的方法
@target(org.springframework.stereotype.Service)

// 匹配所有至少有一个参数被@Valid注解标注的方法
@args(javax.validation.Valid, ..)

这些表达式提供了强大的灵活性,允许开发者基于注解来定义切面的应用范围。

请注意,具体的语法和支持程度可能会根据你所使用的AOP框架(如Spring AOP、AspectJ等)而有所不同。上述示例主要基于Spring AOP和AspectJ的通用语法。

(三) AOP如何添加日志记录

1. 引入AOP依赖

在Spring Boot中引入AOP就跟引入其他模块一样,非常简单,只需要在pom.xml中加入如下依赖:

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

2. 自定义注解

由于面向切面的切入点(Pointcut)支持多种写法,
我这边也用了注解形式的写法,因此就自定义了以下这个注解,供后面测试使用

package com.example.reactboot.aop;

import java.lang.annotation.*;

/**
 * 自定义注解类
 */
//Target注解放置的目标位置,TYPE 指可以放在类上面,METHOD 指可以放在方法上面
@Target({ElementType.TYPE, ElementType.METHOD}) 
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档
public @interface  Aoplog {
    String value() default "";
}

3. 切面类 (仅供 讲解示例 使用)

为了把@Before、@After、@AfterReturning、@AfterThrowing、@Around都讲解一下,所以把日志记录的逻辑分别挪到了@Before、@After、@Around中

实际项目中,其实可以只在@Around里面去实现日志记录即可。
下面目录 6.项目上实际用切面类 有提供

package com.example.reactboot.aop;


import com.example.reactboot.bean.UserBean;
import com.google.gson.Gson;
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.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 系统日志:切面处理类
 *
 * @Aspect:声明该类为一个注解类;
 * @Pointcut:定义一个切点,后面跟随一个表达式,表达式可以定义为某个 package 下的方法,也可以是自定义注解等;
 * <p>
 * 切点定义好后,就是围绕这个切点做文章了:
 * @Before: 在切点之前,织入相关代码;
 * @After: 在切点之后,织入相关代码;
 * @AfterReturning: 在切点返回内容后,织入相关代码,一般用于对返回值做些加工处理的场景;
 * @AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理;
 * @Around: 在切入点前后织入代码,并且可以自由的控制何时执行切点;
 */
@Aspect
@Component
@Order(1)
public class WebLogAspect {

    private final static Logger logger = LoggerFactory.getLogger(WebLogAspect2.class);

    /**
     * execution 表达式
     * 可以基于方法的返回类型、包名、类名、方法名以及参数类型等信息来精确地匹配方法.
     * 以下这个例子:匹配了 com.example.reactboot 包及其子包中所有类的所有方法。
     */
    //@Pointcut("execution(* com.example.reactboot.*.*(..))")
    @Pointcut("execution(public * com.example.reactboot.controller..*.*(..))")
    public void webLog() {
    }

    /**
     * @annotation 表达式
     * 用于匹配被指定注解标注的方法。
     * 以下这个例子:匹配了所有被 com.example.reactboot.aop.Aoplog 注解标注的方法。
     */
    @Pointcut("@annotation(com.example.reactboot.aop.Aoplog)")
    public void aopLog() {
    }

    /**
     * @within 表达式
     * 以下这个例子:匹配了所有被 com.example.reactboot.aop.Aoplog 注解标注的类的所有方法。
     */
    @Pointcut("@within(com.example.reactboot.aop.Aoplog)")
    public void aopLogClass() {
    }


    /**
     * 在切点之前织入
     *
     * @param joinPoint
     * @throws Throwable 以下这个例子:使用了execution 的内容
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 开始打印请求日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 打印请求相关参数
        logger.info("===@Before======= Start ==========");
        // 打印请求 url
        logger.info("请求URL: {}", request.getRequestURL().toString());
        // 打印 Http method
        logger.info("请求方法: {}", request.getMethod());
        // 打印调用 controller 的全路径以及执行方法
        logger.info("全路径以及执行方法 Class Method: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        // 打印请求的 IP
        logger.info("请求IP: {}", request.getRemoteAddr());
        // 打印请求入参
        logger.info("入参Request Args: {}", new Gson().toJson(joinPoint.getArgs()));
    }

    /**
     * 切点返回内容后
     *
     * @throws Throwable
     */
    @AfterReturning("webLog()")
    public void afterReturning() {
        logger.info("===@AfterReturning========= 切点返回内容后执行 ==========");
    }


    /**
     * 切点抛出异常后
     *
     * @throws Throwable
     */
    @AfterThrowing("webLog()")
    public void afterThrowing() {
        logger.info("===@AfterThrowing========= 切点抛出异常后执行 ==========");
    }


    /**
     * 在切点之后织入
     *
     * @throws Throwable
     */
    @After("webLog()")
    public void doAfter() throws Throwable {
        logger.info("===@After======= End ==========");
        // 每个请求之间空一行
        logger.info("");
    }

    /**
     * 环绕
     * 环绕执行,就是在调用目标方法之前和调用之后,都会执行一定的逻辑
     *
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //这个代码等价于@Before
        long startTime = System.currentTimeMillis();
        logger.info("===@Around=========proceed之前=========");
        Object result = proceedingJoinPoint.proceed();
        // 打印出参
        logger.info("===@Around=========proceed之后===出参Response Args  : {}", result);
        // 执行耗时
        logger.info("===@Around=========proceed之后===执行耗时 {} ms", System.currentTimeMillis() - startTime);
        return result;
    }
}

4. 测试一(切入点用execution 表达式)

以上的切面处理类,使用的 webLog 方法,用的为execution 表达式

控制层 代码

package com.example.reactboot.controller;

import com.example.reactboot.aop.Aoplog;
import com.example.reactboot.aop.WebLogAspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 单纯的@Controller,请求的资源面向的就是页面
 * 而 @RestController,请求的资源面向的是对象或者字符串
 */
@RestController
public class HelloController {

    private final static Logger logger = LoggerFactory.getLogger(HelloController.class);

    @RequestMapping("/index")
    public String sayHello(String id){
        logger.info("我是index接口"+id);
        return "index"+id;
    }
}

请求该接口后,控制台显示打印如下操作
是有切面日志输出的
在这里插入图片描述

5. 测试二(切入点用自定义注解方式)

先把以上切面类(WebLogAspect )里面的webLog()全部换成 aopLog() 再进行测试
由于我 aopLog() 中使用的是 @annotation ,所以我下面的代码只能把注解放在方法上面
如果你想只在类上面加自定义注解,不想在方法上面一个一个加,那就把 webLog() 全部换成 aopLogClass()

控制层 代码

我新写了一个 xiaoming 的方法,在它上面加上了@Aoplog注解

package com.example.reactboot.controller;

import com.example.reactboot.aop.Aoplog;
import com.example.reactboot.aop.WebLogAspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 单纯的@Controller,请求的资源面向的就是页面
 * 而 @RestController,请求的资源面向的是对象或者字符串
 */
@RestController
public class HelloController {

    private final static Logger logger = LoggerFactory.getLogger(HelloController.class);


    @RequestMapping("/index")
    public String sayHello(String id){
        logger.info("我是index接口"+id);
        return "index"+id;
    }

    @Aoplog(value = "xiaoming")
    @RequestMapping("/xiaoming")
    public String xiaoming(){
        logger.info("我是xiaoming接口");
        return "xiaoming";
    }
}

两个接口都请求后,控制台显示打印如下操作
只有加了@Aoplog注解的才会有切面日志输出
在这里插入图片描述

6. 项目上实际用切面类 (正式用这个哦!!!)

正式项目中,可以直接在@Around中完成日志记录的操作
如果需要落库,就要新建一个实体类,进行存储,在此处我就不创建了

切面类

package com.example.reactboot.aop;


import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

/**
 * 系统日志:切面处理类
 *
 * @Aspect:声明该类为一个注解类;
 * @Pointcut:定义一个切点,后面跟随一个表达式,表达式可以定义为某个 package 下的方法,也可以是自定义注解等;
 * <p>
 * 切点定义好后,就是围绕这个切点做文章了:
 * @Before: 在切点之前,织入相关代码;
 * @After: 在切点之后,织入相关代码;
 * @AfterReturning: 在切点返回内容后,织入相关代码,一般用于对返回值做些加工处理的场景;
 * @AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理;
 * @Around: 在切入点前后织入代码,并且可以自由的控制何时执行切点;
 */
@Aspect
@Component
@Order(1)
public class WebLogAspect {

    private final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    /**
     * execution 表达式
     * 可以基于方法的返回类型、包名、类名、方法名以及参数类型等信息来精确地匹配方法.
     * 以下这个例子:匹配了 com.example.reactboot 包及其子包中所有类的所有方法。
     */
    //@Pointcut("execution(* com.example.reactboot.*.*(..))")
    @Pointcut("execution(public * com.example.reactboot.controller..*.*(..))")
    public void webLog() {
    }

    /**
     * @annotation 表达式
     * 用于匹配被指定注解标注的方法。
     * 以下这个例子:匹配了所有被 com.example.reactboot.aop.Aoplog 注解标注的方法。
     */
    @Pointcut("@annotation(com.example.reactboot.aop.Aoplog)")
    public void aopLog() {
    }

    /**
     * @within 表达式
     * 以下这个例子:匹配了所有被 com.example.reactboot.aop.Aoplog 注解标注的类的所有方法。
     */
    @Pointcut("@within(com.example.reactboot.aop.Aoplog)")
    public void aopLogClass() {
    }

    /**
     * 切点之前
     *
     * @param joinPoint
     * @throws Throwable
     */
    @Before("webLog()")
    public void before(JoinPoint joinPoint) {
        logger.info("============ 切点之前(@Before)==========");
    }

    /**
     * 切点之后
     *
     * @throws Throwable
     */
    @After("webLog()")
    public void after() {
        logger.info("============ 切点后执行(@After) ==========");
    }

    /**
     * 切点返回内容后
     *
     * @throws Throwable
     */
    @AfterReturning("webLog()")
    public void afterReturning() {
        logger.info("============ 切点返回内容后执行(@AfterReturning) ==========");
    }

    /**
     * 切点抛出异常后
     *
     * @throws Throwable
     */
    @AfterThrowing("webLog()")
    public void afterThrowing() {
        logger.info("============ 切点抛出异常后执行(@AfterThrowing) ==========");
    }

    /**
     * 环绕
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        logger.info("============ 环绕(@Around) ==========");
        return logAround(joinPoint);
    }

    private Object logAround(ProceedingJoinPoint point) throws Throwable {
        //开始时间
        long beginTime = System.currentTimeMillis();
        Object result = null;
        Exception exception = null;
        try {
            //执行目标方法
            result = point.proceed();
        } catch (Exception exp) {
            exception = exp;
        }
        //目标方法完成时间(耗时ms)
        long time = System.currentTimeMillis() - beginTime;
        //业务记录方法
        saveLog(point, result, exception, time);
        //异常必须抛出去
        if (exception != null) {
            throw exception;
        }
        return result;
    }

    /**
     * @param joinPoint
     * @param result    目标方法返回结果
     * @param exception 目标方法返回异常
     * @param time      目标方法完成时间
     */
    private void saveLog(ProceedingJoinPoint joinPoint, Object result, Exception exception, long time) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        logger.info("============ 环绕(@Around) ==proceed之后========");

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取方法所在的类的完全限定名
        String className = signature.getDeclaringTypeName();
        // 获取方法名
        String methodName = signature.getName();
        //接口完整名称
        String interfaceName = className + "#" + methodName;
        //当前时间
        Date nowDate = new Date();

       
        //获取被调用的类 Class
        Class<?> aClass =  joinPoint.getTarget().getClass();
        Aoplog aoplog1 = aClass.getAnnotation(Aoplog.class);
        if(aoplog1!=null){
            String value1 = aoplog1.value();
            logger.info("@Aoplog注解放在类上面的值: {}", value1);
        }

        //获取被调用的方法 Method
        //方法一:
        //Method method = resolveMethod(joinPoint);
        //方法二:
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //获取请求入参
        Object parameter = getParameter(method, joinPoint.getArgs());

        //获取自定义注解的值
        Aoplog aoplog = method.getAnnotation(Aoplog.class);
        if(aoplog!=null){
            String value2 = aoplog.value();
            logger.info("@Aoplog注解放在方法上面的值: {}", value2);
        }
        
        //日志相关信息
        //logger.info("请求IP: {}", request.getRemoteAddr());
        logger.info("请求IP: {}", getIpAddress(request));
        logger.info("请求URL: {}", request.getRequestURL().toString());
        logger.info("请求方法: {}", request.getMethod());
        logger.info("访问时间 : {}", nowDate);
        logger.info("具体类名 : {}", className);
        logger.info("方法名 : {}", methodName);
        logger.info("接口完整名称 : {}", interfaceName);
        logger.info("请求入参 : {}", parameter);
        logger.info("请求耗时 : {} ms", time);
        if (exception != null) {
            logger.info("存在异常 : {} ", exception.getMessage());
        }
        //可以把上面的信息用实体类存储,然后进行落库操作
        //xxx.save(dto);
    }


    /**
     * 根据方法和传入的参数获取请求参数
     * @param method
     * @param args
     * @return
     */
    private Object getParameter(Method method, Object[] args) {
        List<Object> argList = new ArrayList<>();
        Parameter[] parameters = method.getParameters();
        Map<String, Object> map = new HashMap<>();
        for (int i = 0; i < parameters.length; i++) {
            //将RequestBody注解修饰的参数作为请求参数
            RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
            //将RequestParam注解修饰的参数作为请求参数
            RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
            String key = parameters[i].getName();
            if (requestBody != null) {
                argList.add(args[i]);
            } else if (requestParam != null) {
                map.put(key, args[i]);
            } else {
                map.put(key, args[i]);
            }
        }
        if (map.size() > 0) {
            argList.add(map);
        }
        if (argList.size() == 0) {
            return null;
        } else if (argList.size() == 1) {
            return argList.get(0);
        } else {
            return argList;
        }
    }

    /**
     * 获取request中的IP
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }



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

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

}

请求 http://localhost:9021/addUser 的效果如下
在这里插入图片描述

(四)AOP如何修改入参、返参

主要是用到环绕@Around

Controller层

@RestController
public class LoginController {

    @RequestMapping(value = "/loginIn")
    public UserBean login(String name,int age){
        System.out.println("system,login="+name);
        System.out.println(age);
        UserBean userBean = new UserBean(name,age);
        System.out.println(userBean);
        if(userBean!=null){
            return userBean;
        }else {
            return null;
        }
    }
}

切面类的@Around

    /**
     * 环绕
     * 环绕执行,就是在调用目标方法之前和调用之后,都会执行一定的逻辑
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        
        long startTime = System.currentTimeMillis();
        logger.info("===@Around=========proceed之前=========");
        
        //获取目标方法的入参
        Object[] args = proceedingJoinPoint.getArgs(); 
        for (int i = 0; i < args.length; i++) {
            logger.info("argsName: "+args[i]); //输出目标方法的参数
            if(i==0){
                args[i]="无语123";
            }
        }
        // 这个proceed就需要把入参设置进去
        Object result = proceedingJoinPoint.proceed(args);
        
         //根据原方法返回值的类型,进行修改
        if (result instanceof String) {
            result = "我把你的值给改了,哈哈哈哈"; 
        }
        if (result instanceof UserBean) {
            UserBean entity = (UserBean) result;
            entity.setAddress("我把你的值给改了,哈哈哈哈");
            result = entity; 
        }
        // 打印出参
        logger.info("===@Around=========proceed之后===出参Response Args  : {}", result);
        // 执行耗时
        logger.info("===@Around=========proceed之后===执行耗时 {} ms", System.currentTimeMillis() - startTime);
        return result;
    }

接口请求为http://localhost:9021/loginIn?name=项目&age=22

返回结果为
可以看到 name 入参被改了,同时返回值的address被加上了值
在这里插入图片描述

参考文章
【1】Spring Boot AOP 切面统一打印请求与响应日志
【2】Spring Boot 2.X(八):Spring AOP 实现简单的日志切面
【3】在IDEA 、springboot中使用切面aop实现日志信息的记录到数据库
【4】springboot项目使用切面记录用户操作日志
【5】Spring Boot中使用AOP统一处理Web请求日志

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐