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进阶 通知类型
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before :前置通知,此注解标注的通知方法在目标方法前被执行
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning :返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@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 () { } @Pointcut ("@within(com.angindem.aop.interfaces.SkipLoginCheck) || @annotation(com.angindem.aop.interfaces.SkipLoginCheck)" ) public void st () { } @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 { String module () default "日志模块" ; 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()); } }