Spring AOP 教程

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 BootIntelliJ 来进行开发和演示。

  1. 打开 Spring Initializr
Spring Initializr
  • 输入 Group 和 Artifact,你写你自己的就行了
  • 在 Dependency 中选择 Aspects 然后点击 生成项目 按钮

然后打开 IntelliJ 打开刚在下载的项目

Open Project

打开以后就是下面这个样子

Project Structure

Maven 文件如下

<?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
  • thistarget
  • 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 已经生效

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容