SpringBoot 利用自定义注解实现AOP拦截控制
在 SpringBoot 中,利用 AOP 实现拦截控制的方法有很多,个人觉得相对比较简洁、比较简单的方式是通过自定义注解实现拦截控制。这种实现方式只需要预先定义一个新的注解,并实现拦截控制的具体业务逻辑,当我们想要拦截某一个方法进行控制时,只需要在方法前加上该注解,通常不需要做过多的调整。在实际工程应用中,这种实现方式确实有效提升了开发效率。AOP 的基本概念AOP 是 Aspect-orien
在 SpringBoot 中,利用 AOP 实现拦截控制的方法有很多,个人觉得相对比较简洁、比较简单的方式是通过自定义注解实现拦截控制。这种实现方式只需要预先定义一个新的注解,并实现拦截控制的具体业务逻辑,当我们想要拦截某一个方法进行控制时,只需要在方法前加上该注解,通常不需要做过多的调整。在实际工程应用中,这种实现方式确实有效提升了开发效率。
AOP 的基本概念
AOP 是 Aspect-oriented Programming 的缩写,常译作”面向切面编程“,通俗理解就是在程序运行的链路中,从某一个横切面介入业务,进行拦截后实现自定义的业务,然后再恢复原有的业务链路。这是一种程序设计模式,实际上 SpringBoot 的设计在很多地方都使用了这个模式。
与 AOP 相关的常见概念还包括:
-
注解@Aspect:将标记的类作为一个切面供容器进行读取,该切面包含 Point Cut(切点)和 Joint Point(连接点)。
-
Point Cut(切点):切面标记的位置
-
Joint Point(连接点):被拦截的目标方法,即原来的实现方法
-
Advice(增强):在 Advice 中定义了 Point Cut 需要完成的动作,包括在切点 Before、After、替代切点自身执行的代码模块。
- @Around:环绕方法。贯穿整个方法执行的前后,可以决定是否执行,如何执行,执行后如何操作等。
- @Before:在被拦截的目标方法执行前的动作,可以修改或者替换方法的入参。
- @After:在切点方法执行后的动作。
- @AfterReturning:在切点方法成功执行后的动作,可以对返回结果进行拦截修改。
- @AfterThrowing:在切点方法执行中抛出了异常,可以抛出异常后的流程进行处理。
几个注解方法的增强顺序依次为 Around, Before, After, AfterReturning, AfterThrowing。
基本实现流程
引入maven依赖
在 pom.xml 中引入响应的依赖模块:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
新建一个自定义注解类
类实现代码大致如下:
package com.common.util.annotation;
import java.lang.annotation.Documented;
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)
@Documented
public @interface AopAnnotation {
// 注解标识值
String value() default "";
// 描述信息
String description default "";
}
这里需要指定注解的目标为方法,根据实际需求,注解的目标也可以是参数或者类,在这里的例子展示我们暂时只指定方法。同样,指定注解在运行时生效。这个自定义注解就可以用来注解指定切点了。
定义 Aspect 切面类
这里说的切面类,就是通过上述注解拦截方法前后,执行业务逻辑控制的实现类。为了将切面类注入容器,需要在类的前面分别加上注解@Aspect 和 @Component。
以下是常用的实现模式,除了@Pointcut标注的方法是必需之外,其他方法都是可选的。该标注方法指定了当前 AOP 方法面向的切点路径,即通过指定的注解明确切点路径。
@Aspect
@Component
public class DemoAspect {
// Service层切点
@Pointcut("@annotation(aop.AopDemoAnnotation)")
public void servicePointcut() {
System.out.println("Pointcut: 不会被执行");
}
// 切点方法执行前运行
@Before(value = "servicePointcut()")
public void doBefore(JoinPoint joinPoint) {
System.out.println("Before 方法执行 - Start time: " + System.currentTimeMillis());
}
@Around("servicePointcut()")
public void doAround(ProceedingJoinPoint jointPoint) throws Throwable {
System.out.println("Around 方法开始");
// 获取当前访问的class类及类名
Class<?> clazz = jointPoint.getTarget().getClass();
String clazzName = jointPoint.getTarget().getClass().getName();
// 获取访问的方法名
String methodName = jointPoint.getSignature().getName();
// 获取方法所有参数及其类型
Object[] args = jointPoint.getArgs();
Class[] argClz = ((MethodSignature) jointPoint.getSignature()).getParameterTypes();
// 获取访问的方法对象
Method method = clazz.getDeclaredMethod(methodName, argClz);
// 判断当前访问的方法是否存在指定注解
if (method.isAnnotationPresent(AopDemoAnnotation.class)) {
AopDemoAnnotation annotation = method.getAnnotation(AopDemoAnnotation.class);
// 获取注解标识值与注解描述
String value = annotation.value();
String desc = annotation.description();
// 执行目标方法
Object proceed = jointPoint.proceed();
System.out.println("打印方法是执行返回结果:" + proceed.toString());
}
System.out.println("Around 方法结束");
}
// 切点方法执行后运行,不管切点方法执行成功还是出现异常
@After(value = "servicePointcut()")
public void doAfter(JoinPoint joinPoint) {
System.out.println("After 方法执行 - Finish time: " + System.currentTimeMillis());
}
// 切点方法成功执行返回后运行
@AfterReturning(value = "servicePointcut()", returning="returnValue")
public void doAfterReturning(JoinPoint joinPoint, Object returnValue) {
System.out.println("AfterReturning 方法执行 - Returned value: " + returnValue);
}
// 切点方法成功执行返回后运行
@AfterThrowing(value = "servicePointcut()", throwing="throwing")
public void doAfterThrowing(JoinPoint joinPoint, Object throwing) {
System.out.println("AfterThrowing 方法执行 - throwing value: " + throwing);
}
}
以上就是针对切点常用的几种增强方法,具体规则在注释中都有说明。其中,servicePointcut() 这个空方法相当于是一个标记,用来与被注解的服务方法进行关联,作为其他增强方法的注解 value。在 doAround() 方法中,列举了一些常用的获取相关对象的方法,这里通过成员变量方法获取注解的属性值,有另外一种写法可以直接将注解所属的类对象作为增强方法入参,具体如下:
@Aspect
@Component
public class DemoAspect {
// Service层切点
@Pointcut("@annotation(com.common.util.annotation.AopAnnotation)")
public void servicePointcut() {}
// 入参命名要跟 @annotation里的命名一致
@Around(value = "servicePointcut() && @annotation(anno)")
public void doAround(ProceedingJoinPoint jointPoint, AopAnnotation anno) throws Throwable {
// 获取注解标识值与注解描述
String value = annotation.value();
String desc = annotation.description();
// 执行目标方法
proceed = jointPoint.proceed();
}
}
这种方式直接把注解作为入参带入到方法当中,不过需要注意的是,在诸如 @Around 注解里面的 @annotation 注解的值,必须要跟注解入参的命名一致,这里为了便于说明,我都命名为 “anno”。如果命名不同,程序可能会无法获取到该注解对应的对象,造成业务异常。
更多推荐
所有评论(0)