锋盈数科-知识库 Logo
首页
软件开发
计算机基础
Hello Halo
新手必读
关于本知识库
登录 →
锋盈数科-知识库 Logo
首页 软件开发 计算机基础 Hello Halo 新手必读 关于本知识库
登录
  1. 首页
  2. 软件开发
  3. AOP(面向切面编程)

AOP(面向切面编程)

0
  • 软件开发
  • 发布于 2024-09-20
  • 0 次阅读
黄健
黄健

如果将OOP(面向对象编程)看作是一层一层的模块,则AOP(面向切面编程)则是贯穿每一层的不同功能模块(每一层都涉及了这些功能模块),所以才叫作面向切面编程。

1.为什么需要 AOP?

日志记录、事务管理、性能监控等功能通常分散在多个类中 ,导致代码重复、难以维护。这些功能模块也被叫做横切关注点,而 AOP 允许我们将这些关注点集中到一个地方(切面),就相当于包装在一起,并自动织入到相关的业务逻辑中,使得代码更加简洁、可维护。

2.AOP 的核心概念

2.1 横切关注点

所谓"横切关注点”,就是那些跟核心业务逻辑没直接关系,但又贯穿多个模块的功能。(将这些功能被称作横切关注点)。

常见的横切关注点有如下这些:

  • 日志记录:可以自动在每个方法调用前后记录日志,节省手动添加日志的工作。
  • 事务管理:可以在数据库操作时,自动开启、提交或回滚事务。
  • 性能监控:可以在方法执行前后记录时间,监控方法的执行性能。

2.2 切面

切面是横切关注点的模块化实现。它可以包含多个”通知”(Advice) 和“切入点”(Pointcut),定义了在哪里以及如何实现横切逻辑。

具体例子

假设你有一个 UserService 类,专门处理用户注册、更新等操作。每次用户注册前,你想记录日志,表示"正在注册用户”。

不使用 AOP 的写法:

public class UserService {
    public void registerUser(User user) {
        // 日志记录
        System.out.println("正在注册用户...");
        // 核心业务逻辑
        // 注册用户
    }
}

使用 AOP 的写法:

@Service
public class UserService {
    public void registerUser(User user) {
        // 核心业务逻辑
        // 注册用户
    }
}

@Aspect
@Component
public class LoggingAspect {
    // 定义一个切入点,匹配 UserService 的所有方法
    @Pointcut("execution(* com.example.UserService.*(..))")
    public void userServiceMethods() {}

    // 前置通知:在方法执行前记录日志
    @Before("userServiceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("正在注册用户...");
    }
}

在这个例子里,LoggingAspect 就是一个切面,它的前置通知在 UserService 的方法执行前自动插入日志记录逻辑。这样,你的业务代码就干净了,不再需要手动写日志逻辑。

2.3 通知

通知定义了切面的具体行为,也就是在何时、何地、以何种方式对目标方法进行增强。根据增强发生的时机,通知分为:

  • 前置通知(Before Advice):在方法调用前执行。
  • 后置通知(After Advice):在方法调用后执行。
  • 返回通知(After Returning Advice):在方法成功返回后执行。
  • 异常通知(After Throwing Advice):在方法抛出异常后执行。
  • 环绕通知(Around Advice):在方法执行的前后都执行,可以完全控制方法的执行过程。

2.4 切入点

在 Spring AOP 中,切入点(Pointcut)是用来定义你想要拦截哪些方法。

  • 切入点决定了拦截点,即哪些方法会被增强(例如,哪些方法会被日志记录或事务管理等功能拦截)。
  • 配合"通知”(Advice)一起工作,通知决定了在方法执行前、执行后或抛出异常时执行什么逻辑。
  • 切入点:选择哪些方法需要"被拦截”。
  • 通知:指定拦截后要做的事情。

2.5 连接点

连接点就是你业务逻辑层中可以插入横切关注点的位置(简单来说就是业务逻辑层中的方法),因为横切关注点的这些功能是需要在你调用业务逻辑层的方法伴随着进行。程序执行当中的一个方法调用、一个异常抛出都可以作为连接点。

2.6 织入

