spring boot—使用AOP

本文深入探讨了Spring框架中的AOP(面向切面编程)功能,解释了其如何通过定义横向切入点,实现在不破坏原有逻辑的情况下拓展应用程序功能,如事务管理、日志记录等。介绍了AOP的关键注解,如@Aspect、@Pointcut、@Before等,并通过实例展示了如何在控制器中使用AOP记录请求日志和统计执行时间。

AOP简介

AOP是Spring提供的两个核心功能之一:IOC(控制反转),AOP(Aspect Oriented Programming 面向切面编程);IOC有助于应用对象之间的解耦,AOP可以实现横切关注点和它所影响的对象之间的解耦;

AOP,它通过对既有的程序定义一个横向切入点,然后在其前后切入不同的执行内容,来拓展应用程序的功能,常见的用法如:打开事务和关闭事物,记录日志,统计接口时间等。AOP不会破坏原有的程序逻辑,拓展出的功能和原有程序是完全解耦的,因此,它可以很好的对业务逻辑的各个部分进行隔离,从而使业务逻辑的各个部分之间的耦合度大大降低,提高了部分程序的复用性和灵活性。

AOP详解

引入POM.xml

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

AOP几个注解

  • @Aspect,此注解将一个类定义为一个切面类;
  • @Pointcut,此注解可以定义一个切入点,可以是规则表达式,也可以是某个package下的所有函数,也可以是一个注解等,其实就是执行条件,满足此条件的就切入;

然后可以定义切入位置,我们可以选择在切入点的不同位置进行切入:

  • @Before在切入点开始处切入内容;
  • @After在切入点结尾处切入内容;
  • @AfterReturning在切入点return内容之后切入内容(可以用来对返回值做一些处理);
  • @Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容;
  • @AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑;

实现切面

我们定义一个切面,然后定义一个切入点,切入点这个方法,不需要方法体,返回值是void,在切入点后定义一下切入条件,当满足条件时,就会在切入点的指定位置织入我们自定义的内容。

package com.blog.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;


@Aspect
@Component
public class LogAspect {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**记录方法调用开始时间*/
    ThreadLocal<Long> startTime = new ThreadLocal<>();

    @Pointcut("execution(* com.blog.controller.*.*(..))")
    public void log(){};

    @Before("log()")
    public void doBefore(JoinPoint joinPoint){
        //记录调用开始时间
        startTime.set(System.currentTimeMillis());
        ServletRequestAttributes servlet = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = servlet.getRequest();
        String url = request.getRequestURL().toString();
        String ip = request.getRemoteAddr();
        String classMethod = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        ResultLog resultLog = new ResultLog(url,ip,classMethod,args);
        logger.info("Request : {}",resultLog);
    }

    @AfterReturning(returning = "result",pointcut = "log()")
    public void doAfterReturn(Object result){
        logger.info("耗时:"+(System.currentTimeMillis()-startTime.get())/1000+"s");
        logger.info("Result : {}",result);
    }

    private class ResultLog{
        private String url;
        private String ip;
        private String classMethod;
        private Object[] object;

        public ResultLog(String url, String ip, String classMethod, Object[] object) {
            this.url = url;
            this.ip = ip;
            this.classMethod = classMethod;
            this.object = object;
        }

        @Override
        public String toString() {
            return "ResultLog{" +
                    "url='" + url + '\'' +
                    ", ip='" + ip + '\'' +
                    ", classMethod='" + classMethod + '\'' +
                    ", object=" + Arrays.toString(object) +
                    '}';
        }
    }
}

AOP切面优先级

当我们对web层做多个切面时,会有一个问题:究竟先去切入谁?这里引入一个优先级的问题。处理方法非常简单,我们定义切面时,使用一个注解@Order(i),这个i决定着优先级的高低。

比如,现在定义了两个切面,一个@Order(3),一个@Order(8),那么执行时:

  • 在切入点前的操作,按order的值由小到大执行,即:先@Order(3),后@Order(8);
  • 在切入点后的操作,按order的值由大到小执行,即:先@Order(8),后@Order(3);
     
<think>为了在Spring Boot使用AOP打印指定目录下的请求接口和参数,我们可以通过以下步骤实现: 1. 添加Spring AOP依赖。 2. 创建一个切面类,定义切点表达式来匹配指定目录下的方法。 3. 在切面类中编写环绕通知或前通知来打印请求的接口和参数。 假设我们想要打印`com.example.controller`包(包括子包)下的所有public方法的请求参数。 步骤如下: ### 步骤1:添加依赖 在`pom.xml`中添加Spring Boot AOP依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` ### 步骤2:创建切面类 创建一个切面类,使用`@Around`或`@Before`通知来记录方法调用信息。这里我们使用`@Around`以便于获取方法执行的结果,但主要目的是打印请求参数,所以也可以使用`@Before`。这里我们使用`@Around`。 ```java package com.example.aspect; 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.util.Arrays; @Aspect @Component public class LoggingAspect { // 定义切点:com.example.controller包及其子包下的所有public方法 @Around("execution(public * com.example.controller..*.*(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { // 获取方法签名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 获取类名和方法名 String className = signature.getDeclaringType().getSimpleName(); String methodName = signature.getName(); // 获取参数 Object[] args = joinPoint.getArgs(); // 打印请求信息 System.out.println("请求接口: " + className + "." + methodName); System.out.println("请求参数: " + Arrays.toString(args)); // 执行方法 Object result = joinPoint.proceed(); // 这里可以打印返回结果,但根据题目要求,只打印请求接口和参数 // System.out.println("返回结果: " + result); return result; } } ``` ### 解释: - `@Aspect`:标记该类为切面类。 - `@Component`:将该类纳入Spring容器管理。 - `@Around`:环绕通知,可以在方法执行前后执行自定义逻辑。 - 切点表达式:`execution(public * com.example.controller..*.*(..))` 解释: - `public`:匹配public方法。 - `*`:返回类型任意。 - `com.example.controller..*`:匹配`com.example.controller`包及其子包下的任意类。 - `.*(..)`:任意方法,任意参数。 ### 注意事项: 1. 如果参数中包含不可序列化的对象(如HttpServletRequest),直接打印可能会出现问题。我们可以根据实际需要调整打印方式,例如对于特定类型的参数可以跳过或特殊处理。 2. 在实际生产环境中,建议使用日志框架(如SLF4J)代替`System.out.println`。 ### 改进:使用日志框架 通常,我们会使用日志框架(如SLF4J)来记录日志。修改后的代码如下: ```java package com.example.aspect; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.Arrays; @Aspect @Component public class LoggingAspect { private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class); @Around("execution(public * com.example.controller..*.*(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String className = signature.getDeclaringType().getSimpleName(); String methodName = signature.getName(); Object[] args = joinPoint.getArgs(); logger.info("请求接口: {}.{}", className, methodName); logger.info("请求参数: {}", Arrays.toString(args)); return joinPoint.proceed(); } } ``` ### 测试 启动Spring Boot应用,访问`com.example.controller`包下的控制器方法,即可在控制台看到日志输出。 ### 相关问题 根据要求,最后提出四个相关问题: §§相关问题§§: 1. 如何在Spring Boot中配AOP的切点表达式? 2. Spring AOP中的通知类型有哪些?分别用于什么场景? 3. 如何避免在打印日志时,因为参数中包含大对象(如HttpServletRequest)而导致的性能问题? 4. 在Spring AOP中,如何获取被拦截方法的返回值?</think>在Spring Boot使用AOP打印指定目录下的请求接口和参数,可以通过以下步骤实现: ### 解决方案代码 ```java 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.util.Arrays; @Aspect @Component public class ControllerLogAspect { // 定义切点:扫描com.example.controller包及其子包下的所有方法 @Around("execution(* com.example.controller..*.*(..))") public Object logControllerMethods(ProceedingJoinPoint joinPoint) throws Throwable { // 获取方法签名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 构建日志信息 String logMsg = String.format("\n===== 请求接口信息 =====\n" + "类名: %s\n" + "方法: %s\n" + "参数: %s\n" + "=======================", signature.getDeclaringTypeName(), signature.getName(), Arrays.toString(joinPoint.getArgs())); // 打印日志 System.out.println(logMsg); // 执行原方法并返回结果 return joinPoint.proceed(); } } ``` ### 详细解释 1. **AOP原理**: - Spring AOP通过动态代理在目标方法前后插入横切逻辑 - `@Around` 注解实现环绕通知,可以控制方法执行前后 2. **切点表达式**: - `execution(* com.example.controller..*.*(..))` 含义: - `*`:任意返回类型 - `com.example.controller..*`:指定包及其所有子包 - `.*(..)`:任意方法名,任意参数 3. **关键对象**: - `ProceedingJoinPoint`:提供访问目标方法的能力 - `MethodSignature`:获取方法签名信息 - `joinPoint.getArgs()`:获取方法参数数组 4. **执行流程**: 1. 当目标方法被调用时触发切面 2. 构建包含类名、方法名和参数的日志信息 3. 打印日志后通过`proceed()`执行原始方法 4. 返回原始方法的执行结果 ### 配步骤 1. **添加依赖**(pom.xml): ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` 2. **包结构要求**: - 确保切面类在能被Spring扫描的包下 - 建议将切面类放在单独的`aspect`包中 3. **日志输出示例**: ``` ===== 请求接口信息 ===== 类名: com.example.controller.UserController 方法: getUserById 参数: [123] ======================= ``` ### 注意事项 1. **避免打印敏感参数**:实际生产中需过滤密码等敏感信息 2. **性能影响**:大量参数序列化可能影响性能,建议生产环境添加条件开关 3. **JSON格式化**:如需美化输出,可引入Jackson: ```java import com.fasterxml.jackson.databind.ObjectMapper; // 在方法内添加 ObjectMapper mapper = new ObjectMapper(); String jsonArgs = mapper.writerWithDefaultPrettyPrinter() .writeValueAsString(joinPoint.getArgs()); ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值