Spring入门(十):Spring AOP使用讲解
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版)》
原创不易,如果觉得文章能学到东西的话,欢迎点个赞、评个论、关个注,这是我坚持写作的最大动力。
如果有兴趣,欢迎添加我的微信:zwwhnly,等你来聊技术、职场、工作等话题(PS:我是一名奋斗在上海的程序员)。
Spring入门(十):Spring AOP使用讲解的更多相关文章
- Spring入门(十四):Spring MVC控制器的2种测试方法
作为一名研发人员,不管你愿不愿意对自己的代码进行测试,都得承认测试对于研发质量保证的重要性,这也就是为什么每个公司的技术部都需要质量控制部的原因,因为越早的发现代码的bug,成本越低,比如说,Dev环 ...
- Spring入门IOC和AOP学习笔记
Spring入门IOC和AOP学习笔记 概述 Spring框架的核心有两个: Spring容器作为超级大工厂,负责管理.创建所有的Java对象,这些Java对象被称为Bean. Spring容器管理容 ...
- spring的IOC和AOP详细讲解
1.解释spring的ioc? 几种注入依赖的方式?spring的优点? IOC你就认为他是一个生产和管理bean的容器就行了,原来需要在调用类中new的东西,现在都是有这个IOC容器进行产生,同时, ...
- Spring入门(十二):Spring MVC使用讲解
1. Spring MVC介绍 提到MVC,参与过Web应用程序开发的同学都很熟悉,它是展现层(也可以理解成直接展现给用户的那一层)开发的一种架构模式,M全称是Model,指的是数据模型,V全称是Vi ...
- Spring入门(十五):使用Spring JDBC操作数据库
在本系列的之前博客中,我们从没有讲解过操作数据库的方法,但是在实际的工作中,几乎所有的系统都离不开数据的持久化,所以掌握操作数据库的使用方法就非常重要. 在Spring中,操作数据库有很多种方法,我们 ...
- Spring课程 Spring入门篇 5-1 aop基本概念及特点
概念: 1 什么是aop及实现方式 2 aop的基本概念 3 spring中的aop 1 什么是aop及实现方式 1.1 aop,面向切面编程,比如:唐僧取经需要经过81难,多一难少一难都不行.孙悟空 ...
- Spring 学习十五 AOP
http://www.hongyanliren.com/2014m12/22797.html 1: 通知(advice): 就是你想要的功能,也就是安全.事物.日子等.先定义好,在想用的地方用一下.包 ...
- spring MVC(十)---spring MVC整合mybatis
spring mvc可以通过整合hibernate来实现与数据库的数据交互,也可以通过mybatis来实现,这篇文章是总结一下怎么在springmvc中整合mybatis. 首先mybatis需要用到 ...
- Spring学习(十)-----Spring依赖检查
在Spring中,可以使用依赖检查功能,以确保所要求的属性可设置或者注入. 依赖检查模式 4个依赖检查支持的模式: none – 没有依赖检查,这是默认的模式. simple – 如果基本类型(int ...
随机推荐
- liunx某台服务器无法访问其他服务器!!!!!!!!
针对于可以ping通ip地址,但是无法访问端口!!! 访问端口卡死,未响应, 例如mysql出现当前主机无法远程连接数据库,而其他主机都可以 前提条件:防火墙,mysql账号ip限制问题已经解决 问题 ...
- Python用法
Python用法 IDE IDE是集成开发环境:Integrated Development Environment的缩写. 使用IDE的好处在于按,可以把编写代码.组织项目.编译.运行.调试等放到一 ...
- jsp页面中将CST时间格式化为年月日
引入: <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> 格式化: ...
- [OpenGL] 不规则区域的填充算法
不规则区域的填充算法 一.简单递归 利用Dfs实现简单递归填充. 核心代码: // 简单深度搜索填充 (四连通) void DfsFill(int x, int y) { || y < || x ...
- python课堂整理10---局部变量与全局变量
一.局部变量与全局变量 1. 没有缩进,顶头写的变量为全局变量 2. 在子程序里定义的变量为局部变量 3. 只有函数能把变量私有化 name = 'lhf' #全局变量 def change_name ...
- 第一个C# Winform实例
前面我们准备好了相关的库,现在开始搭建环境,本人自动化行业,就用Windorm开发吧,例子仅仅做引导,希望大家能深入.VS版本VS2017 1:打开VS建立一个WInform 项目.拉入两个控件,gr ...
- 数据结构与算法基础之malloc()动态分配内存概述
动态内存分配和释放: 动态构造一维数组: 假设动态构造一个Int型数组: int *p = (int *)malloc(int len); //还可以写作: int *p = (int *)mallo ...
- 基于drone构建CI-CD系统
kubernetes集群三步安装 CI 概述 用一个可描述的配置定义整个工作流 程序员是很懒的动物,所以想各种办法解决重复劳动的问题,如果你的工作流中还在重复一些事,那么可能就得想想如何优化了 持续集 ...
- 原生应用使用cordova并与h5应用分离
个人原创地址:https://www.jianshu.com/p/1ad536e76640 1.需求与使用场景 打开一个新页面,要求能够加载本地zip格式的h5应用,该应用使用了某些原生能力:能够 ...
- middleware中间件
django 中的中间件(middleware),在django中,中间件其实就是一个类,在请求到来和结束后,django会根据自己的规则在合适的时机执行中间件中相应的方法. 在django项目的se ...