织入是将切面与目标对象(业务代码)结合的过程。在 Spring AOP 中,织入通常发生在运行时,通过动态代理来完成。代理对象在调用目标方法之前,会先执行切面中定义的通知逻辑(如记录日志),然后再执行实际的业务逻辑 。

3.AOP的具体实现

3.1 切面的定义

切面(Aspect) 是包含横切关注点逻辑的模块。它主要用于定义在哪些地方(切入点)应用哪些操作(通知)。在 Spring AOP 中,切面使用 @Aspect 注解来标记,并作为 Spring 管理的 Bean。

示例:

@Aspect
@Component
public class LoggingAspect {

    // 这是切入点和通知的具体实现
}
  • @Aspect:用来定义一个切面类。
  • @Component:将这个切面类交给 Spring 容器管理。

关键点:

  • 切面类一般使用 @Component 或者通过 XML 方式配置成 Spring Bean,这样 Spring 才能将其织入到业务逻辑中。
  • @Aspect 注解表明这是一个切面类,包含增强逻辑的通知(Advice)和定义切入点(Pointcut)。

3.2 切入点的定义

通过@Pointcut注解定义在哪些方法、类或特定条件下,AOP 的增强逻辑(即通知)应该被应用。

举例:

@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}

“execution(* com.example.service.UserService.*(..))” 是切入点表达式,用这个表达式规定了通知的使用范围(某些方法或某些类)

后面所跟着的public void userServiceMethods() {},则是这个你用切入点表达式规定的范围的标识符,可以把它看作是对切入点表达式的"命名”,从而在多个地方复用这个切入点表达式。

举例:

@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}

// 复用切入点
@Before("userServiceMethods()")
public void logBefore(JoinPoint joinPoint) {
    System.out.println("执行方法之前:" + joinPoint.getSignature().getName());
}

在以上例子中,将"execution(* com.example.service.UserService.*(..))“这个范围命名为userServiceMethods(),则你在通知复用切入点的时候就可以直接用"userServiceMethods()“来代表 “execution(* com.example.service.UserService.*(..))“这复杂的一串了

3.3 切入点表达式的定义规则

关于注解以外的部分进行标记

3.3.1 execution 表达式

execution() 是 Spring AOP 中最常用的切入点表达式,用来匹配方法的执行。它可以精确地定义目标方法的签名,包括方法的访问修饰符、返回类型、类名、方法名和参数。

语法格式:

execution( [修饰符模式] 返回类型 [类全路径] . 方法名(参数列表) [异常模式] )

  • 修饰符模式(可选) :匹配方法的访问修饰符,如 public、private。如果不指定,则匹配所有修饰符。
  • 返回类型 :匹配方法的返回类型,可以是具体的类型(如 String)或通配符 *(表示任意返回类型)。
  • 类全路径(可选) :可以是类的全路径(如 com.example.UserService),也可以是通配符 (如 com.example..* 表示 com.example 包及其子包的所有类)。
  • 方法名 :匹配具体的方法名或使用通配符 *(表示任意方法)。
  • 参数列表 :匹配方法的参数,可以是具体的类型(如 (int, String)),也可以是通配符 ..(表示任意类型和数量的参数)。
  • 异常模式(可选):匹配抛出的异常类型。

示例:

  • 匹配 com.example.UserService 类中的所有方法,不论返回类型和参数是什么:

    execution(* com.example.UserService.*(..))

  • 匹配 com.example.UserService 中 public 修饰的所有方法:

    execution(public * com.example.UserService.*(..))

  • 匹配 com.example.UserService 中所有返回类型为 String 的方法:

    execution(String com.example.UserService.*(..))

  • 匹配 com.example.UserService 中所有带有两个参数的方法(参数类型为 int 和 String):

    execution(* com.example.UserService.*(int, String))

  • 匹配 com.example.UserService 中所有无参数的方法:

    execution(* com.example.UserService.*())

  • 匹配 com.example 包及其子包中的所有类的所有方法:

    execution(* com.example..*.*(..))

3.3.2 within 表达式

within() 用来匹配特定类或包中的所有方法,常用于指定切入点的作用范围(类或包)。

语法格式:

within(类全路径)

  • 类全路径 :可以是类的全路径(如 com.example.UserService),也可以是通配符 (如 com.example..* 表示 com.example 包及其子包的所有类)。

示例:

  • 匹配 com.example.UserService 类中的所有方法:

    within(com.example.UserService)

  • 匹配 com.example.service 包中的所有类的所有方法:

    within(com.example.service..*)

within() 和 execution() 都可以匹配类中的方法,不同之处在于:

  • execution() 可以精确匹配方法签名;
  • within() 只能匹配类或包中的所有方法,不能细粒度地匹配特定方法。

3.3.3 args 表达式

args() 用来匹配方法的参数类型。它与 execution() 的参数部分类似,但 args() 可以在运行时获取参数的实际类型,而不是编译时确定的类型。

语法格式:

args(参数类型列表)

  • 参数类型列表 :匹配方法的参数,可以是具体的类型(如 (int, String)),也可以是通配符 ..(表示任意类型和数量的参数)。

示例:

  • 匹配方法的第一个参数为 String,且可以有任意数量的其他参数:

    args(String, ..)

  • 匹配所有参数为 String 的方法:

    args(String)

  • 匹配所有方法中至少有一个参数是 String 类型的情况:

    args(.., String, ..)

3.3.4 this 表达式

this() 用来匹配当前代理对象的类型。这个表达式常用于匹配代理类,而不是目标类的类型。

语法格式:

this(类型)

  • 类型:代理对象的类型,可以是类或接口。

示例:

  • 匹配所有代理对象实现了 UserService 接口的方法: this(com.example.service.UserService)

this() 使用的类型是代理对象的类型,而 target() 表达式使用的是目标对象的类型。

3.3.5 代理对象

关于this()中所用到的代理对象,这里需要作出说明:

1.当某个类没有实现任何接口就是个普通类,或者该类就是接口类的时候,这个类的代理对象就是自身。

2.当某个类实现了某个接口的时候,这个类的代理对象就是该接口类

举例:

假设我们有以下类和接口:

public interface UserService {
    void registerUser(User user);
}

@Service
public class UserServiceImpl implements UserService {
    public void registerUser(User user) {
        System.out.println("用户注册:" + user.getName());
    }
}

并且我们有一个切面:

@Aspect
@Component
public class LoggingAspect {

    @Pointcut("this(com.example.service.UserService)")
    public void proxyBasedMethods() {}

    @Before("proxyBasedMethods()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("调用方法前:" + joinPoint.getSignature().getName());
    }
}

调用过程:

1.通过接口 UserService 调用:

@Autowired
private UserService userService;

userService.registerUser(new User("Alice"));

因为this()中的是com.example.service.UserService这个接口类,所以如果我们想使用增强逻辑(通知),就需要让某个依赖注入对象的代理对象为UserService这个接口类才可以。

这里进行依赖注入的时候,因为UserService本身就是个接口类,所以其代理对象还是其本身UserService,故这个地方在切入点的范围内。

2.直接通过 UserServiceImpl 调用

@Autowired
private UserServiceImpl userServiceImpl;

userServiceImpl.registerUser(new User("Alice"));

UserServiceImpl实现了UserService接口类,所以 UserServiceImpl的代理对象还是UserService接口类,故也在切入点定义的范围内。

3.3.6 target 表达式

target() 用来匹配目标对象的类型。它和 this() 类似,但 target() 匹配的是目标对象的类型,而不是代理对象。

当你通过接口类型(例如 UserService)进行依赖注入时,Spring 会根据接口的实现类自动找到并注入相应的目标对象。

目标对象的匹配刚好和代理对象反过来,代理对象的匹配是遇到接口的实现类,回头去找接口类,而目标对象的匹配则是遇到接口类,去找该接口类的实现类。

语法格式:

target(类型)

  • 类型:目标对象的类型,可以是类或接口。

示例:

  • 匹配所有目标对象实现了 UserService 接口的方法:

    target(com.example.service.UserService)

  • 匹配目标对象是某个具体类的所有方法:

    target(com.example.service.MyConcreteClass)

  • this() 通常填写接口类型,因为代理对象的类型是接口类型(尤其是在使用 JDK 动态代理时)。
  • target() 通常填写实现类类型,因为目标对象的实际类型是实现类,而不管代理对象是什么类型。
  • 而如果是普通类的话,目标对象和代理对象都可以看作自身。

关于注解进行标记

3.3.7 @annotation 表达式

