一、概念
AOP:面向切面编程,是相对于OOP面向对象编程而言的。可以对一个对象里的某一个或某一些方法进行增强。
Spring的AOP的存在目的是为了解耦。AOP可以让一组类共享相同的行为。在OOP中只能通过继承类和实现接口来实现,但是这样的缺点是会使代码的耦合度增加,
且类继承只能为单继承,阻碍更多行为添加到一组类上,AOP的出现弥补了OOP的不足。
AOP是Spring
框架中的一个重要内容,在Spring
boot
里配置 AOP非常简单,Spring
Boot
对AOP
的默认配置属性是开启的,也就是说 spring.aop.auto
属性的值默认是true
,
我们只要引入了AOP
依赖后,默认就已经增加了@EnableAspectJAutoProxy
功能,不需要我们在程序启动类上面加入注解@EnableAspectJAutoProxy。
Spring支持AspectJ的注解式切面编程:
(1) 使用@Aspect
注解在类上声明该类是一个切面。
(2) 使用@After
,@Before
,@Around
定义建言(advice
),可直接将拦截规则(切点)作为参数。
(3) 其中@After
,@Before
,@Around
参数的拦截规则为切点(PointCut
),为了使切点复用,可使用@PointCut
专门定义拦截规则,然后在@After
,@Before
,@Around
的参数中调用。
(4) 其中符合条件的每一个被拦截处为连接点(JoinPoint
)。
核心概念
术语 | 描述 |
---|---|
目标对象(Target object) | 目标类、需要被代理的类。如下面的 UserServiceImp |
连接点(Join point) | 所谓连接点是指那些可能被拦截的方法。如目标类的所有方法。 |
切入点(Pointcut) |
已经增强的连接点。这是一组一个或多个连接点,其中应该执行通知(Advice)。指的是实际被拦截的方法。 一个切面并不需要通知应用的所有连接点,切点有助于缩小切面所通知的连接点范围。因此,切点其实就是定义了需要执行在哪些连接点上执行通知。 我们将在AOP示例中看到。用于定义哪个方法会被拦截, 例如: |
通知(Advice) | 这是在方法执行之前或之后采取的实际操作。 这是在Spring AOP框架的程序执行期间调用的实际代码片段。 |
织入(Weaving) |
编织是指将切面连接到其他应用程序或对象身上、并进行创建通知(Advice)对象的过程。 这可以在编译时,加载时间或运行时完成。 织入是把切面应用到目标对象并创建新的代理对象的过程。 |
引入(Introduction) | 介绍允许向现有类添加新的方法或属性。 |
切面(Aspect) |
切入点 和 通知 的结合。一个线是一个特殊的面。 一个切入点和一个通知,组成一个特殊的面。 例如,日志记录模块被称为AOP方面用于记录。应用程序可以根据需要具有任意数量的方面。 |
常用的Pointcut定义有 execution 和 @annotation 两种。
execution 定义对方法无侵入,用于实现比较通用的切面。
@annotation 可以作为注解加到特定的方法上,例如Spring的Transaction注解。
execution切点定义应该放在一个公共的类中,集中管理切点定义。
通知类型
术语 | 描述 |
---|---|
@Before(前置通知) | 在方法执行之前运行通知 |
@After(后置通知) | 在方法执行后运行通知,无论其结果如何 |
@After-returning(返回通知) | 只有方法成功完成后才能在方法执行后运行通知 |
@After-throw(异常通知) | 只有在方法通过抛出异常而退出方法执行之后才能运行通知 |
@Around(环绕通知) | 在调用通知方法之前和之后运行通知,可以停止执行方法或者继续执行,也可以更改方法返回的参数。 |
二、代码展示
下面将通过模拟记录操作的日志系统的实现来演示基于注解拦截和基于方法规则拦截两种方式。
其中注解式拦截能够很好的控制要拦截的粒度和获得更加丰富的信息,Spring
本身在事务处理(@Transaction
)和数据缓存(@Cacheable
等)上面都使用此种形式的拦截。
1、添加依赖
<!-- aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2. 编写拦截规则的注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogAnnotion { String executeName() default ""; }
3、编写切面
@Component @Aspect public class AspectProxy { @Pointcut("@annotation(com.springboot.com.common.annotion.LogAnnotion)") public void aspectExecute() { } /* @After前置通知--在方法执行之前执行Advice,常用于验签、鉴权等 记录拦截URL 操作名称 拦截数据 */ @Before("aspectExecute()") public void doBefore(JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); LogAnnotion annotation = method.getAnnotation(LogAnnotion.class); System.out.println("After前置通知--------------Begin"); System.out.println("URL拦截方法: " + signature.getName()); System.out.println("操作名称: " + annotation.executeName()); System.out.println("拦截数据: " + JacksonUtil.serialize(args)); System.out.println("After前置通知--------------End"); } /* @After后置通知--在方法执行完成后执行,无论是执行成功还是抛出异常. 记录拦截URL 操作名称 拦截数据 */ @After("aspectExecute()") public void doAfter(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); LogAnnotion annotation = method.getAnnotation(LogAnnotion.class); System.out.println("After后置通知-----------------Begin"); System.out.println("URL拦截方法: " + signature.getName()); System.out.println("操作名称: " + annotation.executeName()); Object[] args = joinPoint.getArgs(); System.out.println("拦截数据: " + JacksonUtil.serialize(args)); System.out.println("After后置通知-----------------End"); } /* @AfterReturning--在方法执行完成后执行,仅在方法执行成功后执行. @Param:returning方法返回值 记录拦截URL 操作名称 拦截数据 */ @AfterReturning(value = "aspectExecute()", returning = "retValue") public void doAfterReturning(JoinPoint joinPoint, Object retValue) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); LogAnnotion annotation = method.getAnnotation(LogAnnotion.class); System.out.println("AfterReturning--在方法执行完成后执行-----------------Begin"); System.out.println("URL拦截方法: " + signature.getName()); System.out.println("操作名称: " + annotation.executeName()); System.out.println("拦截数据: " + JacksonUtil.serialize(retValue)); System.out.println("AfterReturning--在方法执行完成后执行-----------------End"); } /* @AfterThrowing异常通知--仅在方法执抛出异常后执行. @Param:returning方法返回值 记录拦截URL 操作名称 拦截数据 */ @AfterThrowing(value = "aspectExecute()", throwing = "e") public void doAfterThrowing(JoinPoint joinPoint, Throwable e) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); LogAnnotion annotation = method.getAnnotation(LogAnnotion.class); System.out.println("AfterThrowing异常通知-----------Begin"); System.out.println("URL拦截方法: " + signature.getName()); System.out.println("操作名称: " + annotation.executeName()); System.out.println("异常信息: " + e); Object[] args = joinPoint.getArgs(); System.out.println("拦截数据: " + JacksonUtil.serialize(args)); System.out.println("AfterThrowing异常通知-----------End"); } /* @Around--环绕通知:在方法执行前和执行后都会执行 @Param:returning方法返回值 记录拦截URL 操作名称 拦截数据 */ @Around(value = "aspectExecute()") public void doAround(ProceedingJoinPoint joinPoint) throws Throwable { try { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); LogAnnotion annotation = method.getAnnotation(LogAnnotion.class); System.out.println("Around--环绕通知-------------------开始。。。"); System.out.println("URL拦截方法: " + signature.getName()); System.out.println("操作名称: " + annotation.executeName()); Object[] args = joinPoint.getArgs(); System.out.println("拦截数据: " + JacksonUtil.serialize(args)); Object obj = joinPoint.proceed(); //执行目标方法,得到方法返回值 System.out.println("方法执行后返回值:" + obj); System.out.println("Around--环绕通知-------------------结束。。。"); } catch (Throwable ex) { ex.printStackTrace(); throw ex; } finally { System.out.println("around"); } } }
切面类说明:
①通过@Aspect
注解声明该类是一个切面。
②通过@Component
让此切面成为Spring
容器管理的Bean
。
③通过@Pointcut
注解声明切面。
④通过@After
注解声明一个建言,并使用@Pointcut
定义的切点。
⑤通过反射可以获得注解上面的属性,然后做日志记录相关的操作,下面的相同。
⑥通过@Before
注解声明一个建言,此建言直接使用拦截规则作为参数。
调用 Controller 方法
@RestController public class IndexController { @Resource private IUserService userService; /* 异常测试 */ @RequestMapping("/ExceptionTest") public String ExceptionTest() { int row = userService.ExceptionTest(1000); return row > 0 ? "ok" : "fail"; } } public interface IUserService { /* 异常测试 */ int ExceptionTest(int i); } @Service public class UserServiceImp implements IUserService { /* 异常测试 */ @LogAnnotion(executeName = "异常测试Action") public int ExceptionTest(int i) { //制造异常 int a = Integer.getInteger("a"); return 1; } }
执行结果:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决