AOP前奏:AOP的相关理论介绍
Spring提供了3种类型的AOP支持:
AspectJ相关注解:
AspectJ增强相关注解:
注解 | 描述 |
---|---|
@Before | 表示将当前方法标记为前置通知 |
@AfterReturning | 表示将当前方法标记为返回通知 |
@AfterThrowing | 表示将当前方法标记为异常通知 |
@After | 表示将当前方法标记为后置通知 |
@Around | 表示将当前方法标记为环绕通知 |
@Pointcut | 表示定义重用切入点表达式,一次定义,处处使用,一处修改,处处生效 |
@DeclareParents | 表示将当前方法标记为引介通知(不要求掌握) |
PointCut Designators 切点指示器),是切点表达式的重要组成部分
①、编写代理对象接口
/** * 代理对象接口 */ 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); } }
⑥、运行结果
在上面的例子中,切入点表达式是写死的,如果有很多地方要切入的话,就要在切面类中编写大量重复性的代码,扩展性和实用性不高,所以下面来学习一下更加强大的切入点表达式。
注意: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))
AOP切入点表达式补充:
上面相关函数的详细使用可以参考:spring aop中pointcut表达式完整版
这里需要用到@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) {
基于前面简单的例子,除了切面类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官方建议选用“能实现所需行为的功能最小的通知类型”: 提供最简单的编程模式,减少了出错的可能性。,本例在环绕通知中触发异常通知。
①、修改代理对象接口的实现类
/** * 目标类,会被动态代理 */ @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; } }
③、运行结果
[1]概念:相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
使用@Order注解可以控制切面的优先级:
[2]实际意义:实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。
此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。
参考资料: