简介
面向切面编程,一种编程范式,指导开发者如何组织程序结构。可以在不经打原始设计的基础上为其进行功能增强。
入门案例
案例:在接口执行前输出当前系统时间
开发模式:XML 或者 注解
思路分析:
- 导入坐标(
pom.xml
) - 制作连接点方法(原始操作、Dao接口实现类)
- 制作共性功能 (通知类与通知)
- 定义切入点
- 绑定切入点与通知关系(切面)
导入坐标
<?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>com.itheima</groupId>
<artifactId>spring_18_aop_quickstart</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
</project>
spring
配置
@Configuration
@ComponentScan("com.itheima")
//开启注解开发AOP功能
@EnableAspectJAutoProxy
public class SpringConfig {
}
Dao接口实现类
@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 ...");
}
}
制作共性功能
aop/MyAdvice
//通知类必须配置成Spring管理的bean
@Component
//设置当前类为切面类
@Aspect
public class MyAdvice {
//设置切入点,要求配置在方法上方,方法名任意
// 切人点依托一个不具有实际意义的方法进行,即无参数,无返回值,方法体无实际逻辑
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt() {
}
//设置在切入点pt()的前面运行当前操作(前置通知)
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
运行update
方法时也会执行公关方法
AOP工作流程与核心概念
工作流程
1、Spring
容器启动
2、读取所有切面配置中的切人点(需要被使用的)
3、初始化bean
,判定bean
对应的类中的方法是否匹配的任意切入点
- 匹配失败,创建对象(初始化bean)
- 匹配成功,创建原始对象(目标对象)的代理对象
4、获取bean
执行方法
- 获取
bean
,调用方法并执行,完成操作 - 获取的
bean
是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作。
核心概念
- 目标对象:原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
- 代理:目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
SpringAOP
本质是代理模式
切入点表达式
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
// 方式1:执行com.itheima.dao包下的BookDao接口中的无参数update方法
execution(void com.itheima.dao.BookDao.update())
// 方式2:执行com.itheima.dao.impl包下的BookDaoImpl类中的无参数update方法
execution(void com.itheima.dao.impl.BookDaoImpl.update())
// 访问修饰符、异常名可以省略
execute(public User com.itheima.service.UserService.findById(int))
可以使用通配符描述切入点,进行快速描述
*
:单个独立的任意符号(返回类型、任意的字符串、任意类型的参数),可以独立出现,也可以作为前缀或者后缀的匹配符出现
// 匹配com.itheima包下的任意包中的User Service类或接口中所有find开头的带有一个参数的方法
execution (public * com.itheima.*.UserService.find*(*))
..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
// 匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
execution (public User com..UserService.findById(..))
书写技巧
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点通常描述接口,而不是描述实现类,避免耦合
- 访问控制修饰符针对接口开发采用public描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用
*
通配符快速描述 - 包名书写尽量不使用
..
匹配,效率过低,常采用*
做单个包描述匹配或精准匹配 - 接口名/类名书写名称与模块相关的采用
*
匹配,例如UserService
书写*Service
,绑定业务层接口名 - 方法名书写一动词进行精准匹配,名词采用
*
匹配,列入getById
书写成getBy*
- 参数规则较为复杂,根据业务方法灵活调整
- 通常不使用异常作为匹配规则
AOP通知类型
- AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
- AOP通知共分为5种类型
- 前置通知
- 后置通知
- 环绕通知(重点)
- 返回后通知(了解)
- 抛出异常后通知(了解)
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}
//@Before:前置通知,在原始方法运行之前执行
// @Before("pt()")
public void before() {
System.out.println("before advice ...");
}
//@After:后置通知,在原始方法运行之后执行
// @After("pt2()")
public void after() {
System.out.println("after advice ...");
}
//@Around:环绕通知,在原始方法运行的前后执行
// @Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
// @Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
Integer ret = (Integer) pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
//@AfterReturning:返回后通知,在原始方法执行完毕后运行,且原始方法执行过程中未出现异常现象
// @AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
//@AfterThrowing:抛出异常后通知,在原始方法执行过程中出现异常后运行
@AfterThrowing("pt2()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
@Around注意事项
- 环绕通知必须依赖形参
ProceedingJoinPoint
才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知 - 通知中如果未使用
ProceedingJoinPoint
对原始方法进行调用将跳过原始方法的执行 - 对原始方法的调用可以不接收返回值,通过方法设置成
void
即可,如果接收返回值,必须设定为Object
类型 - 原始方法的返回值如果是
void
类型,通知方法的返回值类型可以设置为void
,也可以设置成Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出
Throwable
对象
案例,测试执行效率
@Component
@Aspect
public class ProjectAdvice {
//匹配业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt() {
}
//设置环绕通知,在原始操作的运行前后记录执行时间
@Around("ProjectAdvice.servicePt()")
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
//获取执行的签名对象
Signature signature = pjp.getSignature();
// 获取类名
String className = signature.getDeclaringTypeName();
// 获取方法
String methodName = signature.getName();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println("万次执行:" + className + "." + methodName + "---->" + (end - start) + "ms");
}
}
总结
- 概念:
AOP
面向切面编程,一种编程范式 - 作用:在不惊动原始设计的基础上为方法进行功能增强
- 核心概念
- 代理:
SpringAOP
的核心本质是采用代理模式实现的 - 连接点:在
SpringAOP
中,理解为任意方法的执行 - 切入点:匹配连接点的式子,也是具有共性功能的方法描述
- 通知:若干个方法的共性功能,在切入点处执行,最终体现为一个方法
- 切面:描述通知与切入点的对应关系
- 目标对象:被代理的原始对象成为目标对象
- 代理: