1、AOP初识
1.1 AOP概念
- AOP(Aspect Oriented Programming)即:面向切面编程,是一种软件设计模式,以横向切割的方式来解耦系统中的关注点。
- AOP最早是由AOP联盟首次体出的一种编程范式,它旨在提供一种横切关注点(cross-cutting concern)为中心的开发方式,以便将关注点从主要业务逻辑中分离出来,进而提高代码的组织结构和可维护性。
- Spring框架将AOP思想引入到了自己的框架中,通过定义一套规范与实现机制,使开发者能够方便地利用AOP技术来管理和维护程序。Spring AOP主要通过两种方式实现AOP地应用:预编译方式和运行期间动态代理。
- Spring AOP 底层采用动态代理模式实现,动态代理又分为:JDK的动态代理和CGLib的动态代理。
- 可以这么说:AOP是动态代理的一种规范化。
AOP引入原因
- 传统的面向对象编程(OOP)通过将相关功能封装到类中,使得代码更加模块化和可维护。然而,当系统中的某些关注点(如日志、事务、安全等)需要横跨多个类时,OOP的封装机制会变得复杂且难以维护。
- AOP通过将这些横切关注点成为切面(Aspect),并将他们与主页午逻辑进行分离,实现了关注点的重用和集中管理。切面是一组跨越多个对象的通用行为集合,可以通过切点(Pointcut)来定位横切的位置,并通过增强(Advice)在指定的切点上执行额外的行为。
AOP思想概念
1.2 AOP的实现方式
AOP思想实现框架主要有两个:Spring AOP和AspectJ。
1.3 Spring AOP和AspectJ的关系
Spring AOP
- Spring内部实现了AOP规范,主要是在事务处理时使用Spring的AOP功能;
- 项目开发中比较少使用Spring的AOP实现,因为Spring AOP相对比较笨重;
- Spring AOP 只能代理实现了接口的目标类,而对于没有实现接口的目标类,则无法应用AOP;
- Spring中已经集成了AspectJ的框架。
AspectJ
- AspectJ是一个用Java实现的AOP框架;
- AspectJ是一个开源的专门做AOP的框架,是业内最专业的AOP框架,AspectJ是目前实现AOP框架中最成熟的;
- AspectJ通过编译时和运行时的织入来实现切面逻辑;
- 相比于Spring AOP而言,AspectJ更强大。
Spring AOP和AspectJ的关系
- AspectJ是更强的AOP框架,是实际意义的AOP标准;
- Spring整合了AspectJ,他们之间应该是互补而不是竞争的关系;
- AspectJ是注解风格,更胜于Spring XML配置方式,所以Spring AOP使用了和AspectJ一样的注解,并使用AspectJ来做切入点解析和匹配。但AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ的编译器或织入器(weaver);
- 使用Spring AOP时,可以通过配置和使用AspectJ切面来实现更复杂的切面逻辑,而无需引入AspectJ的完整框架;
- Spring AOP与AspectJ的集成主要基于注解的方式,能够方便地将AspectJ的切面应用于Spring的Bean上。
Spring AOP和AspectJ使用选择
- Spring AOP更易用,AspectJ更强大;
- 如果你的项目需要使用AOP来处理更复杂的切面逻辑,或者需要在没有实现接口的类上使用AOP,那么引入AspectJ可能会更适合你的需求;
- 个人更推荐使用Aspect注解方式。
1.4 AOP的应用方面
AOP用于在OOP中模块化横切关注点,使其具有更灵活的功能。
AOP的应用方面
- 日志记录:这是一个关注所有方法的典型横切关注点,例如,当方法开始/结束时,我们需要在控制台上打印消息;
- 事务管理:每个数据库事务都需要清晰的界定脚本或者使用界定脚本;
- 认证和授权:在执行任何方法之前,通常要通过认证用户并授权用户访问特定资源;
- 性能监控:例如,某些方法可能运行得更慢,我们可能要跟踪他们,以确定问题是否仍然存在;
- 缓存:缓存涉及对象存储在内存中的副本,以便下次对此对象的请求可以更快地满足;
- 异常处理:某些应用程序可能需要全局异常处理。这样的例子是,一个方法抛出异常,我们可能想要记录这个异常并通知某人。
1.5 AOP术语
AOP属于图解
- 切面(Aspect):模块化的关注点。也就是代码应用的地方,可以认为是通知、引入和切入点的组合;
- 连接点(Join point):在程序执行过程中的某一点,比如方法被调用或者抛出异常的时候。在Spring AOP中,连接点总是代表方法的执行;
- 通知(Advice):在切面的某个特定的连接点上执行的动作。主要有前置通知、后置通知、最终通知、异常通知和环绕通知;
- 切入点(Pointcut):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行;
- 目标对象(Target Object):被通知的对象。也被称为被代理的对象;
- 代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(通知方法的执行等);
- 织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这可以在编译时、类加载时和运行时完成。
通知类型
- 前置通知(Before advice):通知在连接点之前执行,但是不能阻止执行流程继续进行到连接点;
- 后置通知(After returning advice):通知在连接点之后执行,就算连接点抛出异常也会执行;
- 最终通知(After (finally) advice):通知在连接点退出的时候执行;
- 异常通知(After throwing advice):通知只有在连接点抛出异常后才会执行;
- 环绕通知(Around advice):通知再连接点之前和之后都会执行。这是最强大的通知类型,可以在方法调用前后自定义一段代码。控制权完全在开发者手上,可以决定是继续执行连接点还是直接返回,或者抛出异常等。
2、AOP简单案例
在了解AOP的各项使用方法前,我们先通过一个简单的案例,把AOP理论实践一下,也能加深对AOP的理解。
需求
- 接口有个sayOne()方法,打印一句话:这是One语句;
- 我们通过AOP,在sayOne()方法执行前置通知,打印一句话:这是Before语句;
- 同时,我们通过AOP,在sayOne()方法执行后执行后置通知,打印一句话:这是After语句;
- sayOne()方法原有结构不变,在不改变原有功能情况下,增加功能。
2.1 引入依赖
<!-- spring Context 上下文模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
<!-- spring AOP -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!-- spring单元测试自动注入bean -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.16</version>
</dependency>
Spring AOP 内置了AspectJ的支持,但如果你需要使用更高级或更灵活的AOP功能,你可以选择使用完整的AspectJ框架。
以下是AspectJ完整框架依赖(暂不加入):
<!-- AspectJ的运行时包,提供了AOP相关注解的支持 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.6</version>
</dependency>
<!-- AspectJ的织入包,提供了切入点表达式的支持。 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
2.2 创建接口并实现
// 接口
public interface AopService {
public void sayOne();
}
// 实现类
@Service
public class AopServiceImpl implements AopService {
@Override
public void sayOne() {
System.out.println("这是One语句");
}
}
2.3 创建切面
// 定义为切面类,并交给Spring容器管理
@Component
@Aspect
public class AopAspect {
@Pointcut("execution(public * say*(..))")
public void pointcutName(){
/**
* 事实上,这个方法的实际内容会被忽略,Spring AOP不会调用此方法,
* 所以在@Pointcut注解的方法内部添加的代码不会执行。
*/
System.out.println("这是切点,内容通常都是空方法,因为这里不会执行");
}
@Before("execution(public * say*(..))")
public void sayBefore(){
System.out.println("这是Before语句");
}
@After("pointcutName()")
public void sayAfter(){
System.out.println("这是After语句");
}
}
2.4 创建配置
@Configuration // 声明配置类
@EnableAspectJAutoProxy(proxyTargetClass=true) // 开启AOP注解支持,并使用CGLib代理
@ComponentScan("com.xygalaxy") // 扫描整个包,包括接口和切面
public class AopConfig {
}
2.5 测试验证
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AopConfig.class)
public class AopController {
@Autowired
private AopServiceImpl aopService;
@Test
public void testAop(){
// ApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
// AopServiceImpl aopService = ac.getBean(AopServiceImpl.class);
aopService.sayOne();
}
}
/**
* 输出结果:
* 这是Before语句
* 这是One语句
* 这是After语句
*/
3、AOP配置方式
3.1 Spring的XML配置方式
Spring提供的XML配置方式比较麻烦,实际项目中用的比较少,以前的SSM项目比较常用这种配置方式。
下面通过XML配置方式,将AOP简单案例的代码重新实现一遍。
AopService
依赖和AopService同上面案例一样
// 接口
public interface AopService {
public void sayOne();
}
// 实现类
public class AopServiceImpl implements AopService {
@Override
public void sayOne() {
System.out.println("这是One语句");
}
}
创建切面,这次不用加任何注释
public class AopXmlAspect {
public void pointcutName(){
System.out.println("这是切点,内容通常都是空方法,因为这里不会执行");
}
public void sayBefore(){
System.out.println("这是Before语句");
}
public void sayAfter(){
System.out.println("这是After语句");
}
}
1
创建aopSpring.xml配置文件
注意beans加了很多配置。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 包扫描 -->
<context:component-scan base-package="com.xygalaxy" />
<!-- aop开启 -->
<aop:aspectj-autoproxy/>
<!-- 目标类 -->
<bean id="aopServiceImpl" class="com.guyk.service.impl.AopServiceImpl"></bean>
<!-- 切面 -->
<bean id="aopAspect" class="com.guyk.aspect.AopXmlAspect"></bean>
<!-- aop切面配置 -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect ref="aopAspect">
<!-- 配置切入点 -->
<aop:pointcut id="pointcutName" expression="execution(public * say*(..))"/>
<!-- 前置通知 -->
<aop:before method="sayBefore" pointcut-ref="pointcutName"/>
<!-- 后置通知 -->
<aop:after method="sayAfter" pointcut-ref="pointcutName"/>
</aop:aspect>
</aop:config>
</beans>
测试验证
获取配置的Bean对象,并调用方法
@Test
public void testAop2(){
ApplicationContext ac = new ClassPathXmlApplicationContext("aopSpring.xml");
AopServiceImpl service = ac.getBean("aopServiceImpl", AopServiceImpl.class);
service.sayOne();
}
/**
* 输出结果:
* 这是Before语句
* 这是One语句
* 这是After语句
*/
3.2 Spring基于AspectJ注解配置方式
由于基于XML的声明式AspectJ存在一些缺点,主要是需要在Spring的配置文件中编写大量的代码信息,Spring引入了@AspectJ框架,提供了一套注解来简化AOP的实现。
通过这套注解,我们可以更方便地在代码中直接标注需要进行AOP处理的位置,而不是在配置文件中进行大量的配置,从而提升了代码的可读性和易用性。
AOP简单案例中就是基于AspectJ注解配置方式实现的,这里不再展示案例,对于所有的AOP注解案例,后面都有。
注解 | 说明 |
---|---|
@Aspect |
用于将一个java类定义为一个切面类,表示这个类将包含AOP的切点和通知。 |
@Pointcut |
用于定义切入点,标注方法的返回值为void且方法体通常不包含任何代码。 |
@Before |
用于指定一个前置通知,即在目标方法执行前执行的通知。它需要一个切入点表达式作为参数, |
@After |
用于指定一个后置通知(也被称为最终通知),即无论目标方法是否成功完成,都会执行的通知。 |
@AfterReturning |
用于指定一个返回后通知,即在目标方法成功执行后执行的通知。可以访问到方法的返回值。 |
@AfterThrowing |
用于指定一个异常抛出后通知,即在目标方法因为抛出异常而退出时执行的通知。 |
@Around |
用于指定一个环绕通知,即在目标方法执行前后都执行的通知。 |
使用AspectJ框架实现AOP步骤:参考上面的AOP简单案例【注意引入的AspectJ框架依赖】
<!-- AspectJ的运行时包,提供了AOP相关注解的支持 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.6</version>
</dependency>
<!-- AspectJ的织入包,提供了切入点表达式的支持。 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
3.3 JDK代理方式
JDK代理方式是Spring默认的AOP代理。只能针对实现了接口的类生成代理,否则会抛出异常。这种方式通过在运行时创建一个接口的实现类来完成对目标对象的代理。创建的代理对象可以将所有调用重定向到原始对象,这允许框架通过代理对象对原始对象进行额外的处理。
接口使用JDK代理
实现了接口的类
public class AopServiceImpl implements AopService {
}
开启方式
XML配置方式,Spring AOP默认使用JDK代理方式,可以省略。
<aop:aspectj-autoproxy proxy-target-class="false"/>
注解方式
@EnableAspectJAutoProxy(proxyTargetClass=false)
3.4 CGLib代理方式
当目标类没有实现接口或者要强制使用CGLib时,Spring会使用CGLib库生成一个被代理对象的子类来作为代理。这种方式需要对类的字节码进行操作,生成的代理类比使用JDK动态代理更强大,更复杂。需要注意的是,使用CGLib代理方式不能代理final的方法。
非接口使用CGLib代理
为实现接口的类
public class AopServiceImpl{
}
开启方式
XML配置方式,Spring AOP默认使用JDK代理方式,可以省略。
<aop:aspectj-autoproxy proxy-target-class="true"/>
注解方式
@EnableAspectJAutoProxy(proxyTargetClass=true)
3.5 切入点表达式
在上述的案例中,我们在定以切入点(Pointcut)时,通过execution表达式的形式制定了哪些位置需要且入,那么execution是什么呢??
- execution是一个切入点表达式(pointcut expression),用于指定某个方法执行的时候会被Spring AOP进行截获,然后加入一些自定义的操作,比如前置处理、后置处理、异常处理等。
execution表达式格式
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- modifiers-pattern:表示方法的访问权限,如 public、protected 或 private。该部分是可选的;
- ret-type-pattern:表示方法返回的类型,必须要有。如 void、String、Object、具体的类名等;
- declaring-type-pattern:表示方法所在的类的模式,如包名.类名。这个部分也是可选的;
- name-pattern:表示方法名的模式,是必须的。可以用 * 作为通配符;
- param-pattern:表示方法参数的模式。这部分是必须的。可以用具体的类名,也可以使用 * 或 .. 作为通配符,表示任意类型、任意数量的参数;
- throws-pattern:表示方法需要抛出的异常类型。这部分是可选的。
execution表达式图解
通用切入点表达式
// 任意公共方法的执行
execution(public * *(..))
// 任何一个名字以“set”开始的方法的执行
execution(* set*(..))
// AopServuce 接口定义的任意方法的执行
execution(* com.xygalaxy.service.AopServuce.*(..))
// 在 service 包中定义的任意方法的执行
execution(* com.xygalaxy.service..(..))
// 在 service 包或其子包中定义的任意方法的执行
execution(* com.xygalaxy.service...(..))
// 在 service 包中的任意连接点(在 Spring AOP 中只是方法执行)
within(com.xygalaxy.service.*)
// 在 service 包或其子包中的任意连接点(在 Spring AOP 中只是方法执行)
within(com.xygalaxy.service..*)
// 实现了 AopServuce 接口的代理对象的任意连接点(在 Spring AOP 中只是方法执行)
this(com.xygalaxy.service.AopServuce)
// 实现 AopServuce 接口的目标对象的任意连接点(在 Spring AOP 中只是方法执行)
target(com.xygalaxy.service.AopServuce)
// 任何一个只接受一个参数,并且运行时所传入的参数是 Serializable 接口的连接点(在 Spring AOP 中只是方法执行)
args(java.io.Serializable)
// 目标对象中有一个 @Transactional 注解的任意连接点(在 Spring AOP 中只是方法执行)
@target(org.springframework.transaction.annotation.Transactional)
// 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点(在 Spring AOP 中只是方法执行)
@within(org.springframework.transaction.annotation.Transactional)
// 任何一个执行的方法有一个 @Transactional 注解的连接点(在 Spring AOP 中只是方法执行)
@annotation(org.springframework.transaction.annotation.Transactional)
// 任何一个只接受一个参数,并且运行时所传入的参数类型具有 @Classified 注解的连接点
//(在 Spring AOP 中只是方法执行)
@args(com.xygalaxy.security.Classified)
// 任何一个在名为 'AopServuce' 的 Spring bean 之上的连接点(在 Spring AOP 中只是方法执行)
bean(AopServuce)
// 任何一个在名字匹配通配符表达式 '*Service' 的 Spring bean 之上的连接点(在 Spring AOP 中只是方法执行)
bean(*Service)
4、使用AOP注解案例
4.1 前置通知
前置通知:在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常)
参数:
-
value:切入点表达式,表示切面的功能执行的位置
@Before(value = "execution(public * say*(..))")
public void sayBefore(){
System.out.println("这是Before语句");
}
4.2 后置通知(最终通知)
后置通知:又被成为最终通知,当连接点退出的时候执行的通知,不管是否异常,该通知都会执行。
参数:
- value:切入点表达式,表示切面的功能执行的位置
@After("pointcutName()")
public void sayAfter(){
System.out.println("这是After语句");
}
4.3 后置返回通知
后置返回通知:在连接点正常完成后执行的通知,并接受连接点返回的结果。
参数:
-
value:切入点表达式,表示切面的功能执行的位置
- returning:自定义变量,表示目标方法的返回值
@AfterReturning(pointcut = "execution(* com.guyk.service.UserService.getUser(..))", returning = "result")
public void afterReturningAdvice(Object result){
System.out.println("After method returned, the result is " + result);
}
在上述例子中,@AfterReturning 通知会在 com.guyk.service.UserService.getUser(..) 方法执行并成功返回结果之后执行。returning = "result" 指定了一个名为 result 的参数,这个参数用于接受 getUser() 方法的返回值。
4.4 后置异常通知
后置异常通知:在方法抛出异常退出时执行的通知。
参数:
- value:切入点表达式,表示切面的功能执行的位置
- throwing:自定义的变量,表示目标方法抛出的异常对象
@AfterThrowing(pointcut = "execution(* com.guyk.service.UserService.getUser(..))", throwing = "ex")
public void AfterThrowingAdvice(IllegalArgumentException ex){
System.out.println("There has been an exception: " + ex.toString());
}
在这个示例中,@AfterReturning 注解表明该方法是一个异常通知,即当匹配的方法(这里是 com.guyk.service.UserService.getUser(..) )抛出异常时,该通知就会被触发。throwing = "ex"部分指定了一个名为ex的参数,用于接受抛出的异常。在本例中,异常类型为 IllegalArgumentException。
4.5 环绕通知
环绕通知:在方法调用前后时执行通知,这是最强大的一种通知类型。
参数
- value:切入点表达式,表示切面的功能执行的位置。
- ProceedingJoinPoint:是 Spring AOP 中用于环绕通知的特殊连接点对象。下面有详细说明。
@Around("execution(* com.example.service.*.*(..))")
public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Entering method: " + joinPoint.getSignature());
Object result = joinPoint.proceed();
System.out.println("Exiting method: " + joinPoint.getSignature());
return result;
}
ProceedingJoinPoint 是一个特殊的 JoinPoint,它只在环绕通知中才能使用。它是在目标方法执行之前和之后,允许我们手动控制方法的执行,并且可以访问目标方法的参数和返回值。
在环绕通知中,ProceedingJoinPoint提供了两个重要的方法:
- proceed():这个方法是环绕通知的核心部分。调用 proceed() 方法会继续执行目标方法,并返回目标方法的返回值。如果我们不调用 proceed() 方法,目标方法将被阻塞或终止。
- getArgs():这个方法返回一个数组,表示目标方法的参数列表。我们可以使用 getArgs() 方法来获取目标方法的参数,并在环绕通知中进行处理。
除了这两个常用的方法之外,ProceedingJoinPoint 还提供其他的方法来获取目标方法的信息,例如:
- getSignature():获取方法的签名。
- getTarget():获取目标对象。
- getThis():获取代理对象(在代理模式下)。
通过使用 ProceedingJoinPoint,我们可以在环绕通知中更加灵活地控制方法的执行。我们可以选择继续执行目标方法,也可以在某些条件下直接返回或抛出异常,以实现自定义的行为。同时,我们还可以通过获取目标方法的参数和返回值,在环绕通知中进行前置或后置处理。
3.6 切入点
切入点(@Pointcut):当有很多的切入点表达式的时候,就比较难管理,这时就可以使用过@Pointcut注解。
特点
- 当使用了Pointcut定义在一个方法的上面,此时这个方法的名称就是切入点表达式的别名;
- 其他的通知中,value的属性就可以使用这个方法的名称来代替切入点表达式了;
- 切入点方法的实际内容会被忽略,Spring AOP不会调用此方法,所以在@Pointcut注解的方法内部添加的代码不会执行。
@Pointcut("execution(public * say*(..))")
public void pointcutName(){
// 事实上,这个方法的实际内容会被忽略,Spring AOP不会调用此方法,所以在@Pointcut注解的方法内部添加的代码不会执行。
System.out.println("这是切点,内容通常都是空方法,因为这里不会执行");
}
@After("pointcutName()")
public void sayAfter(){
System.out.println("这是After语句");
}