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 的方式来实现一个打印日志的自定义注解,它的实现步骤如下:
-
- 添加 Spring AOP 依赖
-
- 创建自定义注解
-
- 创建切面:编写 AOP 拦截(自定义注解)的逻辑代码
-
- 使用自定义注解
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
BeanValidator
的validateObject
方法使用了 Hibernate Validator 来校验对象的属性是否符合注解定义的约束。- 当传递一个
UserValidator
对象到这个方法时,它会自动校验所有带有校验注解的属性,包括age
属性的@NotNull
约束。如果age
为null
,则会抛出一个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)
将验证这些属性,并且因为age
为null
,将抛出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());
}
}
回答