Java自定义注解与AOP的完美融合


Java自定义注解与AOP的完美融合

原创 已于 2025-11-25 14:10:00 修改 · 粉丝可见 · 1.1k 阅读 · 8 · 18 GEO检测 · 编辑
文章链接:https://blog.csdn.net/hacker_51/article/details/145301833

一、引言

在现代Java开发中,面向切面编程(Aspect-Oriented Programming,AOP)已经成为一种重要的编程范式。它允许我们将横切关注点(如日志记录、事务管理、权限校验等)从业务逻辑中分离出来,从而提高代码的可维护性和可扩展性。而自定义注解则是实现AOP功能的一种强大工具。通过自定义注解,我们可以清晰地标识出需要进行切面处理的方法或类,让AOP框架能够根据注解来动态地织入增强逻辑。本文将深入探讨Java自定义注解与AOP的结合,从自定义注解的创建到在AOP中的应用,帮助读者更好地理解和使用这一技术。

二、AOP

AOP概述

AOP : A spect O riented P rogramming(面向切面编程、面向方面编程),其实就是面向特定方法编程。

我们想象一个场景,我们写的系统中的某个部分功能运行较慢,需要定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时。

如果我们对每一个业务方法手动的插入检查,这将会是巨大的工程量,且改动原始方法的过程中也会不小心疏忽。

所以,我们提出 AOP 概念,面对不同的方法,我们重复使用一个功能的时候,我们设计出一个模板套用即可。

AOP依赖

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

AOP 核心概念

连接点 : ProceedingJoinPoint  JoinPoint ,可以被AOP控制的方法(暗含方法执行时的相关信息)

注解通知 : Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

注解切入点 : Pointcut,匹配连接点的条件,通知仅会在切入点方法执行时被应用

类注解切面 :Aspect,描述这个类的通知与切入点的对应关系(通知+切入点)

(注解切入点的参数) 目标对象:Target,通知所应用的对象

视图参考如下:

AOP进阶

通知类型

  1. @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行

  2. @Before :前置通知,此注解标注的通知方法在目标方法前被执行

  3. @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行

  4. @AfterReturning :返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行

  5. @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行

通知顺序

1、不同切面类中,默认按照切面类的类名字母排序

  • 目标方法前的通知方法:字母排名靠前的先执行

  • 目标方法后的通知方法:字母排名靠前的后执行

2、用 @Order(数字) 加在切面类上来控制顺序

  • 目标方法前的通知方法:数字小的先执行

  • 目标方法后的通知方法:数字小的后执行

常见的切入点表达式

1. execution(不太推荐)

execution 是最常用的切入点表达式,用于匹配方法的执行。

2. @annotation 注解

@annotation 用于 匹配那些被指定注解修饰的方法 。它关注的是方法上的注解,而不是类或参数上的注解。

3. @within 注解

@within 用于 匹配那些定义在带有指定注解的类中的所有方法 ,而不仅仅是被注解修饰的方法。

三、Java自定义注解基础

(一)什么是注解

注解是Java语言中的一种元数据形式,它提供了关于程序代码的额外信息,但并不直接影响代码的运行。注解可以应用于类、方法、字段等程序元素上。Java内置了一些注解,如 @Override@Deprecated 等,但Java也允许我们自定义注解。

(二)元注解

在Java中,注解本身也可以被注解修饰,这些用于修饰注解的注解被称为 元注解 (Meta-Annotation)。元注解主要用于定义注解的属性和行为,为注解提供更丰富的语义和功能。Java提供了几种标准的元注解,这些元注解在自定义注解时非常关键。

(1) @Retention

@Retention 元注解用于指定注解的保留策略,即注解在程序的生命周期中可以被保留到什么阶段。它接受一个 RetentionPolicy 枚举值,定义了注解的保留范围:

  • RetentionPolicy.SOURCE :注解仅在源代码阶段保留,编译时会被丢弃,不会进入字节码文件。这种保留策略通常用于编译时的检查或代码生成工具。

  • RetentionPolicy.CLASS :注解在编译阶段保留,会被写入字节码文件,但在运行时无法通过反射访问。这种策略适用于一些需要在字节码层面进行处理的场景。

  • RetentionPolicy.RUNTIME :注解在运行时保留,可以通过反射获取注解的信息。这是AOP和框架(如Spring)中常用的保留策略,因为它们需要在运行时动态地处理注解。

(2) @Target

@Target 元注解用于指定注解可以应用的目标类型。它接受一个 ElementType 枚举值,定义了注解可以修饰的程序元素类型:

  • ElementType.TYPE :注解可以应用于类、接口(包括注解类型)或枚举声明。

  • ElementType.FIELD :注解可以应用于字段(包括枚举常量)。

  • ElementType.METHOD :注解可以应用于方法。

  • ElementType.PARAMETER :注解可以应用于方法或构造器的参数。

  • ElementType.CONSTRUCTOR :注解可以应用于构造器。

  • ElementType.LOCAL_VARIABLE :注解可以应用于局部变量。

  • ElementType.ANNOTATION_TYPE :注解可以应用于注解类型。

  • ElementType.PACKAGE :注解可以应用于包声明。

(3) @Documented

@Documented 元注解用于指定注解是否会被包含在JavaDoc文档中。当一个注解被 @Documented 修饰时,它会在生成的JavaDoc文档中显示。这对于开发框架或库时非常有用,因为它可以帮助开发者更好地理解注解的用途。

(4) @Inherited

@Inherited 元注解用于指定注解是否可以被子类继承。当一个类被某个注解修饰,且该注解被 @Inherited 修饰时,其子类也会继承该注解。需要注意的是, @Inherited 仅对类的继承有效,对方法或字段的继承无效。

(5) @Repeatable

@Repeatable 元注解是Java 8引入的,用于支持注解的重复使用。在默认情况下,一个注解在一个程序元素上只能使用一次。通过 @Repeatable ,可以在同一个程序元素上多次使用同一个注解。

元注解在Java注解体系中起着关键作用,它们定义了注解的生命周期、适用范围、是否可继承、是否可重复等重要属性。通过合理使用元注解,可以为自定义注解赋予更丰富的语义和行为,使其能够更好地满足开发需求。

在实际开发中,元注解是自定义注解的基础,它们帮助开发者明确注解的使用方式和范围,从而提高代码的可读性和可维护性。同时,我们懂得如何使用常用的注解即可。

(三)常见通知注解

通知类别

  • @Befor  前置通知(Before advice)- 在目标方便调用前执行通知

  • @After  后置通知(After advice)- 在目标方法完成后执行通知

  • @AfterReturning  返回通知(After returning advice)- 在目标方法执行成功后,调用通知

  • @AfterThrowing  异常通知(After throwing advice)- 在目标方法抛出异常后,执行通知

  • @Around  环绕通知(Around advice)- 在目标方法调用前后均可执行自定义逻辑

(四)自定义注解登录拦截

根据我上一篇文章 Java拦截器应用_java异常拦截器-CSDN博客 拦截器知识。

我们拦截器有时候调节拦截比较吃力,总是需要微调所拦截的控制类

这次我们学会自定义注解后,可以通过注解方式通知系统我们拦截哪个控制器类,不拦截哪个控制器类

定义登录校验拦截的自定义注解

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

定义无需登录校验拦截的自定义注解

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

定义切面类

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
29
30
31
32
33
34
35
36
37
38
39
40
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class LoginCheckAspect{

// 定义方法切点
@Pointcut("@annotation(com.angindem.aop.interfaces.LoginCheck)")
private void at() {
}

// 定义类方法切点
@Pointcut("@within(com.angindem.aop.interfaces.LoginCheck)")
private void wt() {
}

// 匹配标注有 @Skip 注解的类或方法
@Pointcut("@within(com.angindem.aop.interfaces.SkipLoginCheck) || @annotation(com.angindem.aop.interfaces.SkipLoginCheck)")
public void st() {
}

// 逻辑拦截,当遇到 Skip 注解忽略拦截
@Before("(at() || wt()) && !st()")
public void befor() {
// 登录拦截逻辑
}

@Around("(at() || wt()) && !st()")
public Object after(ProceedingJoinPoint joinPoint) throws Throwable {
// 调用原来方法执行
return joinPoint.proceed();
}
}