@annotation() 用来匹配标注了特定注解的方法 。常用于筛选被某些注解标记的方法。

语法格式:

@annotation(注解类型)

  • 注解类型:指定注解的全路径。

示例:

  • 匹配所有标注了 @Transactional 注解的方法:

    @annotation(org.springframework.transaction.annotation.Transactional)

  • 匹配标注了自定义注解 @MyCustomAnnotation 的方法:

    @annotation(com.example.annotation.MyCustomAnnotation)

3.3.8 @within 表达式

@within() 用来匹配标注了特定注解的类中的所有方法。

语法格式:

@within(注解类型)

  • 注解类型:指定注解的全路径。

示例:

  • 匹配所有被 @Service 注解标记的类中的方法:

    @within(org.springframework.stereotype.Service)

3.3.9 @target 表达式

@target() 用来匹配目标对象标注了特定注解的情况,作用与 @within() 类似,但它只针对目标对象而不是类本身。

语法格式:

@target(注解类型)

  • 注解类型:指定注解的全路径。

示例:

  • 匹配所有目标对象被 @Transactional 注解标记的类中的方法: @target(org.springframework.transaction.annotation.Transactional)

3.3.10 @args 表达式

@args() 用来匹配方法参数标注了特定注解的情况。

语法格式:

@args(注解类型)

  • 注解类型:指定注解的全路径。

示例:

  • 匹配方法参数被 @Valid 注解标记的方法: @args(javax.validation.Valid)

3.3.11 @within 与 within 的区别

  • @within 是用来匹配类级别 的注解,而 within 是基于包 或类层级进行匹配。

  • 例如:

  • within(com.example..*) 匹配 com.example 包及其子包下的所有类。

  • @within(org.springframework.stereotype.Service) 匹配被 @Service 注解标记的所有类中的方法。

3.3.12 bean 表达式

bean() 用于匹配特定 Bean 名称的类中的方法。

语法格式:

bean(beanName)

  • beanName:Bean 的名称。

示例:

  • 匹配名称为 userService 的 Bean 的所有方法: bean(userService)

总结

  1. execution():最常用的表达式,匹配方法执行,精细化控制。
  2. within():匹配类或包中的所有方法。
  3. args():匹配方法参数的类型。
  4. this() 和 target():匹配代理对象或目标对象的类型。
  5. @annotation():匹配方法上带有特定注解。
  6. @within():匹配类上带有特定注解的情况。
  7. bean():匹配特定名称的 Spring Bean。

3.4 通知的定义与实现

切入点表达式后跟的函数可以看作是对切入点表达式的"命名”,但是通知后跟的函数是要在拦截调用后要执行的操作。

3.4.1 前置通知的实现

@Before("execution(* com.example.service.UserService.*(..))")
public void logBeforeMethod(JoinPoint joinPoint) {
    System.out.println("执行方法之前:" + joinPoint.getSignature().getName());
}
  • @Before:这是一个前置通知,表示在目标方法执行之前运行。
  • JoinPoint:提供对当前连接点的访问,例如获取方法名、参数等。

3.4.2 后置通知的实现

@After("execution(* com.example.service.UserService.*(..))")
public void logAfterMethod(JoinPoint joinPoint) {
    System.out.println("方法执行结束:" + joinPoint.getSignature().getName());
}
  • @After:无论方法是否正常结束,都会在方法执行后调用。

3.4.3 返回通知的实现

@AfterReturning(pointcut = "execution(* com.example.service.UserService.*(..))", returning = "result")
public void logAfterReturningMethod(JoinPoint joinPoint, Object result) {
    System.out.println("方法返回值:" + result);
}
  • @AfterReturning:仅在目标方法成功返回后才调用,并且可以获取返回值。
  • returning = "result":指定返回值可以作为参数传递给通知。

3.4.4异常通知的实现

@AfterThrowing(pointcut = "execution(* com.example.service.UserService.*(..))", throwing = "error")
public void logAfterThrowingMethod(JoinPoint joinPoint, Throwable error) {
    System.out.println("方法抛出异常:" + error);
}
  • @AfterThrowing:在方法抛出异常后调用。
  • throwing = "error":将异常对象作为参数传递给通知。

3.4.5 环绕通知的实现

