利用注解实现AOP

Sabthever

  AOP(面向切面编程)。AOP是一种编程范式,旨在通过预编译和运行期动态代理来增强方法,而注解在AOP中主要用于定义切面(Aspect)、切入点(Pointcut)和通知(Advice)。

  注解提供了一种在代码中添加元数据的方式,使得开发者可以更方便地定义这些切面、切入点和通知,从而在不改变原有业务逻辑代码的情况下,实现功能增强‌[1]

AOP的基本概念[1]

  • 切面(Aspect)‌:通常是一个类,里面可以定义切入点和通知。
  • 切入点(Pointcut)‌:带有通知的连接点,定义了通知应用的场景。
  • 通知(Advice)‌:在特定的切入点上执行的增强处理,包括前置、返回后、异常处理等类型。
  • 连接点(JointPoint)‌:程序执行过程中的明确点,通常是方法的调用。
  • 目标对象(Target Object)‌:包含连接点的对象,也被称作被通知或被代理对象。
  • 织入(Weave)‌:将切面应用到目标对象并导致代理对象创建的过程。
  • 引入(Introduction)‌:在不修改代码的前提下,为类动态地添加一些方法或字

注解的使用[2]

一、@interface 关键字

我们想定义一个自己的注解 需要使用 @interface 关键字来定义。
如定义一个叫 MyAnnotation 的注解:

1
public @interface MyAnnotation { }

二、元注解

光加上 @interface 关键字 还不够,我们还需要了解5大元注解

  • @Retention
  • @Target
  • @Documented
  • @Inherited(JDK8 引入)
  • @Repeatable(JDK8 引入)

1)@Retention 指定注解的生命周期

@Retention(RetentionPolicy.SOURCE)

其中Retention是一个枚举类:

  1. RetentionPolicy.SOURCE : 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃(.java文件)
  2. RetentionPolicy.CLASS :注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期(.class文件)
  3. RetentionPolicy.RUNTIME: 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在(内存中的字节码)

2)@Target指定注解可以修饰的元素类型

@Target(ElementType.Field)

  1. ElementType.ANNOTATION_TYPE - 标记的注解可以应用于注解类型。
  2. ElementType.CONSTRUCTOR - 标记的注解可以应用于构造函数。
  3. ElementType.FIELD - 标记的注解可以应用于字段或属性。
  4. ElementType.LOCAL_VARIABLE - 标记的注解可以应用于局部变量。
  5. ElementType.METHOD - 标记的注解可以应用于方法。
  6. ElementType.PACKAGE - 标记的注解可以应用于包声明。
  7. ElementType.PARAMETER - 标记的注解可以应用于方法的参数。
  8. ElementType.TYPE - 标记的注解可以应用于类的任何元素。

3)@Documented

指定注解会被JavaDoc工具提取成文档。默认情况下,JavaDoc是不包括文档的

4)@Inherited
表示该注解会被子类继承,注意,仅针对类,成员属性、方法并不受此注释的影响。

5)@Repeatable
表示注解可以重复使用,为了解决同一个注解不能重复在同一类/方法/属性上使用的问题。

其中最常用的就是 @Retention@Target

三、切面注解

在SpringBoot项目的pom文件中,引入 spring-boot-starter-aop依赖。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

原文链接:https://blog.csdn.net/shaopengjie2/article/details/136174655

简单说一下各个注解代表什么含义:

  • **@Aspect:**作用是把当前类标识为一个切面供容器读取 ,也就是加上这个注解,spring才知道你这是一个切面类,用于处理切点的逻辑的。
  • @Pointcut:切入点,@Pointcut切点表达式非常丰富,可以将 方法(method)、类(class)、接口(interface)、包(package) 等作为切入点,非常灵活,常用的有@annotation、@within、execution等方式,上面的示例使用的是@annotation方式,意思就是说被Spring扫描到方法上带有@annotation中的注解 就会执行切面通知。
  • @Before:该注解标注的方法在业务模块代码执行之前执行,其不能阻止业务模块的执行,除非抛出异常;
  • @AfterReturning:该注解标注的方法在业务模块代码执行之后执行;
  • @AfterThrowing:该注解标注的方法在业务模块抛出指定异常后执行;
  • @After:该注解标注的方法在所有的 Advice 执行完成后执行,无论业务模块是否抛出异常,类似于 finally 的作用;
  • @Around:该注解功能最为强大,其所标注的方法用于编写包裹业务模块执行的代码,通知的第一个参数必须是 ProceedingJoinPoint 类型。在通知体内,调用 ProceedingJoinPoint 的 proceed () 方法使得连接点方法执行如果不调用 proceed () 方法,连接点方法则不会执行。无论是调用前逻辑还是调用后逻辑,都可以在该方法中编写,甚至其可以根据一定的条件而阻断业务模块的调用; 如果切面中使用了@Around 注解,如果不调用 ProceedingJoinPoint 的 proceed () 方法的话,那么 @Before 和 @After 直接标注的方法也不会被触发。@Around 注解标注的方法,在 ProceedingJoinPoint 的 proceed () 方法 前的逻辑是比@Before的逻辑还要靠前, 在proceed () 方法之后的逻辑比 @After 的逻辑还要靠后。
  • Joint Point:JointPoint是程序运行过程中可识别的点,这个点可以用来作为AOP切入点。JointPoint对象则包含了和切入相关的很多信息。比如切入点的对象,方法,属性等。我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。

