1. 什么是AOP?

AOP是Aspect Oriented Programming的缩写,意思是:面向切面编程,它是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

可以认为AOP是对OOP(Object Oriented Programming 面向对象编程)的补充,主要使用在日志记录,性能统计,安全控制等场景,使用AOP可以使得业务逻辑各部分之间的耦合度降低,只专注于各自的业务逻辑实现,从而提高程序的可读性及维护性。

比如,我们需要记录项目中所有对外接口的入参和出参,以便出现问题时定位原因,在每一个对外接口的代码中添加代码记录入参和出参当然也可以达到目的,但是这种硬编码的方式非常不友好,也不够灵活,而且记录日志本身和接口要实现的核心功能没有任何关系。

此时,我们可以将记录日志的功能定义到1个切面中,然后通过声明的方式定义要在何时何地使用这个切面,而不用修改任何1个外部接口。

在讲解具体的实现方式之前,我们先了解几个AOP中的术语。

1.1 通知(Advice)

在AOP术语中,切面要完成的工作被称为通知,通知定义了切面是什么以及何时使用。

Spring切面有5种类型的通知,分别是:

  • 前置通知(Before):在目标方法被调用之前调用通知功能
  • 后置通知(After):在目标方法完成之后调用通知,此时不关心方法的输出结果是什么
  • 返回通知(After-returning):在目标方法成功执行之后调用通知
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

1.2 连接点(Join point)

连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、修改某个字段时。

1.3 切点(Pointcut)

切点是为了缩小切面所通知的连接点的范围,即切面在何处执行。我们通常使用明确的类和方法名称,或者利用正则表达式定义所匹配的类和方法名称来指定切点。

1.4 切面(Aspect)

切面是通知和切点的结合。通知和切点共同定义了切面的全部内容:它是什么,在何时和何处完成其功能。

1.5 引入(Introduction)

引入允许我们在不修改现有类的基础上,向现有类添加新方法或属性。

1.6 织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。

切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里,有以下几个点可以进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

2. Spring 对AOP的支持

2.1 动态代理

Spring AOP构建在动态代理之上,也就是说,Spring运行时会为目标对象动态创建代理对象。

代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。

当代理类拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。

2.2 织入切面时机

通过在代理类中包裹切面,Spring在运行期把切面织入到Spring 管理的bean中,也就是说,直到应用需要被代理的bean时,Spring才会创建代理对象。

因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP切面。

2.3 连接点限制

Spring只支持方法级别的连接点,如果需要字段级别或者构造器级别的连接点,可以利用AspectJ来补充Spring AOP的功能。

3. Spring AOP使用

假设我们有个现场表演的接口Performance和它的实现类SleepNoMore:

package chapter04.concert;

/**
* 现场表演,如舞台剧,电影,音乐会
*/
public interface Performance {
void perform();
}
package chapter04.concert;

import org.springframework.stereotype.Component;

/**
* 戏剧:《不眠之夜Sleep No More》
*/
@Component
public class SleepNoMore implements Performance {
@Override
public void perform() {
System.out.println("戏剧《不眠之夜Sleep No More》");
}
}

既然是演出,就需要观众,假设我们的需求是:在看演出之前,观众先入座并将手机调整至静音,在观看演出之后观众鼓掌,如果演出失败观众退票,我们当然可以把这些逻辑写在上面的perform()方法中,但不推荐这么做,因为这些逻辑理论上和演出的核心无关,就算观众不将手机调整至静音或者看完演出不鼓掌,都不影响演出的进行。

针对这个需求,我们可以使用AOP来实现。

3.1 定义切面

首先,在pom.xml文件中添加如下依赖:

<!--spring aop支持-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
<!--aspectj支持-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>

然后,定义一个观众的切面如下:

package chapter04.concert;

import org.aspectj.lang.annotation.Aspect;

/**
* 观众
* 使用@Aspect注解定义为切面
*/
@Aspect
public class Audience {
}

注意事项:@Aspect注解表明Audience类是一个切面。

3.2 定义前置通知

在Audience切面中定义前置通知如下所示:

/**
* 表演之前,观众就座
*/
@Before("execution(* chapter04.concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("Taking seats");
} /**
* 表演之前,将手机调至静音
*/
@Before("execution(* chapter04.concert.Performance.perform(..))")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}

这里的重点代码是@Before("execution(* chapter04.concert.Performance.perform(..))"),它定义了1个前置通知,其中execution(* chapter04.concert.Performance.perform(..))被称为AspectJ切点表达式,每一部分的讲解如下:

  • @Before:该注解用来定义前置通知,通知方法会在目标方法调用之前执行
  • execution:在方法执行时触发
  • *:表明我们不关心方法返回值的类型,即可以是任意类型
  • chapter04.concert.Performance.perform:使用全限定类名和方法名指定要添加前置通知的方法
  • (..):方法的参数列表使用(..),表明我们不关心方法的入参是什么,即可以是任意类型

3.3 定义后置通知

在Audience切面中定义后置通知如下所示:

/**
* 表演结束,不管表演成功或者失败
*/
@After("execution(* chapter04.concert.Performance.perform(..))")
public void finish() {
System.out.println("perform finish");
}

注意事项:@After注解用来定义后置通知,通知方法会在目标方法返回或者抛出异常后调用

3.4 定义返回通知

在Audience切面中定义返回通知如下所示:

/**
* 表演之后,鼓掌
*/
@AfterReturning("execution(* chapter04.concert.Performance.perform(..))")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}

注意事项:@AfterReturning注解用来定义返回通知,通知方法会在目标方法返回后调用

3.5 定义异常通知

在Audience切面中定义异常通知如下所示:

/**
* 表演失败之后,观众要求退款
*/
@AfterThrowing("execution(* chapter04.concert.Performance.perform(..))")
public void demandRefund() {
System.out.println("Demanding a refund");
}

注意事项:@AfterThrowing注解用来定义异常通知,通知方法会在目标方法抛出异常后调用

3.6 定义可复用的切点表达式

细心的你可能会发现,我们上面定义的5个切点中,切点表达式都是一样的,这显然是不好的,好在我们可以使用@Pointcut注解来定义可重复使用的切点表达式:

/**
* 可复用的切点
*/
@Pointcut("execution(* chapter04.concert.Performance.perform(..))")
public void perform() {
}

然后之前定义的5个切点都可以引用这个切点表达式:

/**
* 表演之前,观众就座
*/
@Before("perform()")
public void takeSeats() {
System.out.println("Taking seats");
} /**
* 表演之前,将手机调至静音
*/
@Before("perform()")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
} /**
* 表演结束,不管表演成功或者失败
*/
@After("perform()")
public void finish() {
System.out.println("perform finish");
} /**
* 表演之后,鼓掌
*/
@AfterReturning("perform()")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
} /**
* 表演失败之后,观众要求退款
*/
@AfterThrowing("perform()")
public void demandRefund() {
System.out.println("Demanding a refund");
}

3.7 单元测试

新建配置类ConcertConfig如下所示:

package chapter04.concert;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
@Bean
public Audience audience() {
return new Audience();
}
}

注意事项:和以往不同的是,我们使用了@EnableAspectJAutoProxy注解,该注解用来启用自动代理功能。

新建Main类,在其main()方法中添加如下测试代码:

package chapter04.concert;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConcertConfig.class); Performance performance = context.getBean(Performance.class);
performance.perform(); context.close();
}
}

运行代码,输出结果如下所示:

Silencing cell phones

Taking seats

戏剧《不眠之夜Sleep No More》

perform finish

CLAP CLAP CLAP!!!

稍微修改下SleepNoMore类的perform()方法,让它抛出一个异常:

@Override
public void perform() {
int number = 3 / 0;
System.out.println("戏剧《不眠之夜Sleep No More》");
}