@Around("execution(* com.example.service.UserService.*(..))")
public Object logAroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("方法执行之前:" + joinPoint.getSignature().getName());
    Object result = joinPoint.proceed();  // 继续执行目标方法
    System.out.println("方法执行之后:" + joinPoint.getSignature().getName());
    return result;
}
  • @Around:环绕通知既能控制方法执行前后,也可以决定是否执行目标方法。
  • ProceedingJoinPoint:它是 JoinPoint 的子类,可以通过 proceed() 方法调用目标方法。

3.5 通知中方法使用的参数JoinPoint 和 ProceedingJoinPoint

3.5.1 JoinPoint 的常用方法

| 方法 | 说明 |
|——————|——————————–|
| getArgs() | 获取目标方法的参数,返回一个 Object[] 数组。 |
| getSignature() | 获取目标方法的签名(包括方法名、返回类型、参数类型等信息)。 |
| getTarget() | 获取目标对象,即实际被代理的对象(目标类的实例)。 |
| getThis() | 获取代理对象(代理类实例)。 |
| toString() | 返回当前连接点的字符串表示。 |

3.5.2 ProceedingJoinPoint 的常用方法 (继承自 JoinPoint,用于 @Around 通知)

| 方法 | 说明 |
|————————–|————————————————|
| proceed() | 执行目标方法。 |
| proceed(Object[] args) | 执行目标方法,并传入新的参数。 |
| getArgs() | 获取目标方法的参数,返回一个 Object[] 数组(继承自 JoinPoint)。 |
| getSignature() | 获取目标方法的签名(继承自 JoinPoint)。 |
| getTarget() | 获取目标对象(继承自 JoinPoint)。 |
| getThis() | 获取代理对象(继承自 JoinPoint)。 |

  • JoinPoint 提供了获取目标方法、参数、目标对象和代理对象的功能。
  • ProceedingJoinPoint 继承自 JoinPoint,除了 JoinPoint 的功能外,ProceedingJoinPoint 还可以通过 proceed() 控制目标方法的执行,允许修改方法的参数和返回值,通常用于环绕通知(@Around)。
  • 以下内容的joinPoint是ProceedingJoinPoint joinPoint中的,故还是ProceedingJoinPoint类型,而并非是JoinPoint 类型,故具有proceed() 方法控制目标方法的执行。
  • joinPoint.proceed(); 调用实际的目标方法,执行业务逻辑。
  • proceed() 方法返回的类型是 Object,它是目标方法的返回值。
  • Object result = joinPoint.proceed(); 的写法用于捕获目标方法的返回值,这样可以对返回值进行处理、修改或记录。
  • 直接使用 joinPoint.proceed(); 适合不需要处理返回值的场景,但在大多数情况下,捕获返回值会更灵活和实用。

3.5.3 具体使用例子

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.UserService.*(..))")
    public void logBeforeMethod(JoinPoint joinPoint) {
        System.out.println("开始执行方法: " + joinPoint.getSignature().getName());

        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            System.out.println("方法参数:" + arg);
        }

        System.out.println("目标对象:" + joinPoint.getTarget());
        System.out.println("代理对象:" + joinPoint.getThis());
    }
}

JoinPoint 在上面的通知方法中提供了以下信息:

  • joinPoint.getSignature().getName():获取被调用方法的名称。
  • joinPoint.getArgs():获取被调用方法的参数,返回一个 Object[] 数组。
  • joinPoint.getTarget():获取被代理的目标对象,即执行目标方法的实际对象(目标类的实例)。
  • joinPoint.getThis():获取当前的代理对象(即 Spring AOP 生成的代理类)。

输出示例:

如果我们有一个 UserService 类如下:

@Service
public class UserService {
    public void registerUser(String userName) {
        System.out.println("用户注册: " + userName);
    }
}

然后调用:

userService.registerUser("Alice");

输出结果可能是:

开始执行方法: registerUser
方法参数:Alice
目标对象:com.example.service.UserService@123abc
代理对象:com.example.service.UserService$$EnhancerBySpringCGLIB$$123abc

4.五个横切关注点的实现

4.1 日志记录

在 Spring AOP 中实现日志记录功能,可以让开发者通过切面(Aspect)来记录应用程序中的方法调用信息,例如方法名、参数、返回值、异常等,而不需要手动在每个方法中编写日志代码。

4.1.1 引入依赖

Spring Boot 默认包含日志框架,不需要额外配置。如果你是传统项目,可以添加以下依赖:

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

4.1.2 实现日志切面

创建一个 AOP 切面类,用于记录方法执行前后的日志。

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

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

    // 定义切入点:匹配 service 包下的所有方法
    @Before("execution(* com.example.service.*.*(..))")
    public void logBeforeMethod(JoinPoint joinPoint) {
        logger.info("开始执行方法: {}", joinPoint.getSignature().getName());
    }

    @After("execution(* com.example.service.*.*(..))")
    public void logAfterMethod(JoinPoint joinPoint) {
        logger.info("方法执行结束: {}", joinPoint.getSignature().getName());
    }
}

详细解释:

Logger logger :创建一个日志记录器,用来记录日志。LoggerFactory.getLogger(LoggingAspect.class) 返回一个与 LoggingAspect 类绑定的日志记录器。

logger.info("开始执行方法: {}", joinPoint.getSignature().getName()); :使用日志记录器记录日志,输出目标方法的名称。joinPoint.getSignature().getName() 返回被调用方法的名称。

4.1.3 配置日志存储

在 application.properties中,配置日志输出到控制台或文件。

# 输出日志到文件
logging.file.name=logs/application.log
logging.level.root=INFO
  • logging.file.name=logs/application.log :将日志输出到文件,文件路径为 logs/application.log。日志文件存储在项目的 logs 目录下。
  • logging.level.root=INFO :设置全局日志级别为 INFO。这意味着,日志级别低于 INFO 的日志(如 DEBUG、TRACE)将不会被记录,而 INFO、WARN、ERROR 级别的日志会被记录。

4.2 事务管理

Spring AOP 提供了声明式事务管理,只需使用 @Transactional 注解,Spring AOP 会自动处理事务的开启、提交和回滚。

实现示例:

@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        // 业务逻辑:插入用户数据到数据库
    }
}

工作原理:

  • @Transactional:Spring AOP 在方法调用时,自动开启事务,方法执行完毕后提交事务。如果方法抛出异常,事务将回滚。

4.3 性能监控

4.3.1 实现性能监控的基本步骤

  1. 创建一个 AOP 切面类 :通过 @Aspect 和 @Around 注解,围绕目标方法计算方法的执行时间。
  2. 记录方法的执行时间:在方法执行前记录开始时间,方法执行后记录结束时间,计算差值就是方法的执行时间。
  3. 将执行时间输出到日志:使用日志记录器将方法的执行时间记录到日志中,方便后续分析和调试。

4.3.2 具体实现步骤

4.3.2.1 配置日志框架

为了将监控的性能数据记录到日志中,我们首先需要确保已经配置了日志框架,例如 Logback 或 Log4j。在 Spring Boot 中,默认会使用 Logback,因此我们不需要手动添加额外的依赖。

4.3.2.2 创建 AOP 切面类

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PerformanceMonitorAspect {

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

    // 定义切入点,匹配 com.example.service 包下所有类的所有方法
    @Around("execution(* com.example.service.*.*(..))")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();  // 记录方法开始时间

        // 执行目标方法
        Object result = joinPoint.proceed();

        long executionTime = System.currentTimeMillis() - start;  // 计算方法执行时间
        logger.info("方法 {} 执行时间: {} 毫秒", joinPoint.getSignature(), executionTime);

        return result;  // 返回方法的执行结果
    }
}

代码解释

  • @Aspect:标记该类为切面类,表明该类中包含横切逻辑(性能监控)。
  • @Component:将切面类注册为 Spring 管理的 Bean,使其可以参与 AOP 切面。
  • @Around("execution(* com.example.service.*.*(..))") :定义切入点,匹配 com.example.service 包下所有类中的所有方法。@Around 表示环绕通知,允许在目标方法执行的前后加入逻辑。
  • ProceedingJoinPoint joinPoint:表示当前的目标方法,通过它可以获取方法签名、参数等信息,并执行目标方法。
  • long start = System.currentTimeMillis():记录方法开始的时间(以毫秒为单位)。
  • joinPoint.proceed():执行目标方法,返回目标方法的执行结果。
  • long executionTime = System.currentTimeMillis() - start:计算方法执行的总耗时。
  • logger.info:将方法的执行时间记录到日志中。

4.3.2.3 如何应用到具体的方法

假设我们有一个 UserService 类,其中包含方法 getUserDetails。AOP 切面将自动拦截并监控其性能:

@Service
public class UserService {

    public String getUserDetails(String userId) {
        // 模拟业务逻辑
        try {
            Thread.sleep(100);  // 模拟耗时操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "用户详情: " + userId;
    }
}

当调用 UserService.getUserDetails 时,PerformanceMonitorAspect 切面将自动记录该方法的执行时间。

4.3.2.4 测试结果输出

当 getUserDetails 方法被调用时,日志将输出类似的信息:

INFO - 方法 public java.lang.String com.example.service.UserService.getUserDetails(java.lang.String) 执行时间: 102 毫秒

4.3.3 自定义性能阈值与报警

你可以进一步优化切面,设置一个性能阈值,当方法执行时间超过某个值时输出警告日志,提醒开发人员关注性能问题。

实现自定义性能阈值

@Aspect
@Component
public class PerformanceMonitorAspect {

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

    // 设置一个阈值,超过这个时间就记录警告日志(以毫秒为单位)
    private static final long THRESHOLD = 500;

    @Around("execution(* com.example.service.*.*(..))")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();

        // 执行目标方法
        Object result = joinPoint.proceed();

        long executionTime = System.currentTimeMillis() - start;

        if (executionTime > THRESHOLD) {
            logger.warn("方法 {} 执行时间: {} 毫秒,超过阈值 {} 毫秒", joinPoint.getSignature(), executionTime, THRESHOLD);
        } else {
            logger.info("方法 {} 执行时间: {} 毫秒", joinPoint.getSignature(), executionTime);
        }

        return result;
    }
}

解释:

  • THRESHOLD :设置性能阈值为 500 毫秒。如果方法执行时间超过这个阈值,将记录警告日志(logger.warn),否则记录普通信息日志。
  • 当某个方法执行时间超过设定的阈值时,会输出类似以下的警告信息:
WARN  - 方法 public java.lang.String com.example.service.UserService.getUserDetails(java.lang.String) 执行时间: 800 毫秒,超过阈值 500 毫秒

4.3.4 日志配置

可以通过 application.properties 或 application.yml 文件配置日志的存储位置、格式等信息。

在 application.properties 中配置日志文件和日志级别:

# 将日志输出到 logs/application.log 文件中
logging.file.name=logs/application.log

# 设置日志级别,INFO 级别及以上的日志会被记录
logging.level.root=INFO
logging.level.com.example=DEBUG

4.3.5 扩展性能监控的功能

使用注解实现更精细的性能监控

你可以通过自定义注解来标注需要监控的方法,避免对所有方法都进行性能监控。例如:

4.3.5.1 定义一个注解 @MonitorPerformance

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MonitorPerformance {
}
  • public @interface MonitorPerformance {} :这定义了一个名为 MonitorPerformance 的注解。注解的定义类似于接口声明,使用 @interface 关键字创建。

  • 这个注解目前是空的,没有任何属性或方法,它的主要作用是作为标记,可以通过反射或 AOP 等机制识别并应用特定的逻辑(如性能监控)。

  • @Retention 是元注解(Meta-Annotation),它定义了注解的保留策略。注解保留策略决定了注解在生命周期的哪个阶段对其进行保留。

  • RetentionPolicy.RUNTIME :表示注解会保留到运行时 ,因此可以通过反射机制在运行时读取注解。大多数与 AOP 或反射相关的注解都需要 RUNTIME 级别的保留策略,因为它们通常用于运行时动态处理逻辑。

  • @Target 也是元注解,它定义了注解可以应用的程序元素类型。

  • ElementType.METHOD :表示这个注解只能应用在方法上。它限制了 MonitorPerformance 注解的使用范围,确保它只能标注方法,不能标注类、字段、构造器等其他程序元素。

4.3.5.2 在切面中使用注解作为切入点

@Aspect
@Component
public class PerformanceMonitorAspect {

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

    @Around("@annotation(com.example.annotations.MonitorPerformance)")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();

        Object result = joinPoint.proceed();

        long executionTime = System.currentTimeMillis() - start;
        logger.info("方法 {} 执行时间: {} 毫秒", joinPoint.getSignature(), executionTime);

        return result;
    }
}

上面所定义的注解在这段代码里只出现在了@Around(“@annotation(com.example.annotations.MonitorPerformance)“) 这堆里,而后面的函数public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {}则跟你所定义的注解无关,只是名字起的很像而已。

4.3.5.3 在具体方法上使用 @MonitorPerformance 注解

@Service
public class UserService {

    @MonitorPerformance
    public String getUserDetails(String userId) {
        // 模拟耗时业务逻辑
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "用户详情: " + userId;
    }
}

这样,只有带有 @MonitorPerformance 注解的方法会进行性能监控。

  • 使用 AOP 进行性能监控 :通过 @Around 注解和 ProceedingJoinPoint,可以在不修改业务逻辑的情况下,监控方法的执行时间。
  • 日志记录执行时间:将方法执行的时间记录到日志中,帮助开发者发现性能瓶颈。
  • 阈值报警:可以通过设置执行时间的阈值,在执行时间过长时发出警告。
  • 扩展性:通过自定义注解,可以灵活地对特定的方法进行性能监控,而不必对所有方法进行监控。

原文链接: https://blog.csdn.net/m0_73837751/article/details/142341367

标签: #软件开发 1171
相关文章

万字:支付“核心系统”详解 2024-11-02 15:33

专栏作者:隐墨星辰 \| 主编:陈天宇宙 这篇文章也尝试化繁为简,探寻支付系统的本质,讲清楚在线支付系统最核心的一些概念和设计理念。 虽然支付行业已经过了风头最劲的时光,但跨境支付仍然在蓬勃发展,每年依然有很多新人进入这个行业,这篇文章尝试为这些刚入行的新人提供一点帮助。 文章只介绍一些支付行业十几

资深支付架构师视角:实战从问题定义到代码落地的完整套路 2024-11-02 15:33

前言 今天从一个实际案例入手,介绍站在架构师的角度,如何识别并定义问题,提炼需求,技术方案选型,再到详细设计,最后利用AI的能力协助写出核心的代码,验证与调优。 解决问题存在一定的模式,也可以称之为框架,总结出自己的思考和解题框架,以后再碰到同类型的问题就可以如庖丁解牛一样容易。 很多年前,我写代码

Spring 实现 3 种异步接口 2024-10-18 09:07

大家好,我是苏三~ 如何处理比较耗时的接口? 这题我熟,直接上异步接口,使用 Callable、WebAsyncTask 和 DeferredResult、CompletableFuture等均可实现。 但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地

重学SpringBoot3-集成Redis(五)之布隆过滤器 2024-10-08 11:24

更多SpringBoot3内容请关注我的专栏:《SpringBoot3》 期待您的点赞👍收藏⭐评论✍ 重学SpringBoot3-集成Redis(五)之布隆过滤器 1. 什么是布隆过滤器? * 基本概念 适用场景 2. 使用 Redis 实现布隆过滤器 * 项目依赖 Redis 配置

设计模式第16讲——迭代器模式(Iterator) 2024-10-08 11:24

一、什么是迭代器模式 迭代器模式是一种行为型设计模式,它提供了一种统一的方式来访问集合对象中的元素,而不是暴露集合内部的表示方式。简单地说,就是将遍历集合的责任封装到一个单独的对象中,我们可以按照特定的方式访问集合中的元素。 二、角色组成 抽象迭代器(Iterator):定义了遍历聚合对象所需的方法

vue2路由和vue3路由区别及原理 2024-10-08 11:24

一、Vue2 与 Vue3 路由的区别 1. 创建路由实例方式的不同 Vue 2 中,通过 Vue.use() 注册路由插件,并通过 new VueRouter() 来创建路由实例。 import Vue from 'vue';import VueRouter from 'vue-router';i

目录

IT 外包服务商

  • 意见投递
  • zyf6619

软件开发应用

主菜单

  • 首页
  • 软件开发
  • 计算机基础
  • Hello Halo
  • 新手必读
  • 关于本知识库
Copyright © 2024 your company All Rights Reserved. Powered by Halo.