四、切入点表达式

 这个我也还没搞明白,先拿下来。

​ 我们定义切点除了使用 @Pointcut() 之外,我们还有丰富的切点表达式可以定义切点。

1)切点表达式简介

 2)通配符合与逻辑运算符

@AspectJ 支持三种通配符:

逻辑运算符: 切点表达式由切点函数组成,切点函数之间还可以进行逻辑运算,组成复合切点。

3)切点表达式:

  1. arg() :匹配切入点方法的参数类型,匹配的上才是切点。
    语法:args(param-pattern) param-pattern:参数类型的全路径。
    注意:要先匹配到某些类,不然会报错,也就是不能单独用
    示例:

    1
    2
    3
    @Pointcut("args(java.lang.String)")  //这样就是错的,不能单独使用要匹配到某些类

    @Pointcut("within(com.example.demo.service.impl.UserServiceImpl) && args(java.lang.String,java.lang.String)") //要像这样使用 within 先匹配到某个具体的类,在使用args匹配到某个类型参数的方法
  2. @args:匹配切入点方法上的参数的类上,参数的类必须要有指定的注解
    语法:@args(annotation-type) annotation-type:注解类型的全路径
    注意:也不能单独使用,必须先指定到类,而且匹配参数个数至少有一个且为第一个参数的类含有该注解才能匹配的上
    示例:
    @Pointcut("within(com.demo.RedisTest) && @args(com.demo.aaa.annotation.MyAnnotation)")

  3. **within:**匹配切入点的指定类的任意方法,不能匹配接口。
    语法:within(declaring-type) 参数为全路径的类名(可使用通配符),表示匹配当前表达式的所有类都将被当前方法环绕
    注意: 这个是指定到具体的类
    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //within表达式的粒度为类,其参数为全路径的类名(可使用通配符),表示匹配当前表达式的所有类都将被当前方法环绕。如下是within表达式的语法:
    @Pointcut(within(declaring-type-pattern))

    //within表达式只能指定到类级别,如下示例表示匹配com.spring.service.BusinessObject中的所有方法:
    @Pointcut(within(com.spring.service.BusinessObject))

    //within表达式路径和类名都可以使用通配符进行匹配,比如如下表达式将匹配com.spring.service包下的所有类,不包括子包中的类:
    @Pointcut(within(com.spring.service.*))

    //如下表达式表示匹配com.spring.service包及子包下的所有类:
    @Pointcut(within(com.spring.service..*))
  4. **@within:**表示匹配带有指定注解的类。
    语法:@within(annotation-type) 注解的全类名
    注意:这个是指定到带有某个注解的类
    示例:

    1
    2
    //如下所示示例表示匹配使用com.spring.annotation.BusinessAspect注解标注的类:
    @within(com.spring.annotation.BusinessAspect)
  5. **@annotation() :**匹配带有指定注解的连接点
    语法:@annotation(annotation-type) annotation-type:注解类型的全路径
    示例:
    @Pointcut("@annotation(com.test.annotations.LogAuto)")

  6. execution() 用于匹配是连接点的执行方法,Spring 切面粒度最小是达到方法级别,而 execution 表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的配置,所以是使用最广泛的。
    用法:

    • modifiers-pattern:方法的可见性修饰符,如 public,protected,private;
    • ret-type-pattern:方法的返回值类型,如 int,void 等;
    • declaring-type-pattern:方法所在类的全路径名,如 com.spring.Aspect;
    • name-pattern:方法名,如 getOrderDetail();
    • param-pattern:方法的参数类型,如 java.lang.String;
    • throws-pattern:方法抛出的异常类型,如 java.lang.Exception;

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
modifiers-pattern:方法的可见性修饰符,如 publicprotectedprivate
ret-type-pattern:方法的返回值类型,如 intvoid 等;
declaring-type-pattern:方法所在类的全路径名,如 com.spring.Aspect;
name-pattern:方法名,如 getOrderDetail();
param-pattern:方法的参数类型,如 java.lang.String;
throws-pattern:方法抛出的异常类型,如 java.lang.Exception;
示例:

