文章目录
- AOP
- AOP核心概念
- AOP快速入门
- AOP工作流程
- AOP切入点表达式
- AOP通知类型
- AOP通知获取数据
- AOP总结
AOP
AOP核心概念
AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构
OOP(Object Oriented Programming)面向对象编程
作用:在不惊动原始设计的基础上为其进行功能增强
Spring理念:无入侵式/无侵入式
我们来看下面这样一段程序:
某个类中, 有如下四个方法, save方法会打印一句话, 并循环一万次计算出所消耗的时间; 而其他三个方法中只打印一句话, 我们想要update和delete也有save相同的功能, 而select方法就只单纯打印一句话
public void save() {
//记录程序当前执行执行(开始时间)
Long startTime = System.currentTimeMillis();
//业务执行百万次
for (int i = 0;i<10000;i++) {
System.out.println(“book dao save ...”);
}
//记录程序当前执行时间(结束时间)
Long endTime = System.currentTimeMillis();
//计算时间差
Long totalTime = endTime-startTime;
//输出信息
System.out.println("万次耗时:" + totalTime + "ms");
}
public void update(){
System.out.println("book dao update ...");
}
public void delete(){
System.out.println("book dao delete ...");
}
public void select(){
System.out.println("book dao select ...");
}
我们可以使用AOP思想, 将方法的共性抽取出来, 在不改变方法的原代码上, 追加上循环一万次并计算时间的功能
连接点(JoinPoint):程序执行过程中的任意位置,可以为执行方法、抛出异常、设置变量等
在SpringAOP中,理解为
方法的执行
切入点(Pointcut):匹配连接点的式子
在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
- 一个具体方法:com.chenyq.dao包下的BookDao接口中的无形参无返回值的save方法
- 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
通知(Advice):在切入点处执行的操作,也就是共性功能
在SpringAOP中,功能最终以方法的形式呈现, 执行共性操作的方法
通知类:定义通知的类
切面(Aspect):描述通知与切入点的对应关系
简单总结:
连接点: 代表所有的方法
切入点: 代表要追加功能的方法
通知: 代表共性功能的方法
通知类: 代表定义通知(共性功能方法)的类
切面: 代表切入点与通知之间的关系
AOP快速入门
案例需求:要求方法在之前之前打印出当前系统时间
开发模式:注解开发
AOP入门案例思路分析:
- 导入坐标到pom.xml文件
- 制作连接点方法
- 制作共性功能, 通知类和通知
- 定义切入点
- 绑定切入点与通知关系
实现步骤如下:
导入aop和aspect相关坐标, 我们在导入spring坐标时会默认导入aop的依赖坐标, 所以我们只需导入aspect坐标即可
<dependencies>
<!--spring依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!--aspect依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
定义dao接口和实现类
BookDaoImpl实现类类中有下面几个方法, 我们希望update方法在不改变源程序的基础上, 添加执行之前打印当前系统时间的功能, 而add方法不变
public interface BookDao {
void save();
void update();
void add();
}
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save...");
}
public void update() {
System.out.println("book dao update...");
}
public void add() {
System.out.println("book dao add...");
}
}
将共性功能抽取到一个通知类下的通知中, 通知类中需要使用@Component注解, 让Spring识别该类为bean; 在通过@Aspect注解告知Spring当前类为切面类
@Component
@Aspect
public class MyAdvice {
public void method() {
System.out.println(System.currentTimeMillis());
}
}
@Pointcut定义切入点, 切入点要求依托在一个不具有实际意义的方法, 即无参数, 无返回值, 方法体无实际逻辑
@Component
@Aspect
public class MyAdvice {
// 表示包下无返回值无参数的update方法
@Pointcut("execution(void com.chenyq.dao.BookDao.update())")
private void pt() {}
public void method() {
System.out.println(System.currentTimeMillis());
}
}
绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置, 由于我们希望在方法之前执行, 所以使用@Before注解
@Component
@Aspect
public class MyAdvice {
// 表示包下无返回值无参数的update方法
@Pointcut("execution(void com.chenyq.dao.BookDao.update())")
private void pt() {}
// 描述切入点与通知之间的关系
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
Spring核心配置文件中, 通过@EnableAspectJAutoProxy开启Spring对AOP注解驱动支持
@Configuration
@ComponentScan("com.chenyq")
@EnableAspectJAutoProxy // 告知Spring有用注解开发的AOP
public class SpringConfig {
}
此时运行update方法也会打印当前的系统时间了, 我们就做到了在不改变源代码的基础上添加了新的功能
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
AOP工作流程
- Spring容器启动
- 读取所有
切面配置
中的切入点
注意: 是所有切面配置的切入点, 并不是所有配置的切入点, 例如下面代码, 我们有两个切入点, 但是只配置使用了一个切入点pt, ptx是没有配置使用的那么不会被读取的
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.chenyq.dao.BookDao.add())")
public void ptx() {}
@Pointcut("execution(void com.chenyq.dao.BookDao.update())")
private void pt() {}
// 描述切入点与通知之间的关系
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
- 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
匹配失败,创建对象
匹配成功,创建原始对象(
目标对象
)的代理
对象
- 获取bean执行方法
获取bean,调用方法并执行,完成操作
获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
目标对象(Target):
原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
代理(Proxy):
目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
AOP切入点表达式
切入点:要进行增强的方法
切入点表达式:要进行增强的方法的描述方式
对于任何一个方法, 它的表达形式是多种多样的:
例如我们有一个接口BookDao
package com.chenyq.dao;
public interface BookDao {
void update();
}
BookDao接口的实现类BookDaoImpl
@Repository
public class BookDaoImpl implements BookDao {
public void update() {
System.out.println("book dao update...");
}
}
描述上面的update方法, 我们就有两种方法进行描述:
描述方式一: 按照接口描述
// 执行com.chenyq.dao包下的BookDao接口中的无参数, 无返回值的update方法
execution(void com.itheima.dao.BookDao.update())
描述方式二: 按照类描述
// 执行com.chenyq.dao.impl包下的BookDaoImpl类中的无参数, 无返回值的update方法
execution(void com.itheima.dao.impl.BookDaoImpl.update())
切入点表达式标准格式如下:
动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)
execution(public User com.chenyq.service.UserService.findById(int))
动作关键字:描述切入点的行为动作,例如execution表示执行到指定切入点
访问修饰符:public,private等,可以省略
返回值: 方法返回值的类型
包名: 方法所在哪个包下
类/接口名: 方法所在的类或者接口
方法名
参数
异常名:方法定义中抛出指定异常,可以省略
在程序中, 我们可以使用通配符描述切入点,快速描述
*
: 单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
// 匹配chenyq包下的任意包中的UserService类或接口下的所有以find开头且带有一个任意参数,任意返回值的方法
execution(public * com.chenyq.*.UserService.find*(*))
..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
// 匹配com包下任意包中的UserService类或接口的findById方法, 且参数任意个(0个或多个)
execution(public User com..UserService.findById(..))
+
:专用于匹配子类类型
// 匹配任意返回值 任意包下以Service结尾的类或接口的子类的任意方法
execution(* *..*Service+.*(..))
切入点表达式书写技巧:
所有代码按照标准规范开发, 也就是命名要标准,否则以下技巧全部失效
描述切入点
通常描述接口
,而不描述实现类, 因为描述到实现类耦合过高访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
包名书写
尽量不使用..匹配
,效率过低,常用*做单个包描述匹配,或精准匹配接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll
参数规则较为复杂,根据业务方法灵活调整
通常不使用异常
作为匹配规则
AOP通知类型
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
AOP通知共分为5种类型:
前置通知
后置通知
环绕通知(重点)
返回后通知(了解)
抛出异常后通知(了解)
前置通知
名称:@Before
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行前运行
相关属性:value(默认):切入点方法名,格式为类名.方法名()
示例:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.chenyq.dao.BookDao.update())")
private void pt() {}
// 描述切入点与通知之间的关系
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
后置通知
名称:@After
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行后运行
相关属性:value(默认):切入点方法名,格式为类名.方法名()
示例:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.chenyq.dao.BookDao.update())")
private void pt() {}
@After("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
环绕通知
名称:@Around(重点,常用)
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法执行的前后运行
相关属性:value(默认):切入点方法名,格式为类名.方法名()
注意: 环绕操作, 我们需要接受一个ProceedingJoinPoint类型的参数, 该参数的proceed方法代表着对原始方法调用的操作; 简单来说就是proceed方法代表着什么执行原始方法
示例:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.chenyq.dao.BookDao.update())")
private void pt() {}
@Around("pt()")
public void method(ProceedingJoinPoint pjp) throws Throwable {
System.out.println(System.currentTimeMillis());
System.out.println("方法执行前的操作...");
pjp.proceed(); // 代表着原始方法发调用
System.out.println("方法执行后的操作...");
}
}
环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
对原始方法的调用可以不接收返回值,通知方法设置成void即可; 如果接收返回值,必须设定为Object类型, 并将原始方法的返回值返回出去
原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.chenyq.dao.BookDao.update())")
private void pt() {}
@Around("pt()")
public Object method(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("方法执行前的操作...");
Object res = pjp.proceed();// 接收原始方法的返回值
System.out.println("方法执行后的操作...");
// 将原始方法的返回值返回
return res;
}
}
返回后通知
名称:@AfterReturning(了解)
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行
示例:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.chenyq.dao.BookDao.update())")
private void pt() {}
@AfterReturning("pt()")
public void method() {
System.out.println("afterReturning...");
}
}
抛出异常后通知
名称:@AfterThrowing(了解)
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行, 有异常才会执行, 无异常不执行
示例:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.chenyq.dao.BookDao.update())")
private void pt() {}
@AfterThrowing("pt()")
public void method() {
System.out.println("AfterThrowing...");
}
}
五种通知类型中, 最常用的是环绕通知@Around
AOP通知获取数据
通知获取数据分三种:
获取切入点方法的参数
获取切入点方法返回值
获取切入点方法运行异常信息(了解)
AOP通知获取参数数据:
JoinPoint:适用于前置、后置、返回后、抛出异常后通知
- JoinPoint对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(String com.chenyq.dao.BookDao.findName(*))")
private void pt() {}
@Before("pt()")
public void methods(JoinPoint jp) {
// 通过JoinPoint获取参数
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
}
}
ProceedJointPoint:适用于环绕通知
@Aspect
public class MyAdvice {
@Pointcut("execution(String com.chenyq.dao.BookDao.findName(*))")
private void pt() {}
@Around("pt()")
public void methods(ProceedingJoinPoint pjp) throws Throwable {
// 环绕通知通过ProceedingJoinPoint获取参数
Object[] args = pjp.getArgs();
pjp.proceed();
System.out.println(Arrays.toString(args));
}
}
获取切入点方法返回值, 只有返回后通知和环绕通知可以获取
返回后通知获取返回值
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(String com.chenyq.dao.BookDao.findName(*))")
private void pt() {}
// 设置通过ret变量获取返回值
@AfterReturning(value = "pt()", returning = "ret")
public void methods(Object ret) {
System.out.println(ret);
}
}
环绕通知中可以手工书写对原始方法的调用,得到的结果即为原始方法的返回值
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(String com.chenyq.dao.BookDao.findName(*))")
private void pt() {}
@Around("pt()")
public void methods(ProceedingJoinPoint pjp) throws Throwable {
// 环绕通知通过ProceedingJoinPoint获取参数
Object res = pjp.proceed();
System.out.println(res);
}
}
获取切入点方法运行异常信息只有抛出异后常通知和环绕通知可以获取(了解)
抛出异常后通知可以获取切入点方法中出现的异常信息,使用形参可以接收对应的异常对象
@AfterThrowing(value = "pt()", throwing = "err")
public void afterThrowing(Throwable err) {
System.out.println(err);
}
环绕通知可以通过try…catch获取
@Around("pt()")
public void methods(ProceedingJoinPoint pjp) {
// 环绕通知通过ProceedingJoinPoint获取参数
Object res = null;
try {
res = pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println(res);
}
AOP总结
概念:AOP(Aspect Oriented Programming)面向切面编程,一种编程范式
作用:在不惊动原始设计的基础上为方法进行功能增强
核心概念:
代理(Proxy):SpringAOP的核心本质是采用代理模式实现的
连接点(JoinPoint): 在SpringAOP中,理解为任意方法的执行
切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
切面(Aspect):描述通知与切入点的对应关系
目标对象(Target):被代理的原始对象成为目标对象
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)
execution(* com.itheima.service.*Service.*(..))
切入点表达式描述通配符:
作用:用于快速描述,范围描述
*
:匹配任意符号(常用)
..
:匹配多个连续的任意符号(常用)
+
:匹配子类类型
切入点表达式书写技巧:
按标准规范开发
查询操作的返回值建议使用*匹配
减少使用…的形式描述包
对接口进行描述,使用表示模块名,例如UserService的匹配描述为Service
方法名书写保留动词,例如get,使用表示名词,例如getById匹配描述为getBy
参数根据实际情况灵活调整
通知类型:
前置通知
后置通知
环绕通知(重点)
- 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
- 环绕通知可以隔离原始方法的调用执行
- 环绕通知返回值设置为Object类型
- 环绕通知中可以对原始方法调用过程中出现的异常进行处理
返回后通知
抛出异常后通知
获取切入点方法的参数
JoinPoint:适用于前置、后置、返回后、抛出异常后通知,设置为方法的第一个形参
ProceedJointPoint:适用于环绕通知
获取切入点方法返回值
返回后通知
环绕通知
获取切入点方法运行异常信息
抛出异常后通知
环绕通知