Spring3️⃣浅聊AOP、AspectJ
1、浅聊 AOP
1.1、AOP
Aspect-oriented Programming(面向切面编程)
通过预编译方式和运行时动态代理实现程序功能的统一维护。
- 好处:对业务逻辑的各个部分进行隔离,降低耦合度,提高程序的可重用性、开发效率。
- 作用:
- 提供声明式企业服务(如声明式事务管理)
- 允许自定义切面,以 AOP 来补充 OOP。
1.2、动态代理
代理(Proxy)(设计模式)
给某个对象提供一个代理,以控制对该对象的访问
Spring AOP 底层使用了动态代理。
JDK 代理 | CGLIB 代理 | |
---|---|---|
使用前提 | 目标对象有实现接口 | 目标对象非 final 修饰 |
技术 | 反射机制,生成实现类 | asm 开源包,修改字节码创建子类 |
代理实现 | 实现接口 | 继承类并重写方法 |
1.3、术语
AOP 中的术语
结合例子理解:假设以下情景
- UserServiceImpl 实现了 UserService 接口(有增删改查 4 个方法)
- 编写一个 MyLog 类,声明一个打印日志的方法 printLog()。
- 要在 add() 和 delete() 方法执行前调用 printLog() 方法。
- AOP 会生成代理类(假设称为 UserServiceProxy):实现 UserService 接口,在 add() 和 delete() 方法中先调用 printLog(),再执行方法。
注意区分理解
- 连接点和切入点:连接点是所有可以被增强的方法,而切入点其中的一个或多个方法,是实际被增强的。
- 横切关注点和通知:横切关注点是所有可以被注意和加强程序的功能,而通知是其中的一个或多个,是实际运用到的。
含义 | 例子 | ||
---|---|---|---|
目标对象 | Target | 被通知(增强)的对象,也称为通知对象 | UserServiceImpl |
连接点 | JoinPoint | 程序执行过程中的一个点。 即类中可以被增强的方法 |
增删改查 4 个方法 |
切入点 | Pointcut | 实际被增强的方法 | add()、delete() 方法 |
*横切关注点 | 跨越多个类的方法或功能。 即与业务逻辑无关,但是需要关注的部分 |
安全、日志、缓存、事务等 | |
通知 | Advice | 在切入点采取的行动,即增强的功能 | 日志,即 printLog() 方法 |
切面 | Aspect | 横切关注点的模块化 (即所在类) |
MyLog 类 |
代理 | Proxy | AOP 创建的对象,用于执行通知的方法等 | UserServiceProxy 类 |
织入 | Weaving | 将切面与其他应用程序或对象联系起来,以创建目标对象,在运行时执行 | 将日志功能添加到 add()、delete() 方法的过程 |
1.4、通知 Advice
1.4.1、通知类型
切入点可采用的通知有以下 5 种类型。
名称 | 时机 | 场景 | |
---|---|---|---|
前置 | Before | 切入点之前 | 日志 |
后置 | AfterReturning | 切入点正常执行后 | 日志 |
环绕 | Around | 前置 + 后置 | 事务、权限 |
异常 | AfterThrowing | 切入点执行发生异常 | 异常处理、事务 |
最终 | After | 切入点后(即使有异常) | 日志 |
1.4.2、通知执行顺序
2、AspectJ
2.1、概述
AspectJ 是一个实现了 AOP 的框架,不属于 Spring。
Spring 通常使用 AspectJ 进行 AOP 操作。
-
Maven 依赖:使用 AspectJ 需要导入相关依赖
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.7</version> </dependency>
-
XML 配置文件:使用 AOP 需要导入相关命名空间
xmlns:aop="http://www.springframework.org/schema/aop" http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"
-
AOP 实现方式
- 基于 XML 配置(了解即可)
- 基于注解(实际使用)
-
注意:向容器中注册或获取对象的区别
- 注册:需要以实现类型注册而非接口类型(原因:注册的是 bean 对象)
- 获取:需要以接口类型获取而非实现类型(原因:AOP 底层生成代理类,而非实现类本身)
2.2、切入点表达式
切入点表达式:表示对哪个类的哪个方法进行增强。
语法:execute([权限修饰符] 返回值类型 全限类名.方法名(方法参数))
- 权限修饰符:可省略
- 返回值类型、全限类名、方法名:可使用通配符
*
- 方法参数:
..
代表任意个数、任意类型的形参列表
示例
// 对indi.jaywee.pojo.User类的add()
execution(* indi.jaywee.pojo.User.add(..))
// 对indi.jaywee.pojo.User类的所有方法
execution(* indi.jaywee.pojo.User.*(..))
// 对indi.jaywee.pojo包的所有类的所有方法
execution(* indi.jaywee.pojo.*.*(..))
2.3、实现
本例中会使用到 AOP 术语,可以回顾 1.3 节
以 UserService 为例。
public interface UserService {
void add();
void delete();
}
public class UserServiceImpl implements UserService {
@Override
public void add() { System.out.println("add..."); }
@Override
public void delete() { System.out.println("delete..."); }
}
2.3.1、基于注解实现
在实际开发中,使用注解实现 AOP 技术。
配置
-
导入 AspectJ 的 Maven 依赖
-
导入相关命名空间、开启注解扫描
-
开启 AspectJ 自动代理
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="包名"> </context:component-scan> <aop:aspectj-autoproxy/> </beans>
目标对象
注册 bean:UserServiceImpl 类添加 @Service 注解
- 注意:是在实现类添加注解,而不是在接口上添加。
- 原因:是将具体的一个实现类注入到 IoC 容器中,作为一个 bean 对象。
创建切面
- 切面:创建一个类,添加 @Aspect 注解。
- 通知:创建一个方法,添加相应类型的注解、切入点表达式。
-
注册 bean:@Component
-
注册切面:@Aspect
-
使用通知
// 省略部分代码 @Component @Aspect public class MyLog { @Before("execution(* indi.jaywee.annotation.UserService.*(..))") public void beforeLog() {...} @AfterReturning("execution(* indi.jaywee.annotation.UserService.*(..))") public void afterReturningLog() {...} @Around("execution(* indi.jaywee.annotation.UserService.*(..))") public void aroundLog(ProceedingJoinPoint joinPoint) throws Throwable { // 1、环绕前 // 2、执行连接点 joinPoint.proceed(); // 3、环绕后 } @AfterThrowing("execution(* indi.jaywee.annotation.UserService.*(..))") public void afterThrowingLog() {...} @After("execution(* indi.jaywee.annotation.UserService.*(..))") public void afterLog() {...} }
测试一下
- 需要以接口类型获取 bean:因为获取的是代理类,而不是原本的实现类。
- 顺序:正常执行时、发生异常时(手动在 add() 中添加一个异常)
-
正常
-
异常
抽取切入点
从以上例子看出:不同通知的使用注解的不同,但切入点表达式相同。
-
使用 @PointCut 注解,定义切入点表达式。
-
通知只需引用该表达式。
// @Pointcut只能作用于方法,声明一个空方法,方法名即为表达式名 @Pointcut("execution(* indi.jaywee.annotation.UserService.*(..))") public void expression() { } // 通过方法名引用切入点表达式 @Before("expression()") public void beforeLog() { System.out.println("== 前置"); }
2.3.2、基于 XML 实现
XML 方式实现,了解即可。
有 2 种实现方式
- 自定义切面,通过配置文件注册
- 使用 Spring 提供的 Advice 相关的 API
以环绕通知为例,演示 XML 的自定义切面方式。
创建切面
public class MyXmlLog {
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("===== 环绕 =====");
long start = System.nanoTime();
joinPoint.proceed();
long end = System.nanoTime();
System.out.println("== 耗时" + (end - start) / 1000 + "微秒 ==");
}
}
配置
-
注册 bean
-
AOP 配置:
<aop:config>
标签-
切入点:
<aop:pointcut>
-
切面、通知:
<aop:aspect>
注册切面,<aop:around>
注册通知并关联切入点。<bean id="userService" class="indi.jaywee.annotation.UserServiceImpl"> </bean> <bean id="xmlLog" class="indi.jaywee.annotation.MyXmlLog"> </bean> <aop:config> <!-- 切入点 --> <aop:pointcut id="userPoint" expression="execution(* indi.jaywee.annotation.UserService.*(..))"/> <!-- 切面 --> <aop:aspect ref="xmlLog"> <!-- 通知 --> <aop:around method="aroundAdvice" pointcut-ref="userPoint"/> </aop:aspect> </aop:config>
-