问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

结合代码理解Spring AOP的概念(切面、切入点、连接点等)

创作时间:
作者:
@小白创作中心

结合代码理解Spring AOP的概念(切面、切入点、连接点等)

引用
CSDN
1.
https://blog.csdn.net/weixin_37477009/article/details/146463880

本文通过具体代码示例,详细介绍了Spring AOP的核心概念及其实现原理,包括切面、切入点、连接点和通知等关键组件。通过阅读本文,读者可以深入理解AOP的工作机制,并掌握如何在实际项目中应用AOP来实现日志记录、性能监控等功能。

前情回顾

在上一篇文章中,我们介绍了为什么需要AOP(AOP解决了什么问题)以及如何实现AOP。但在实现AOP的时候,并未探讨AOP相关概念,例如:切面、切入点、连接点等。因此,本篇文章希望结合代码去理解Spring AOP的相关概念。

Talk is cheap, show me the code.

背景

在使用AOP时,我们大概率遇到了这样的场景:我现在有多个方法,在这多个方法执行前/执行后要做一些统一的操作。

例如:

@RequestMapping("/user")
@RestController
public class UserController {
    @GetMapping("/query")
    public String queryUser() {
        return "I am a user";
    }
}
@RequestMapping("/student")
@RestController
public class StudentController {
    @GetMapping("/query")
    public String queryStudent() {
        return "I am a student";
    }
}

我希望在执行这两个方法前,打印一行日志:start execute。

为多个方法增加逻辑,这些代码写在哪里呢?当然是写到一个类里啊(Java嘛,万事万物皆对象,要封装到类里)。

public class LogAspect {
    public void log() {
        System.out.println("start execute");
    }
}

这样显然是不够的,因为,Spring并不知道这个类是特殊的类,这些代码要为谁增强。因此,我们要遵循Spring规范,提供一些标记。

@Aspect
public class LogAspect {
    public void log() {
        System.out.println("start execute");
    }
}

查看下@Aspect这个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Aspect {
    String value() default "";
}

非常的简单,就是告知Spring这个类是一个切面类。切面类中的方法,是其他方法的补充逻辑。

然而,仅仅打上@Aspect这个注解是不够的(因为LogAspect没有注入到Spring容器中),还需要打上@Component注解,告诉Spring帮我管理这个Bean。

在Spring容器中,Spring管理着UserController和StudentController这些bean,可以为它们分别生成代理类,然后将Spring容器中的LogAspect合适地织入到代理类中,从而增强了UserController和StudentController的功能。

切面 + 切入点 + 连接点 + 通知

这个切面类中的方法,给谁用呢?显然,这也需要告知Spring。

开发者自己是知道要给谁用的,例如:给UserController的queryUser方法和StudentController的queryStudent方法用。这些方法可以被通俗地理解为一个个连接点(Joinpoint)。LogAspect的log方法是给多个连接点使用的,这多个连接点又称为切入点(Pointcut)

@Aspect
@Component
public class LogAspect {
    @Pointcut("execution(* com.forrest.learn.springboot.example5.controller.*.*(..))")
    private void example5Controller() {}
    public void log() {
        System.out.println("start execute");
    }
}

execution(* com.forrest.learn.springboot.example5.controller..(..))
切入点表达式,不太好写,而且容易过度拦截连接点。我们只想拦截UserController的queryUser方法和StudentController的queryStudent方法。这时候怎么办?用注解。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LogController {
}
@RequestMapping("/student")
@RestController
public class StudentController {
    @LogController
    @GetMapping("/query")
    public String queryStudent() {
        return "I am a student";
    }
}
@RequestMapping("/user")
@RestController
public class UserController {
    @LogController
    @GetMapping("/query")
    public String queryUser() {
        return "I am a user";
    }
}
@Aspect
@Component
public class LogAspect {
    @Pointcut("@annotation(com.forrest.learn.springboot.example5.annotation.LogController)")
    private void example5Controller() {}
    public void log() {
        System.out.println("start execute");
    }
}

只要方法打上了@LogController注解,就要被拦截。又引出了另一个问题,什么时候拦截呢?是方法执行前拦截?还是执行后拦截?显然,需要通知(Advise)

  • @Before :拦截方法,在方法执行前增强
  • @AfterReturning :拦截方法,在方法执行并正常返回后增强
  • @AfterThrowing :拦截方法,在方法执行并异常返回后增强
  • @After :拦截方法,在方法执行后增强
  • @Around :拦截方法,用户自行决定在方法前/后进行增强,也就是包含了前面4个注解的功能了,是最自由的增强。
@Aspect
@Component
public class LogAspect {
    @Pointcut("@annotation(com.forrest.learn.springboot.example5.annotation.LogController)")
    private void example5Controller() {}
    @Before("example5Controller()")
    public void log() {
        System.out.println("start execute");
    }
}

很清楚地知道了,给哪些连接点增强了。

在Spring Boot应用中,通常不需要手动添加@EnableAspectJAutoProxy注解来启用AOP功能。这是因为Spring Boot已经为你自动配置了AOP支持。具体来说,Spring Boot会自动扫描项目中的

@Aspect注解类,并将其注册为切面(Aspect),同时启用AspectJ代理机制。

小结

@Aspect  // 切面(为多个类提供增强逻辑,逻辑由方法实现,方法写在类中)
@Component // 需要将切面类注入到Spring容器中
public class LogAspect {
    // 为哪些方法进行增强?靠定义切入点(一组连接点)
    @Pointcut("@annotation(com.forrest.learn.springboot.example5.annotation.LogController)")
    private void example5Controller() {}
    // 什么时候进行增强?靠通知(Advice)
    @Before("example5Controller()")
    public void log() {
        System.out.println("start execute");
    }
}

连接点的进阶

我需要统计方法执行的耗时,并且打印出方法名、方法入参。

/**
 * 从连接点中获取方法名,而不是通过注解的字段
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MetricTime {
//    String value() default "";  // 用户可以将方法名称传给value
}
@Aspect
@Component
public class MetricTimeAspect {
    // 切入点作为通知的参数
    @Around("@annotation(com.forrest.learn.springboot.example5.annotation.MetricTime)")
    public Object metricTime(ProceedingJoinPoint pjp) throws Throwable {
        long startAt = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            System.out.println(pjp.getSignature().getName() + " cost " + (System.currentTimeMillis() - startAt) + " ms");
            System.out.println("入参:" + Arrays.toString(pjp.getArgs()));
        }
    }
}
/*
queryUser cost 0 ms
入参:[]
*/

切面类中的方法,可以有哪些入参?

  • ProceedingJoinPoint pjp

JoinPoint是AOP的核心接口之一,它提供了连接点的信息,例如方法名、参数值等。ProceedingJoinPoint是JoinPoint的子接口,专门用于@Around通知中。在其他通知(如@Before、@After、@AfterReturning、@AfterThrowing)中,通常使用JoinPoint。查看源码,就知道ProceedingJoinPoint、JoinPoint提供了哪些方法。

还可以传入注解:

@Aspect
@Component
public class MetricTimeAspect {
    @Around("@annotation(metricTime)")
    public Object metricTime(ProceedingJoinPoint pjp, MetricTime metricTime) throws Throwable {
        long startAt = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            System.out.println(pjp.getSignature().getName() + " cost " + (System.currentTimeMillis() - startAt) + " ms");
            System.out.println("入参:" + Arrays.toString(pjp.getArgs()));
        }
    }
}

思路 > 技术细节

Spring AOP在技术细节上还有很多知识。等真正需要用到这些知识时,我们可以查看官方文档,借助AI来帮助落地。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号