再次运行代码,输出结果如下所示:

Silencing cell phones

Taking seats

perform finish

Demanding a refund

Exception in thread "main" java.lang.ArithmeticException: / by zero

由此也可以说明,不管目标方法是否执行成功,@After注解都会执行,但@AfterReturning注解只会在目标方法执行成功时执行。

值得注意的是,使用@Aspect注解的切面类必须是一个bean(不管以何种方式声明),否则切面不会生效,因为AspectJ自动代理只会为使用@Aspect注解的bean创建代理类。

也就是说,如果我们将ConcertConfig配置类中的以下代码删除或者注释掉:

@Bean
public Audience audience() {
return new Audience();
}

运行结果将变为:

戏剧《不眠之夜Sleep No More》

3.8 创建环绕通知

我们可以使用@Around注解创建环绕通知,该注解能够让你在调用目标方法前后,自定义自己的逻辑。

因此,我们之前定义的5个切点,现在可以定义在一个切点中,为不影响之前的切面,我们新建切面AroundAudience,如下所示:

package chapter04.concert;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; @Aspect
public class AroundAudience {
/**
* 可重用的切点
*/
@Pointcut("execution(* chapter04.concert.Performance.perform(..))")
public void perform() {
} @Around("perform()")
public void watchPerform(ProceedingJoinPoint joinPoint) {
try {
System.out.println("Taking seats");
System.out.println("Silencing cell phones"); joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!");
} catch (Throwable throwable) {
System.out.println("Demanding a refund");
} finally {
System.out.println("perform finish");
}
}
}

这里要注意的是,该方法有个ProceedingJoinPoint类型的参数,在方法中可以通过调用它的proceed()方法来调用目标方法。

然后修改下ConcertConfig类的代码:

package chapter04.concert;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
/*@Bean
public Audience audience() {
return new Audience();
}*/ @Bean
public AroundAudience aroundAudience() {
return new AroundAudience();
}
}

运行结果如下所示:

Taking seats

Silencing cell phones

戏剧《不眠之夜Sleep No More》

CLAP CLAP CLAP!!!

perform finish

4. 源码及参考

源码地址:https://github.com/zwwhnly/spring-action.git,欢迎下载。

Craig Walls 《Spring实战(第4版)》

AOP(面向切面编程)_百度百科

原创不易,如果觉得文章能学到东西的话,欢迎点个赞、评个论、关个注,这是我坚持写作的最大动力。

如果有兴趣,欢迎添加我的微信:zwwhnly,等你来聊技术、职场、工作等话题(PS:我是一名奋斗在上海的程序员)。

