基于AspectJ注解实现AOP
AOP前奏:AOP的相关理论介绍
1、Spring对AOP的支持
Spring提供了3种类型的AOP支持:
- 基于AspectJ注解驱动的切面(推荐):使用注解的方式,这是最简洁和最方便的!
- 基于XML的AOP:使用XML配置,aop命名空间
- 基于代理的经典SpringAOP:需要实现接口,手动创建代理
2、AspectJ相关的注解
AspectJ相关注解:
- @Aspect:标记这个类是一个切面类。
AspectJ增强相关注解:
注解 | 描述 |
---|---|
@Before | 表示将当前方法标记为前置通知 |
@AfterReturning | 表示将当前方法标记为返回通知 |
@AfterThrowing | 表示将当前方法标记为异常通知 |
@After | 表示将当前方法标记为后置通知 |
@Around | 表示将当前方法标记为环绕通知 |
@Pointcut | 表示定义重用切入点表达式,一次定义,处处使用,一处修改,处处生效 |
@DeclareParents | 表示将当前方法标记为引介通知(不要求掌握) |
PointCut Designators 切点指示器),是切点表达式的重要组成部分
3、注解AOP的简单例子
①、编写代理对象接口
/**
* 代理对象接口
*/
public interface IUserService {
void addUser(String userName,Integer age);
}
②、编写代理对象接口的实现类
/**
* 目标类,代理对象实现类,会被动态代理
*/
@Service
public class UserServiceImpl implements IUserService{
@Override
public void addUser(String userName, Integer age) {
System.out.println(userName+":"+age);
}
}
③、编写切面类
注意:AspectJ切入点表达式语法:execution(<访问修饰符>? <返回类型> <全限定名>? <方法名称>(<参数类型>) <异常>?),通过execution函数,可以定义切入的方法。
代码块中带
?
符号的匹配式都是可选的,对于execution
必不可少的只有三个:
- 返回类型
- 方法名
- 参数
/**
* 创建日志切面类
*/
@Aspect // @Aspect注解标记这个类是一个切面类
@Component // @Component注解标记这个被扫描包扫描到时需要加入IOC容器
public class LogAspect { //定义一个日志切面类
// @Before注解将当前方法标记为前置通知
// value属性:配置当前通知的切入点表达式,通俗来说就是这个通知往谁身上套
@Before(value = "execution(public void com.thr.aop.target.UserServiceImpl.addUser(String,Integer))")
public void doBefore(JoinPoint joinPoint) { // 在通知方法中,声明JoinPoint类型的形参,就可以在Spring调用当前方法时把这个类型的对象传入
// 1.通过JoinPoint对象获取目标方法的签名
// 所谓方法的签名就是指方法声明时指定的相关信息,包括方法名、方法所在类等等
Signature signature = joinPoint.getSignature();
// 2.通过方法签名对象可以获取方法名
String methodName = signature.getName();
// 3.通过JoinPoint对象获取目标方法被调用时传入的参数
Object[] args = joinPoint.getArgs();
// 4.为了方便展示参数数据,把参数从数组类型转换为List集合
List<Object> argList = Arrays.asList(args);
System.out.println("[前置通知]"+ methodName +"方法开始执行,参数列表是:" + argList);
}
}
④、编写配置文件
<?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:context="http://www.springframework.org/schema/context"
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/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.thr.aop"/>
<!-- 开启基于AspectJ注解的AOP功能 -->
<aop:aspectj-autoproxy/>
</beans>
⑤、编写测试类
public class AOPTest {
//创建ApplicationContext对象
private ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
@Test
public void testAOP(){
// 1.从IOC容器中获取接口类型的对象
IUserService userService = ac.getBean(IUserService.class);
// 2.调用方法查看是否应用了切面中的通知
userService.addUser("张三",20);
}
}
⑥、运行结果
4、切入点表达式语法(重要)
在上面的例子中,切入点表达式是写死的,如果有很多地方要切入的话,就要在切面类中编写大量重复性的代码,扩展性和实用性不高,所以下面来学习一下更加强大的切入点表达式。
注意:AspectJ切入点表达式语法:execution(<访问修饰符>? <返回类型> <全限定名>? <方法名称>(<参数类型>) <异常>?),通过execution函数,可以定义切入的方法。代码块中带
?
符号的匹配式都是可选的,对于execution
必不可少的只有三个:
- 返回类型
- 方法名
- 参数
完整的传统切入点表达式:execution(public void com.thr.aop.target.UserServiceImpl.addUser(String,Integer))
上面最大可以简写为:execution(* *..*.*(..)) 表示匹配任意修饰符,返回值,包,类,方法,参数。
- 用
*
号代替“权限修饰符”和“返回值”部分,表示“权限修饰符”和“返回值”不限,即任意类型,注意:这里一个*
代表两部分,下面有介绍 - 在包名的部分,使用
*
表示包名任意 - 在包名的部分,使用
*..
表示包名任意、包的层次深度任意 - 在类名的部分,使用
*
号表示类名任意,也可以可以使用*
号代替类名的一部分,例如:
*Service
上面例子*Service
表示匹配所有类名、接口名以Service结尾的类或接口(*号位置不限)
- 在方法名部分,使用
*
号表示方法名任意,也可以使用*
号代替方法名的一部分,例如:
*Operation
上面例子*Operation
表示匹配所有方法名以Operation结尾的方法(*号位置不限)
- 在方法参数列表部分,使用
(..)
表示参数列表任意 - 在方法参数列表部分,使用
(int,..)
表示参数列表以一个int类型的参数开头,后面的任意 - 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
execution(public int *..*Service.*(.., int))
上面例子是对的,而下面例子是错的:
execution(* int *..*Service.*(.., int))
- 对于execution()表达式整体可以使用三个逻辑运算符号(了解,几乎不用)
- execution() || execution()表示满足两个execution()中的任何一个即可
- execution() && execution()表示两个execution()表达式必须都满足
- !execution()表示不满足表达式的其他方法
AOP切入点表达式补充:
上面相关函数的详细使用可以参考:spring aop中pointcut表达式完整版
4、重用切入点表达式
这里需要用到@Pointcut注解。在一处声明切入点表达式之后,在其它有需要的地方引用这个切入点表达式就好。易于维护,一处修改,处处生效。声明方式如下:
// 切入点表达式重用
@Pointcut("execution(* *..*.add*(..))")
public void doPointCut() {}
在同一个类内部引用时:
@Before(value = "doPointCut()")
public void doBefore(JoinPoint joinPoint) {
在不同类中引用:
@Before(value = "com.thr.aop.aspect.LogAspect.doPointCut")
public void doBefore(JoinPoint joinPoint) {
5、注解AOP的完整例子
基于前面简单的例子,除了切面类LogAspect代码需要改变之外,其它的类中代码都不变。
/**
* 创建日志切面类
*/
@Aspect // @Aspect注解标记这个类是一个切面类
@Component // @Component注解标记这个被扫描包扫描到时需要加入IOC容器
public class LogAspect { //定义一个日志切面类
// 使用@Pointcut注解重用切入点表达式
// 当前类引用时:doPointCut()
// 其他类引用时:com.thr.aop.aspect.LogAspect.doPointCut()
@Pointcut(value = "execution(* *..*.add*(..))")
public void doPointCut() {
}
// @Before注解将当前方法标记为前置通知
// value属性:配置当前通知的切入点表达式,通俗来说就是这个通知往谁身上套
@Before(value = "doPointCut()")
public void doBefore(JoinPoint joinPoint) { // 在通知方法中,声明JoinPoint类型的形参,就可以在Spring调用当前方法时把这个类型的对象传入
// 1.通过JoinPoint对象获取目标方法的签名
// 所谓方法的签名就是指方法声明时指定的相关信息,包括方法名、方法所在类等等
Signature signature = joinPoint.getSignature();
// 2.通过方法签名对象可以获取方法名
String methodName = signature.getName();
// 3.通过JoinPoint对象获取目标方法被调用时传入的参数
Object[] args = joinPoint.getArgs();
// 4.为了方便展示参数数据,把参数从数组类型转换为List集合
List<Object> argList = Arrays.asList(args);
System.out.println("[前置通知]" + methodName + "方法开始执行,参数列表是:" + argList);
}
// @AfterReturning注解将当前方法标记为返回通知
// 使用returning指定一个形参名,Spring会在调用当前方法时,把目标方法的返回值从这个位置传入
@AfterReturning(value = "doPointCut()", returning = "returnValue")
public void doAfterReturning(JoinPoint joinPoint, Object returnValue) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[返回通知]" + methodName + "方法成功结束,返回值是:" + returnValue);
}
// @AfterThrowing注解将当前方法标记为异常通知
// 使用throwing属性指定一个形参名称,Spring调用当前方法时,会把目标方法抛出的异常对象从这里传入
@AfterThrowing(value = "doPointCut()", throwing = "throwable")
public void doAfterThrowing(JoinPoint joinPoint, Throwable throwable) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[异常通知]" + methodName + "方法异常结束,异常信息是:" + throwable.getMessage());
}
// @After注解将当前方法标记为后置通知
@After(value = "doPointCut()")
public void doAfter(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[后置通知]" + methodName + "方法最终结束");
}
}
运行结果:
小细节,通知执行的顺序
- Spring版本5.3.x以前:
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
- Spring版本5.3.x以后:
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
6、环绕通知的举例
环绕通知就是前面四个通知的结合,但Spring官方建议选用“能实现所需行为的功能最小的通知类型”: 提供最简单的编程模式,减少了出错的可能性。,本例在环绕通知中触发异常通知。
①、修改代理对象接口的实现类
/**
* 目标类,会被动态代理
*/
@Service
public class UserServiceImpl implements IUserService {
@Override
public void addUser(String userName, Integer age) {
//出现异常
int i = 1;
int j = 0;
int x = i / j;
System.out.println(userName + ":" + age);
}
}
②、编写环绕通知切面类
/**
* 创建日志环绕通知切面类
*/
@Aspect // @Aspect注解标记这个类是一个切面类
@Component // @Component注解标记这个被扫描包扫描到时需要加入IOC容器
public class Log1Aspect { //定义一个日志切面类
// 使用@Pointcut注解重用切入点表达式
// 当前类引用时:doPointCut()
// 其他类引用时:com.thr.aop.aspect.LogAspect.doPointCut()
@Pointcut(value = "execution(* *..*.add*(..))")
public void doPointCut() {
}
// 使用表示当前方法是环绕通知
@Around(value = "doPointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
// 获取目标方法名
String methodName = joinPoint.getSignature().getName();
// 声明一个变量,用来接收目标方法的返回值
Object targetMethodReturnValue = null;
// 获取外界调用目标方法时传入的实参
Object[] args = joinPoint.getArgs();
try {
// 调用目标方法之前的位置相当于前置通知
System.out.println("[环绕通知]" + methodName + "方法开始执行,参数列表:" + Arrays.asList(args));
// 通过ProceedingJoinPoint对象的proceed(Object[] var1)调用目标方法
targetMethodReturnValue = joinPoint.proceed();
// 调用目标方法成功返回之后的位置相当于返回通知
System.out.println("[环绕通知]" + methodName + "方法成功返回,返回值是:" + targetMethodReturnValue);
} catch (Throwable throwable) {
throwable.printStackTrace();
// 调用目标方法抛出异常之后的位置相当于异常通知
System.out.println("[环绕通知]" + methodName + "方法抛出异常,异常信息:" + throwable.getMessage());
} finally {
// 调用目标方法最终结束之后的位置相当于后置通知
System.out.println("[环绕通知]" + methodName + "方法最终结束");
}
// 将目标方法的返回值返回
// 这里如果环绕通知没有把目标方法的返回值返回,外界将无法获取这个返回值数据
return targetMethodReturnValue;
}
}
③、运行结果
7、切面的优先级
[1]概念:相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用@Order注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
[2]实际意义:实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。
此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。
参考资料:
- 好文,写得比较全面:https://zhuanlan.zhihu.com/p/25522841
- 写得不错,可以参考学习一下:https://blog.csdn.net/mu_wind/article/details/102758005