如何通过AOP实现一个具体的注解?


image.png


1- 引言:AOP与注解(WW)

1-1 AOP和注解的概念(What)

什么是AOP

  • 面向切面编程(AOP) 是一种编程范式,它允许开发者将程序中影响多个类的功能分离出来,形成一个独立的模块,这种模块被称为切面(Aspect)。这使得开发者可以将关注点(如日志、事务管理、安全等)从业务逻辑中分离出来,提高代码的可重用性和可维护性。
  • AOP 是 Spring 的核心思想之一,提供了一种代码增强的方式。Spring中的 AOP 是基于动态代理实现的,AOP 切面编程一般可以帮助我们在不修改现有代码的情况下,对程序的功能进行拓展往往用于实现 日志处理权限控制、事务控制 等。

什么是注解

  • 注解(Annotation) 是一种应用于代码的元数据形式,可以用于类、方法或字段上,它为代码提供了额外的信息,无需改变代码本身。
  • 注解的定义:在Java中,注解可以通过使用 @interface关键字来定义。

1-2 为什么要用AOP自定义注解?(Why)

使用AOP实现注解的主要优点包括:

  • 解耦代码:通过使用注解和AOP,可以将非业务逻辑(如安全检查、日志记录)与业务逻辑代码分离,降低系统的耦合度。
  • 提高代码的可维护性:相关横切逻辑集中在一个地方,更易于修改和维护。
  • 增强代码的可读性:通过注解明确标记代码的用途,其他开发者可以更容易理解程序的行为。

2- 核心:怎么用AOP实现自定义注解?(How)

2-1 实现自定义注解的步骤

下面我们先使用 AOP 的方式来实现一个打印日志的自定义注解,它的实现步骤如下:

    1. 添加 Spring AOP 依赖
    1. 创建自定义注解
    1. 创建切面:编写 AOP 拦截(自定义注解)的逻辑代码
    1. 使用自定义注解

2-2 具体实现

① 添加AOP依赖

  • 在 pom.xml 文件中引入 AOP 依赖
<dependencies>
  <!-- Spring AOP dependency -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
</dependencies>

② 创建自定义注解

  • 创建一个新的 Java 注解类,通过 @interface 关键字来定义,并可以添加元注解以及属性。
  • 定义一个名为@Loggable的注解,用于标记需要进行日志记录的方法
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    // 可以定义注解的属性
    String value() default "";
    boolean enable() default true;
}

在上面的例子中,我们定义了一个名为 Loggable 的注解,它有两个属性:value 和 enable,分别设置了默认值。

  • @Target(ElementType.METHOD) 指定了该注解只能应用于方法级别。
  • @Retention(RetentionPolicy.RUNTIME) 表示这个注解在运行时是可见的,这样 AOP 代理才能在运行时读取到这个注解。

③ 创建切面:编写 AOP 拦截(自定义注解)的逻辑代码

  • 这是一个前置通知,它定义了一个切点表达式@annotation(Loggable)
  • 这个表达式指定了通知将织入到所有标记了@Loggable注解的方法上。@Before表示这个通知将在匹配的方法执行之前运行。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("@annotation(Loggable)")
    public void logMethodCall() {
        System.out.println("Method is being called");
    }
}

④ 使用自定义注解

public class SomeService {

    @Loggable
    public void performAction() {
        System.out.println("Performing an action");
    }
}
  • 上述代码的执行结果
    • 这个切面配置了一个前置通知(Before advice),它将在标有@Loggable注解的方法执行之前运行。因此,当performAction方法被调用时,控制台将首先输出切面中定义的日志信息,然后输出方法内部的打印语句。具体的输出结果将是:
Method is being called
Performing an action

2-3 使用场景:自定义注解实现前置参数检查

  • 在我们编写后端接口是,一般会对参数的合法性进行检查。这种业务逻辑可以通过 AOP 实现。
  • 这里我们使用 HibernateValidator 工具 + AOP自定义注解方式实现 前置参数检查

① 待校验的参数添加 NotNull

  • 对需要校验的参数添加 NotNull
public class UserValidator {

    private String username;

    @NotNull(message = "年龄不能为空")
    private Integer age;

    // getter and setter methods
}

② 定义工具类 BeanValidator

  • 使用 Hibernate validator 定义工具类 BeanValidator
  • BeanValidatorvalidateObject 方法使用了 Hibernate Validator 来校验对象的属性是否符合注解定义的约束。
  • 当传递一个 UserValidator 对象到这个方法时,它会自动校验所有带有校验注解的属性,包括 age 属性的 @NotNull 约束。如果 agenull,则会抛出一个 ValidationException,异常信息为 “年龄不能为空”。
public class BeanValidator {

    private static Validator validator = Validation.byProvider(HibernateValidator.class)
                                                   .buildValidatorFactory().getValidator();

    /**
     * @param object object
     * @param groups groups
     */
    public static void validateObject(Object object, Class<?>... groups) throws ValidationException {
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
        if (constraintViolations.stream().findFirst().isPresent()) {
            throw new ValidationException(constraintViolations.stream().findFirst().get().getMessage());
        }
    }
}

③ 自定义注解 ValidateParams

  • 我们定义一个注解 ValidateParams
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateParams {
    
}

④ 创建切面

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ValidationAspect {

    @Pointcut("@annotation(ValidateParams)")
    public void validateParams() {}

    @Before("validateParams()")
    public void beforeMethod(JoinPoint joinPoint) {
        try {
            Object[] args = joinPoint.getArgs();
            if (args != null) {
                for (Object arg : args) {
                    BeanValidator.validateObject(arg);
                }
            }
        } catch (ValidationException e) {
            // Handle the exception, e.g., log it or throw a custom exception
            throw new RuntimeException("Validation failed: " + e.getMessage(), e);
        }
    }
}

⑤ 使用注解

  • 现在,您可以在任何 Spring 管理的 Bean 的方法上使用 @ValidateParams 注解来触发参数校验:
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @ValidateParams
    public void createUser(UserValidator user) {
        // 业务逻辑
    }
}
  • 如果 createUser 方法被调用,但传入的 UserValidator 对象的 age 为 null,例如:
UserValidator user = new UserValidator();
user.setUsername(张三);
user.setAge(null);
  • BeanValidator.validateObject(arg) 将验证这些属性,并且因为 agenull,将抛出 ValidationException
Exception in thread "main" java.lang.RuntimeException: Validation failed: 用户名不能为空
    at com.example.ValidationAspect.beforeMethod(ValidationAspect.java:xx)
    ...

3- 小结:AOP自定义注解小结

1- 如何通过AOP实现一个具体的注解?
回答

在这里插入图片描述


2- 如何通过AOP实现自定义注解?before/after里面代码怎么写?

  • 考点:@Before @After 中的切点表达式。
  • 切点表达式:@annotation(Loggable) 指定了这些通知应当触发在任何被@Loggable注解标记的方法上。这是切点表达式的编写部分。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.JoinPoint;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    // 使用@Loggable注解作为切点
    @Before("@annotation(Loggable)")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before method: " + joinPoint.getSignature().getName());
    }

    @After("@annotation(Loggable)")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("After method: " + joinPoint.getSignature().getName());
    }
}

回答
在这里插入图片描述


相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-28 01:02:02       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-28 01:02:02       106 阅读
  3. 在Django里面运行非项目文件

    2024-04-28 01:02:02       87 阅读
  4. Python语言-面向对象

    2024-04-28 01:02:02       96 阅读

热门阅读

  1. c++后台开发八股文遗漏复习点

    2024-04-28 01:02:02       30 阅读
  2. 薪酬构成要素:合理配置,满足员工需求

    2024-04-28 01:02:02       34 阅读
  3. 5. HTTPS的特点

    2024-04-28 01:02:02       36 阅读
  4. LeetCode热题Hot100 - 最长有效括号

    2024-04-28 01:02:02       32 阅读
  5. 力扣经典150题第四十题:同构字符串

    2024-04-28 01:02:02       33 阅读
  6. 使用Spring和MyBatis构建流浪猫狗救助网站

    2024-04-28 01:02:02       38 阅读
  7. 力扣1518. 换水问题

    2024-04-28 01:02:02       36 阅读
  8. 【leetcode】对撞指针题目总结

    2024-04-28 01:02:02       33 阅读
  9. OpenCV如何使用分水岭算法进行图像分割

    2024-04-28 01:02:02       36 阅读
  10. 【贪心算法】Leetcode 55. 跳跃游戏【中等】

    2024-04-28 01:02:02       33 阅读
  11. 【运维】Gitlab备份

    2024-04-28 01:02:02       35 阅读
  12. 探究C++20协程(4)——协程中的调度器

    2024-04-28 01:02:02       25 阅读