AOP 简介
AOP (Aspect Oriented Programming) 即 面向切面编程,听上去有点抽象和高大上,那么这玩意儿有啥用呢?和平时我们说的 OOP (面向女朋友编程) 有啥区别呢?
下面是引用自 Spring 官方文档的描述:
Aspect-oriented Programming (AOP) complements Object-oriented Programming (OOP) by providing another way of thinking about program structure. The key unit of modularity in OOP is the class, whereas in AOP the unit of modularity is the aspect. Aspects enable the modularization of concerns that cut across multiple types and objects.
也是就是说,AOP 是一种编程典范,它通过分离横切关注点来增加程序的模块化。
刚才那句话是不是看不懂?没事,我也看不懂。
简单说就是 AOP 可以在不修改现有代码的情况下对现有代码增加一些功能,那么这就是 AOP 最强大的功能。
目前最受欢迎的 AOP 库有两个,一个是 AspectJ, 另外一个就是我们今天讲的 Spring AOP。
在介绍 Spring AOP 之前,先讲讲 AOP 的核心概念。
AOP 核心概念
- Aspect:即切面,切面一般定义为一个 Java 类, 每个切面侧重于特定的跨领域功能,比如,事务管理或者日志打印等。
- Joinpoint:即连接点,程序执行的某个点,比如方法执行。构造函数调用或者字段赋值等。在 Spring AOP 中,连接点只会有 方法调用 (Method execution)。
- Advice:即通知,在连接点要的代码。
- Pointcut:即切点,一个匹配连接点的正则表达式。当一个连接点匹配到切点时,一个关联到这个切点的特定的 通知 (Advice) 会被执行。
- Weaving:即编织,负责将切面和目标对象链接,以创建通知对象,在 Spring AOP 中没有这个东西。
Spring AOP
接下来就正式进入本教程,在这个教程中,我们将学习如何使用 Spring AOP 在我们的代码中。
创建项目
在这次的教程中我们使用 Spring Boot 和 IntelliJ 来进行开发和演示。
Spring Initializr- 输入 Group 和 Artifact,你写你自己的就行了
- 在 Dependency 中选择 Aspects 然后点击 生成项目 按钮
然后打开 IntelliJ 打开刚在下载的项目
Open Project打开以后就是下面这个样子
Project StructureMaven 文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>aaric</groupId>
<artifactId>spring-aop-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-aop-demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
好了,准备工作已经就绪了,我们再通过一张图来回顾一下 AOP 的概念和术语:
图片来自 baeldung创建业务对象
业务对象就是一个普通的 Java 类,然后有自己的一些业务逻辑。我们就以下面这个 微信服务 对象为例,这个对象只有一个简单的业务逻辑就是 分享文章到朋友圈。
- 我们在 IntelliJ 里面选中根包,然后按下 Alt + Insert (window)
- 这里直接按 回车
- 然后在对话框中输入我们的类名 WeixinService 然后按 回车
- 然后在类中我们定义如下方法
package aaric.springaopdemo;
import org.springframework.stereotype.Service;
@Service
public class WeixinService {
public void share(String articleUrl) {
System.out.println("Share article:" + articleUrl);
}
}
- 这里补充一下,在输入 System.out.println 的时候我们只用输入sout四个字母就行了
- 然后在 SpringAopDemoApplication 中增加如下代码
package aaric.springaopdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class SpringAopDemoApplication {
public static void main(String[] args) {
ApplicationContext applicationContext = SpringApplication.run(SpringAopDemoApplication.class, args);
WeixinService weixinService = applicationContext.getBean(WeixinService.class);
weixinService.share("https://www.jianshu.com/u/db7d7a281529");
}
}
- 点击右上角的 运行 按钮,程序运行以后我们会看到如下结果
定义切面
上面我们创建了自己的业务对象,那么现在我们创建一个切面,使用 AOP 在不对业务对象进行修改的情况增加一些功能,比如在分享到朋友圈之后我们将这次分享记录到日志中。
我们按照上面的方法在同样的包中创建类 WeixinServiceAspect
package aaric.springaopdemo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class WeixinServiceAspect {
@AfterReturning("execution(public void WeixinService.share(String))")
public void log(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature() + " executed");
}
}
同时在 SpringAopDemoApplication 中增加注解 @EnableAspectJAutoProxy
package aaric.springaopdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringAopDemoApplication {
public static void main(String[] args) {
ApplicationContext applicationContext = SpringApplication.run(SpringAopDemoApplication.class, args);
WeixinService weixinService = applicationContext.getBean(WeixinService.class);
weixinService.share("https://www.jianshu.com/u/db7d7a281529");
}
}
- 运行程序得到如下结果
可以发现我们增加的日志记录已经在控制台输出了,但是我们并没有修改我们的业务对象。
到这里,大家应该对 AOP 有了一个直观的感受,下面我们就来具体说一说 Spring AOP 给我们提供了哪些 API 来使用。
Aspect 定义
package aaric.springaopdemo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class WeixinServiceAspect {
@AfterReturning("execution(public void WeixinService.share(String))")
public void log(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature() + " executed");
}
}
- 在 Spring 中使用 Aspect 需要使用 @Component 直接将其标记为一个 Bean
- 并且使用 @Aspec 注解将其标记为一个切面
- 然后在该类中定义上面我们说的切点,通知等
Pointcut 定义
这里我们说一下 Pointcut 的表达式如何写,我们首先将上面例子中的切面类修改如下
package aaric.springaopdemo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class WeixinServiceAspect {
@Pointcut("execution(public void WeixinService.share(String))")
public void shareCut() {
}
@AfterReturning("shareCut()")
public void log(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature() + " executed");
}
}
- 下面这个便是切点的定义
- 切点定义在方法上,并使用 @Pointcut 注解,注解中的值便是切点的表达式
- 切点的名称就是方法的名称,这里是 shareCut(),注意这里有括号
@Pointcut("execution(public void WeixinService.share(String))")
public void shareCut() {
}
- 若要将具体的通知 Advice 关联的某个切点上,在 Advice 的注解上写上切点的名称就可以了,如下
@AfterReturning("shareCut()")
public void log(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature() + " executed");
}
Pointcut 指示器
切点的表达式以 指示器 开始, 指示器 就是一种关键字,用来告诉 Spring AOP 如何匹配连接点,Spring AOP 提供了以下几种指示器
- execution
- within
- this 和 target
- args
- @target
- @annotation
下面我们依次说明这些指示器的作用
execution
该指示器用来匹配方法执行连接点,即匹配哪个方法执行,如
@Pointcut("execution(public String aaric.springaopdemo.UserDao.findById(Long))")
上面这个切点会匹配在 UserDao 类中 findById 方法的调用,并且需要该方法是 public 的,返回值类型为 String,只有一个 Long 的参数。
切点的表达式同时还支持宽字符匹配,如
@Pointcut("execution(* aaric.springaopdemo.UserDao.*(..))")
上面的表达式中,第一个宽字符 * 匹配 任何返回类型,第二个宽字符 * 匹配 任何方法名,最后的参数 (..) 表达式匹配 任意数量任意类型 的参数,也就是说该切点会匹配类中所有方法的调用。
within
如果要匹配一个类中所有方法的调用,便可以使用 within 指示器
@Pointcut("within(aaric.springaopdemo.UserDao)")
这样便可以匹配该类中所有方法的调用了。同时,我们还可以匹配某个包下面的所有类的所有方法调用,如下面的例子
@Pointcut("within(aaric.springaopdemo..*)")
this 和 target
- 如果目标对象实现了任何接口,Spring AOP 会创建基于CGLIB 的动态代理,这时候需要使用 target 指示器
- 如果目标对象没有实现任何接口,Spring AOP 会创建基于JDK的动态代理,这时候需要使用 this 指示器
@Pointcut("target(aaric.springaopdemo.A)") A 实现了某个接口
@Pointcut("target(aaric.springaopdemo.B)") B 没有实现任何一个接口
args
该指示器用来匹配具体的方法参数
@Pointcut("execution(* *..find*(Long))")
这个切点会匹配任何以 find 开头并且只有一个 Long 类型的参数的方法。
如果我们想匹配一个以 Long 类型开始的参数,后面的参数类型不做限制,我们可以使用如下的表达式
@Pointcut("execution(* *..find*(Long,..))")
@target
该指示器不要和 target 指示器混淆,该指示器用于匹配连接点所在的类是否拥有指定类型的注解,如
@Pointcut("@target(org.springframework.stereotype.Repository)")
@annotation
该指示器用于匹配连接点的方法是否有某个注解
@Pointcut("@annotation(org.springframework.scheduling.annotation.Async)")
组合切点表达式
切点表达式可以通过 &&、 || 和 !等操作符来组合,如
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void repositoryMethods() {}
@Pointcut("execution(* *..create*(Long,..))")
public void firstLongParamMethods() {}
@Pointcut("repositoryMethods() && firstLongParamMethods()")
public void entityCreationMethods() {}
上面的第三个切点需要同时满足第一个和第二个切点表达式
Advice 定义
Advice 通知,即在连接点出要执行的代码,分为以下几种类型
- Around
- Before
- After
开启 Advice
如果要在 Spring 中使用 Spring AOP 需要开启 Advice,使用 @EnableAspectJAutoProxy 注解就可以了,代码如下
@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringAopDemoApplication {
}
Before Advice
Before Advice 用来执行在方法调用之前的操作,如果 Before Advice 在执行的过程中抛出异常的话,那么连接点的方法就不会被执行。
@Aspect
@Component
public class PrintAspect {
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void repositoryMethods() {};
@Before("repositoryMethods()")
public void logMethodCall(JoinPoint jp) {
String name = jp.getSignature().getName();
System.out.println("Before " + name);
}
}
logMethodCall Advice 会在任何 Repository 方法执行之前调用。
After Advice
顾名思义,该 Advice 会在方法被调用之后被执行,但是有三种注解可以使用:
- @AfterReturing:该 Advice 会在方法正常返回以后执行
- @AfterThrowing: 该 Advice 会在方法抛出异常以后执行
- @After: 该 Advice 无论如何,在方法执行以后都会执行
Around Advice
这个是最有用的 Advice, 它可以控制方法在执行前后的行为。
它可以选择是否继续执行连接点的方法,或者中断该方法的执行,而返回自己的返回值。
自定义 AOP Annotation
最后,在这篇教程中,我们了解如何使用 AOP 以及 AOP 的 API,下面我们尝试自己定义一个 AOP 的 Annotation,@CalculateExecuteTime,任何使用该注解的方法,都会打印出该方法的执行时间。
创建 Annotation
package aaric.springaopdemo;
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)
public @interface CalculateExecuteTime {
}
创建切面
package aaric.springaopdemo;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class CalculateExecuteTimeAspect {
}
创建切点和通知
package aaric.springaopdemo;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class CalculateExecuteTimeAspect {
@Around("@annotation(CalculateExecuteTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object proceed = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
return proceed;
}
}
- 这里的 ProceedingJoinPoint 代表连接的的方法
在方法上加上自定义注解
package aaric.springaopdemo;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class WeixinService {
@CalculateExecuteTime
public void share(@NotNull String articleUrl) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception ignored) {
}
}
}
- 这里我们模拟该方法会执行3秒的时间
- 运行程序以后得到如下结果
可以看到该 Advice 已经生效
网友评论