AOP
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,使得我们能够更清晰、更模块化地组织和维护代码。
在传统的面向对象编程中,我们主要关注业务逻辑的实现,例如类的行为、方法的执行流程等。然而,很多应用都包含一些横切关注点,例如日志记录、事务管理、安全控制、性能优化等,这些关注点不属于核心业务逻辑,但却分布在整个代码基底。AOP 的目标就是将这些横切关注点模块化,使得我们能够更好地维护和管理它们。
AOP 主要通过两个概念来实现:切面(Aspect)和连接点(Join Point)。
- 切面(Aspect): 切面是一个模块化单元,它封装了横切关注点的代码。切面定义了在何处(连接点)以及如何应用横切关注点。在 AOP 中,切面可以包括通知(Advice)、切点(Pointcut)和引介(Introduction)等。
- 连接点(Join Point): 连接点是在应用中程序执行的点,例如方法的执行、异常的处理等。切面通过定义切点来选择连接点,然后在这些连接点上应用通知,从而插入横切关注点的逻辑。
AOP 的一些关键概念:
- 通知(Advice): 通知是切面的具体行为,它定义了在连接点上执行的代码。常见的通知类型包括前置通知(在连接点之前执行代码)、后置通知(在连接点之后执行代码)、环绕通知(在连接点前后执行代码,可以控制连接点的执行)、异常通知(在连接点抛出异常时执行代码)等。
- 切点(Pointcut): 切点定义了一组连接点的集合,通知被应用到这些连接点上。切点通过表达式或者是通过指定方法名来定义。
- 引介(Introduction): 引介允许我们在不修改类结构的情况下,向类添加新的方法或属性。
AOP 的主要优势在于它提高了代码的模块化程度,将横切关注点从核心业务逻辑中解耦,使得代码更易于理解和维护。在 Java 中,Spring 框架提供了强大的支持,通过 Spring AOP 可以轻松实现面向切面的编程。
配置maven
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10</version> <!-- 使用适当的版本号 -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.10</version> <!-- 使用适当的版本号 -->
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version> <!-- 使用适当的版本号 -->
</dependency>
基本使用方法
一个bean中有不同的方法↓
package dao;
import org.springframework.stereotype.Repository;
//使其成为bean
@Repository
public class BookDao {
//func1在内部实现了输出当前时间的功能
public void func1(){
System.out.println(System.currentTimeMillis());
System.out.println("this is func1");
}
//func2要通过AOP的方式实现
public void func2(){
System.out.println("this is func2");
}
}
创建通知类,实现相应效果。
package aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
//定义为可识别的bean
@Component
//定义为AOP类型
@Aspect
public class MyAdvice {
@Pointcut("execution(void dao.BookDao.func2())")
//切入点
private void pt(){}
//提供绑定
@Before("pt()")
//这是一个通知类
public void method(){
System.out.println(System.currentTimeMillis());
}
}
对Spring进行配置,创建配置类。
package config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
//配置文件
@Configuration
@ComponentScan({"aop","config", "dao"})
@EnableAspectJAutoProxy
public class SpringConfig {
}
调用实现
import config.SpringConfig;
import dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.func1();
bookDao.func2();
}
}
工作流程
- 目标对象方法调用: 首先,程序执行到某个连接点,通常是目标对象(Target Object)的方法调用。这是 AOP 中切面要织入的具体业务逻辑的执行起点。
- 切面的匹配: 在连接点上,AOP 框架会检查所有定义的切面,看哪些切面的切点匹配当前的连接点。切点定义了切面要应用的位置。
- 通知的执行: 一旦切面的切点匹配了连接点,相应的通知就会被执行。通知是切面中具体的业务逻辑,包括前置通知、后置通知、环绕通知等。不同类型的通知会在不同的时机执行。
- 目标对象方法的继续执行(对于环绕通知): 如果切面是环绕通知,它在执行自己的逻辑后需要决定是否调用连接点(目标对象方法)。在环绕通知中,连接点的执行由切面来控制。
切入点表达式
切入点表达式的一般结构包括以下几个部分:
- execution: 这是关键字,指定匹配方法执行的切入点。在 Spring AOP 中,通常以
execution
关键字开始。 - 返回类型: 用一个通配符
*
表示匹配任意返回类型的方法。如果你想具体指定返回类型,可以使用类的全限定名。 - 包路径: 表示要匹配的包路径,可以使用通配符
*
匹配任意包名,也可以指定具体的包路径。 - 类名: 使用
*
匹配任意类名,如果要匹配特定的类,可以指定类的名称。 - 方法名: 使用
*
匹配任意方法名,如果要匹配特定的方法,可以指定方法的名称。 - 参数列表: 使用
(..)
表示匹配任意参数列表。如果要匹配特定类型和顺序的参数,可以在括号内指定。
切入点表达式的一般结构如下:
execution([返回类型] [包路径].[类名].[方法名]([参数列表]))
其中,方括号中的部分可以根据需要省略,使用通配符或具体指定。以下是一些例子:
execution(* com.example.service.*.*(..))
: 匹配com.example.service
包下所有类的所有方法。execution(public * com.example.service.UserService.*(..))
: 匹配com.example.service.UserService
类中所有公共方法。execution(* com.example.service.*.find*(Long))
: 匹配com.example.service
包下所有类中以 “find” 开头且接受一个 Long 类型参数的方法。
通知类型
在 AOP 中,通知是切面中具体行为的定义,它指定了在连接点(方法执行、异常抛出等)上执行的代码。通知类型主要包括以下几种:
前置通知(Before Advice): 在连接点方法执行之前执行的通知。可以用于执行一些准备工作或者验证操作。
@Before("execution(* com.example.service.*.*(..))") public void beforeAdvice() { // 在连接点方法执行之前执行的逻辑 }
后置通知(After Returning Advice): 在连接点方法成功执行之后执行的通知。可以用于执行一些清理或记录日志等操作。
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result") public void afterReturningAdvice(Object result) { // 在连接点方法成功执行之后执行的逻辑,可以访问连接点方法的返回值 }
==环绕通知(Around Advice): ===在连接点方法前后执行的通知,可以完全控制连接点方法的执行。需要手动调用连接点方法。
@Around("execution(* com.example.service.*.*(..))") //最常用,记得抛出异常,创建返回值 public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { // 在连接点方法执行之前执行的逻辑 Object result = joinPoint.proceed(); // 手动调用连接点方法 // 在连接点方法执行之后执行的逻辑 return result; }
异常通知(After Throwing Advice): 在连接点方法抛出异常时执行的通知。可以用于处理异常情况。
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception") public void afterThrowingAdvice(Exception exception) { // 在连接点方法抛出异常时执行的逻辑,可以访问连接点方法抛出的异常 }
最终通知(After Advice): 无论连接点方法是否抛出异常,都会执行的通知。常用于执行一些收尾工作,比如资源释放。
@After("execution(* com.example.service.*.*(..))") public void afterAdvice() { // 无论连接点方法是否抛出异常,都会执行的逻辑 }