通过以上逻辑,可以直接使用 两个 自定义注解灵活拦截。

(五)获取注解上下文信息的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Axin {
/**
* 所属模块
* @return
*/
String module() default "日志模块";

/**
* 动作描述
* @return
*/
String desc() default "无动作";
}

JoinPoint

JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象. 注意: 这用于非环绕通知

方法名 功能
Signature getSignature(); 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Object[] getArgs(); 获取传入目标方法的参数对象
Object getTarget(); 获取被代理的对象
Object getThis(); 获取代理对象

模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Befor
public void doBefore(JoinPoint joinPoint,Axin axin) {
System.out.println("******拦截前的逻辑******");
System.out.println("目标方法名为:" + joinPoint.getSignature().getName());
System.out.println("目标方法所属类的简单类名:" + joinPoint.getSignature().getDeclaringType().getSimpleName());
System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName());
System.out.println("目标方法声明类型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));
//获取传入目标方法的参数
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("第" + (i + 1) + "个参数为:" + args[i]);
}
System.out.println("被代理的对象:" + joinPoint.getTarget());
System.out.println("代理对象自己:" + joinPoint.getThis());

System.out.println("拦截的注解的参数:");
System.out.println(axin.module());
System.out.println(axin.desc());
}

ProceedingJoinPoint

ProceedingJoinPoint对象是JoinPoint的子接口, 该对象只用在@Around的切面方法中

方法名 功能
Object proceed() throws Throwable 执行目标方法
Object proceed(Object[] var1) throws Throwable 传入的新的参数去执行目标方法

测试方法:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//Service接口
public interface AxinService {
String axinRun(String arg1, User user);
}

//实现类
/**
* @author Axin
*/
@Component
public class AxinServiceImpl implements AxinService {

@Axin(module = "print",desc = "打印")
@Override
public String axinRun(String arg1, User user) {
String res = arg1 + user.getName() + user.getAge();
return res;
}

public String axinRun(String arg1, Person person) {
String res = arg1 + person.getName() + person.getAge();
return res;
}
}

//控制类
/**
* @author Axin
*/
@RestController
public class HelloController {
@Autowired
AxinService axinService;

@RequestMapping("/hello")
public String hello() {
User user = new User();
user.setAge(10);
user.setName("张三");
String res = axinService.axinRun("Test:", user);

return "Hello Spring Boot!<br>"+res;
}
}

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
环绕通知:
print
打印
******拦截前的逻辑******
目标方法名为:axinRun
目标方法所属类的简单类名:AxinService
目标方法所属类的类名:com.axin.springboot.service.AxinService
目标方法声明类型:public abstract
第1个参数为:Test:
第2个参数为:User(id=null, name=张三, age=10, date=null)
被代理的对象:com.axin.springboot.service.AxinServiceImpl@ac2ddcc
代理对象自己:com.axin.springboot.service.AxinServiceImpl@ac2ddcc
拦截的注解的参数:
print
打印
******拦截后的逻辑******

本人实践:通过反射获取注解值

注解类:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Before("wt()")
public void beforeClass(JoinPoint joinPoint) {
logger.info("---------- LogAspect 类方法切点 -----------");
// 获取目标类
Class<?> targetClass = joinPoint.getTarget().getClass();
// 获取类上的注解
Log log = targetClass.getAnnotation(Log.class);
PrintLogInfo(log);
Object[] args = joinPoint.getArgs();
for (Object arg : args){
System.out.println(arg.toString());
}
}
注解方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Before("at()")
public void beforeMethod(JoinPoint joinPoint) {
logger.info("---------- LogAspect 方法切点 -----------");
// 获取目标方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取注解
Log log = method.getAnnotation(Log.class);
PrintLogInfo(log);
Object[] args = joinPoint.getArgs();
for (Object arg : args){
System.out.println(arg.toString());
}
}

觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭

微信二维码

wechat pay

支付宝二维码

ali pay

Java自定义注解与AOP的完美融合
http://blog.angindem.cn/2025/11/25/Angindem-CSDN博客/177_177/
作者
Angindem
发布于
2025年11月25日
许可协议