Spring入门(十):Spring AOP使用讲解的更多相关文章

  1. Spring入门(十四):Spring MVC控制器的2种测试方法

    作为一名研发人员,不管你愿不愿意对自己的代码进行测试,都得承认测试对于研发质量保证的重要性,这也就是为什么每个公司的技术部都需要质量控制部的原因,因为越早的发现代码的bug,成本越低,比如说,Dev环 ...

  2. Spring入门IOC和AOP学习笔记

    Spring入门IOC和AOP学习笔记 概述 Spring框架的核心有两个: Spring容器作为超级大工厂,负责管理.创建所有的Java对象,这些Java对象被称为Bean. Spring容器管理容 ...

  3. spring的IOC和AOP详细讲解

    1.解释spring的ioc? 几种注入依赖的方式?spring的优点? IOC你就认为他是一个生产和管理bean的容器就行了,原来需要在调用类中new的东西,现在都是有这个IOC容器进行产生,同时, ...

  4. Spring入门(十二):Spring MVC使用讲解

    1. Spring MVC介绍 提到MVC,参与过Web应用程序开发的同学都很熟悉,它是展现层(也可以理解成直接展现给用户的那一层)开发的一种架构模式,M全称是Model,指的是数据模型,V全称是Vi ...

  5. Spring入门(十五):使用Spring JDBC操作数据库

    在本系列的之前博客中,我们从没有讲解过操作数据库的方法,但是在实际的工作中,几乎所有的系统都离不开数据的持久化,所以掌握操作数据库的使用方法就非常重要. 在Spring中,操作数据库有很多种方法,我们 ...

  6. Spring课程 Spring入门篇 5-1 aop基本概念及特点

    概念: 1 什么是aop及实现方式 2 aop的基本概念 3 spring中的aop 1 什么是aop及实现方式 1.1 aop,面向切面编程,比如:唐僧取经需要经过81难,多一难少一难都不行.孙悟空 ...

  7. Spring 学习十五 AOP

    http://www.hongyanliren.com/2014m12/22797.html 1: 通知(advice): 就是你想要的功能,也就是安全.事物.日子等.先定义好,在想用的地方用一下.包 ...

  8. spring MVC(十)---spring MVC整合mybatis

    spring mvc可以通过整合hibernate来实现与数据库的数据交互,也可以通过mybatis来实现,这篇文章是总结一下怎么在springmvc中整合mybatis. 首先mybatis需要用到 ...

  9. Spring学习(十)-----Spring依赖检查

    在Spring中,可以使用依赖检查功能,以确保所要求的属性可设置或者注入. 依赖检查模式 4个依赖检查支持的模式: none – 没有依赖检查,这是默认的模式. simple – 如果基本类型(int ...

随机推荐

  1. py+selenium IE 用driver.close()却把两个窗口都关了【已解决】

    环境:py3  selenium  unittest 测试浏览器:IE10 目标:在单个文件中,有多个用例,执行完A用例,由于打开了新的窗口,必须关闭新的窗口,才不会影响下一条用例的执行. 问题:按例 ...

  2. MFC开发--截图工具

    近期学习了MFC的相关知识,MFC(Microsoft Foundation Classes)是微软公司提供的一个类库,可以这样简单理解,就是对于Win32的封装(MFC对windows API函数的 ...

  3. jsp页面中将CST时间格式化为年月日

    引入: <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> 格式化: ...

  4. Hadoop值Partition分区

    分区操作 为什么要分区? 要求将统计结果按照条件输出到不同文件中(分区).比如:将统计结果按 照手机归属地不同省份输出到不同文件中(分区) 默认 partition 分区 /** 源码中:numRed ...

  5. http面试笔试常考知识点(二)

    接上一篇随笔 1. https协议为什么比http安全? 内容加密:建立一个信息安全通道,确保信息传输安全: 身份认证:确保网站的真实性: 数据完整性校验:防止内容被第三方冒充或者篡改 2.常见状态码 ...

  6. PTA 打印沙漏

    https://pintia.cn/problem-sets/17/problems/260 #include <bits/stdc++.h> using namespace std; i ...

  7. 自动生成Mybatis的Mapper文件

    自动生成Mybatis的Mapper文件 工作中使用mybatis时我们需要根据数据表字段创建pojo类.mapper文件以及dao类,并且需要配置它们之间的依赖关系,这样的工作很琐碎和重复,myba ...

  8. Golang高效实践之interface、reflection、json实践

    前言 反射是程序校验自己数据结构和类型的一种机制.文章尝试解释Golang的反射机制工作原理,每种编程语言的反射模型都是不同的,有很多语言甚至都不支持反射. Interface 在将反射之前需要先介绍 ...

  9. Redis(四)--- Redis的命令参考

    1.简述 数据类型也称数据对象,包含字符串对象(string).列表对象(list).哈希对象(hash).集合对象(set).有序集合对象(zset). 2.String数据类型命令 string  ...

  10. jsp数据交互(一).2

    01.什么是JSP内置对象(jsp核心)? Java 内置对象 Java  作用域 解析:jsp内置对象是web容器创建的一组对象.我们都知道tomcat这款软件可以看成是一种web容器,所以我们可以 ...