// 匹配目标类的所有 public 方法,第一个 * 代表返回类型,第二个 * 代表方法名,..代表方法的参数
execution(public * *(..))

// 匹配目标类所有以 User 为后缀的方法。第一个 * 代表返回类型,*User 代表以 User 为后缀的方法
execution(* *User(..))

// 匹配 User 类里的所有方法
execution(* com.test.demo.User.*(..))

// 匹配 User 类及其子类的所有方法
execution(* com.test.demo.User+.*(..)) :

// 匹配 com.test 包下的所有类的所有方法
execution(* com.test.*.*(..))

// 匹配 com.test 包下及其子孙包下所有类的所有方法
execution(* com.test..*.*(..)) :

// 匹配 getOrderDetail 方法,且第一个参数类型是 Long,第二个参数类型是 String
execution(* getOrderDetail(Long, String))

程序样例

  该样例主要是给方法的参数赋值,里面有值传递和地址传递的问题,还关系到增加String字符,地址的改变。

  • Controller调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    @RequestMapping("/test")
    public class TestController {
    @Autowired
    private TestService testService;

    @GetMapping("/annotation")
    public String AnnotationTest(){
    HashMap<String, String> stringStringHashMap = new HashMap<>();
    return testService.AnnotationTest("wrong",stringStringHashMap);
    }
    }
  • Service层中的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public interface TestService {
    public String AnnotationTest(String text, Map<String,String> mmp);
    }

    @Service
    public class TestServiceImpl implements TestService {
    @Override
    @MyAnnotation(message = "bbb")
    public String AnnotationTest(String text, Map<String,String> mmp) {
    for(String key : mmp.keySet()) {
    System.out.println(key + ":" + mmp.get(key));
    }
    String info = "output: "+text;
    System.out.println(info);
    return info;
    }
    }
  • 注解类

    1
    2
    3
    4
    5
    6
    7
    8
    import java.lang.annotation.*;

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @Documented
    public @interface MyAnnotation {
    String message() default "aaa";
    }
  • 注解方法实现(Before)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Aspect
    @Component
    public class MyAnnotationAspect {
    @Before("@annotation(myAnnotation)")
    public void before(JoinPoint joinPoint, MyAnnotation myAnnotation){
    String message = myAnnotation.message();
    Object[] args = joinPoint.getArgs();
    for (Object arg : args) {
    if(arg instanceof String){
    arg = message;
    }
    if(arg instanceof Map){
    Map<String, String> paramMap = (Map<String, String>) arg;
    paramMap.put("key1","value1");
    paramMap.put("key2", "value2");
    }
    }
    }
    }

      这个方法中,是将被注解的方法中String类型提出,赋值为message,但是这边是无法成功的,原因是这边的message获取了String类型的数据后,我给arg进行赋值是没有用的,因为Service中的那个String类型的地址是不会改变的,而我修改String的时候Aspect中的地址会发生改变。Map能成功是因为它是个对象,是直接进行操作的。

      调用一次接口,在输出终端中输出为:

    1
    2
    3
    key1:value1
    key2:value2
    output: wrong
  • 注解方法实现(Around)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Aspect
    @Component
    public class MyAnnotationAspect {
    @Around("@annotation(myAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint, MyAnnotation myAnnotation) throws Throwable {
    String message = myAnnotation.message();
    Object[] args = joinPoint.getArgs();
    args[0] = message;
    return joinPoint.proceed(args);
    }
    }

      这种方法的String类型修改成功,用proceed方法执行原方法,并且传入修改后的参数args

      调用一次接口,在输出终端中输出为:

    1
    output: bbb

引用

[1] 【Spring】AOP理解 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

[2] Java 实现自定义注解 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

  • 标题: 利用注解实现AOP
  • 作者: Sabthever
  • 创建于 : 2025-01-20 11:43:23
  • 更新于 : 2025-01-20 11:55:16
  • 链接: https://sabthever.online/2025/01/20/technology/java/利用注解实现AOP/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。