三、AOP 的通知处理
Spring 提供了五大通知类型,每个通知的执行的位置不一样,我们可以通过在定义通知的方法上添加参数来获取当前 AOP 拦截切点的一些信息,比如拦截目标类,代理类,方法参数等等。
1. JoinPoint
在前置通知、后置通知、最终通知和异常通知中,可以在方法的第一个参数使用JoinPoint
接口,他提供了一些方法如下:
getArgs()
:返回方法的参数getThis()
:返回代理对象getTarget()
:返回目标对象getSignature()
:返回被代理对象的方法和类相关的信息
2. ProceedingJoinPoint
在环绕通知中,可以在方法中使用ProceedingJoinPoint
接口,它是JoinPoint
接口的子接口,增加的proceed()
方法用来调用被代理方法,它的返回值也就是调用被代理方法执行完的返回值,如果一个切面配置了多个通知,它会调用下一个通知,我们也可以使用另一个方法proceed(Object[])
来给代理的方法传递参数。
注意:虽然在环绕通知中,
proceed()
方法不调用或调用多次都是合法的,但最好调用且只调用一次。
3. Advice 相关接口
在某些特殊场景,我们需要自定义一个通知,例如使用 Advisor 来实现定义切面,此时需要实现不同的接口来实现不同的通知。
3.1 MethodBeforeAdvice
通过实现MethodBeforeAdvice
接口来实现前置通知。
public class AopBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { // ... } }
3.2 AfterReturningAdvice
通过实现AfterReturningAdvice
接口来实现后置通知。
public class AopAfterReturningAdvice implements AfterReturningAdvice { @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { // ... } }
3.3 MethodInterceptor
通过实现MethodInterceptor
接口来实现环绕通知,proceed()
的用法和前面讲解的ProceedingJoinPoint
用法类似。
public class AopAroundAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { return invocation.proceed(); } }
3.4 ThrowsAdvice
通过实现ThrowsAdvice
接口来实现异常通知,这个接口没有任何方法,通过特定的方法来实现异常通知功能,支持的方法形式如下:
public void afterThrowing(Exception ex) public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
上面两种方法中的Exception
参数可以换成其他特定的异常类型,表示只抛出该类型异常时才执行通知。比如定义只抛出空指针才执行的异常通知:
public class AopAfterThrowingAdvice implements ThrowsAdvice { public void afterThrowing(Method method, Object[] args, Object target, NullPointerException ex) { // ... } }
4. 通知的执行顺序
当同一个切点上存在多个通知时,在“进入”连接点情况下,优先级高的通知先执行(给定两个前置通知,优先级高的先执行),在“退出”连接点情况下,优先级高的通知后执行(给定两个后置通知,优先级高的后执行)。
当同一个切点上存在不同的切面的两个通知,除非你指定,否则执行顺序是未知的,你可以在切面类实现Ordered
接口来指定优先级,或者使用@Order
注解来指定,Ordered.getValue()
返回的值(或注解的值)越小的优先级越高。
在基于 XML 方式实现时,可以在
和
标签中配置
order
属性来配置切面的优先级。
当同一个切点上存在相同切面的两个通知,执行顺序是未知的,因为没有方法通过反射 javac 编译的类来获取声明顺序。
四、AOP 的切点定义
切点控制通知执行的范围,Spring AOP 只支持 Spring beans 的方法切入,所以你定义的切点表达式只能拦截 Spring beans 的方法。
1.execution
匹配方法执行切点,它是 Spring AOP 中最原始的切点表达式。它的一般格式如下:
// 中括号里面的可以省略 execution([方法访问权限] 返回类型 [类全路径名.]方法名(参数类型) [异常类型]) // 示例 execution(* cn.codeartist.spring.aop.service..*.*(..))
只有execution
表达式匹配到的方法,才能被拦截,切点表达式支持一些简单的通配符,*
号表示单个任何类型,..
符号表示任意个,例如参数类型的匹配,()
匹配没有参数的方法,(..)
匹配任意个参数的方法,(*)
匹配一个参数的方法,(*,String)
匹配两个参数的方法,并且第二个参数必须为String
类型。还有其他示例:
// 匹配任何public访问权限的方法 execution(public * *(..)) // 匹配所有以set开头命名的方法 execution(* set*(..)) // 匹配AccountService接口(或类)的所有方法 execution(* com.xyz.service.AccountService.*(..)) // 匹配service包下所有类的方法 execution(* com.xyz.service.*.*(..)) // 匹配service包下及其子包下所有类的方法 execution(* com.xyz.service..*.*(..))
2.within
匹配指定类里面的方法,不匹配接口,within
表达式的参数指定的是类全路径名。例如:
// 匹配service包下所有类的方法 within(com.xyz.service.*) // 匹配service包下及其子包下所有类的方法 within(com.xyz.service..*)
3.target
匹配目标类对象里面的方法,目标类就是我们在代码中定义的类,target
表达式的参数指定的是类全路径名。例如:
// 匹配实现AccountService接口的类对象的方法 target(com.xyz.service.AccountService)
4.this
匹配代理类对象里面的方法,我们在后面会讲到 Spring AOP 代理机制,那样会更好理解this
表达式,this
表达式的参数指定的是类全路径名。
// 匹配实现AccountService接口的代理类对象的方法 this(com.xyz.service.AccountService)
5.args
匹配方法里面的参数类型,args
表达式的参数指定的是一个或多个参数的类全路径名。例如:
// 匹配参数只有一个且属于Serializable类型(或实现Serializable接口的类)的所有方法 args(java.io.Serializable)
该表达式与execution(* *(java.io.Serializable))
表达式功能不一样,args
匹配参数在运行时类型为Serializable
类型,也可以是Serializable
的子类,而execution
匹配方法声明的参数类型为Serializable
类型。
6.bean
匹配 Bean 名称,也支持*
通配符。例如:
// 匹配在Spring容器中,bean名称为tradeService的类方法 bean(tradeService) // 匹配在Spring容器中,bean名称以Service结尾所有类的方法 bean(*Service)
在前面讲解的几种表达式,within
、this
和target
都是对类型的匹配,within
只能匹配类而不能匹配接口,target
匹配目标类,对接口和类都能够匹配到,this
匹配代理类,所以使用 JDK 动态代理的时候,不能匹配到实现接口的类,原因我们会在 AOP 代理机制中讲到。匹配情况如下:
表达式拦截位置 | within |
this |
target |
接口 | ✘ | ✔ | ✔ |
实现接口的类 | ✔ | 〇 | ✔ |
不实现接口的类 | ✔ | ✔ | ✔ |
7.@annotation
匹配带有指定注解的方法,对接口中方法的注解无效。例如:
// 匹配所有含有@Transactional注解的方法 @annotation(org.springframework.transaction.annotation.Transactional)
8.@within
匹配带有指定注解的执行方法所在的类,对接口无效,在父类中使用,如果子类没有重写方法也会生效。例如:
// 匹配所有含有@Transactional注解的类 @within(org.springframework.transaction.annotation.Transactional)
9.@target
匹配带有指定注解的目标类对象,对接口无效,对父类无效,它要求对象的运行时类型必须与被注解的目标类型是同一个类型。例如:
// 匹配所有含有@Transactional注解的目标类 @target(org.springframework.transaction.annotation.Transactional)
10.@args
匹配方法参数带有指定注解的方法。例如:
// 匹配运行时参数含有@Classified注解的方法 @args(com.xyz.security.Classified)
前面讲解的几种注解拦截表达式中,表达式参数都是注解类型,@within
和@target
都是对类型是否含有注解的匹配,只需要记住,@within
匹配方法实际执行所在的类,@target
只匹配类型。匹配情况如下:
注解位置 | @within |
@target |
接口 | ✘ | ✘ |
实现接口的类 | ✔ | ✔ |
不实现接口的类 | ✔ | ✔ |
父类(子类重写方法) | ✘ | ✘ |
父类(子类未重写方法) | ✔ | ✘ |
子类(子类重写方法) | ✔ | ✔ |
子类(子类未重写方法) | ✘ | ✔ |
11. 切点表达式的组合
你可以使用&&
、||
和!
来组合多个切点表达式,表示多个表达式“与”、“或”和“非”的逻辑关系。例如:
// 匹配符合anyOldTransfer()切点表达式并且参数第一个为Account类型的方法 @Before("anyOldTransfer() && args(account,..)") public void validateAccount(Account account) { // ... } // 参数绑定 @Before("@annotation(demo)") public void doAtMethod(Demo demo) { // ... }
切点表达式中的参数类型,可以与通知方法的参数通过名称绑定,这样在表达式中就不需要写类或注解的全路径名,而且能直接获取到切点拦截的参数信息。
为了使得切点的匹配性能达到最佳,在编写表达式匹配切点目标时,应该尽可能缩小匹配范围,存在的切点表达式可以分为下面三大类:
- 类型表达式:匹配某个特定的切入点,例如
execution
等 - 作用域表达式:匹配某组特定的切入点,例如
within
等 - 上下文表达式:基于上下文匹配某些特定的切入点,例如
this
、target
和@annotation
等
一个好的切点表达式应该至少包含前两种(类型和作用域),你可以使用上下文表达式来基于切入点上下文匹配或在通知中绑定上下文。单独使用类型表达式或上下文表达式比较消耗性能(时间或内存)。作用域表达式匹配的性能非常快,所以表达式中尽可能的使用作用域类型。
五、AOP 代理机制
Spring AOP 是基于代理实现的,默认使用标准 JDK 动态代理,只有在接口被代理的时候启用。AOP 代理也使用 CGLib 动态代理,在类被代理的时候启用,也就是类没有实现任何接口的时候。
1. JDK动态代理与CGLib动态代理
JDK 动态代理是通过实现接口来生成代理类,而 CGLib 动态代理是通过继承类来生成代理子类,通过子类对父类方法的重写覆盖来实现代理,所以 CGLib 对final
修饰的方法不能代理。
在 Spring AOP 中,你可以通过以下配置强制使用 CGLib 代理:
基于 XML 配置
<aop:config proxy-target-class="true"> <!-- other beans defined here... --> </aop:config>
基于注解配置
<aop:aspectj-autoproxy proxy-target-class="true"/> @EnableAspectJAutoProxy(proxyTargetClass = true)
在前面讲解切点表达式时,this
用来匹配代理类,使用 JDK 动态代理时,实现接口的类与代理类只是实现了同一个接口,不是同一个类型,所以无法匹配到。而使用 CGLib 动态代理时,代理类是接口实现类的子类,所以使用this
能匹配到。