AOP相关术语
- 切面(Aspect):切面是对象操作过程中的截面。实际上"切面"就是一段代码,这段代码将被"植入"到程序流程中。
- 链接点(Join Point):连接点是对象操作过程中的某个阶段点(代码的某个位置,如某个方法执行前或执行后)。它实际上是对象的一个操作,例如:对象调用某个方法,读写对象的实例,或者某个方法抛出了异常等等。
- 切入点(Pointcut):切入点是连接点的集合,它是"切面"注入到程序中的位置,换句话说,"切面"是通过切入点被"注入"的。程序中可以有很多个切入点。
- 通知(Advice):通知是某个切入点被横切后,所采取的处理逻辑。在"切入点"处拦截程序后,通过通知来执行切面。
- 目标对象(Target):所有被通知的对象(也可以理解为被代理的对象)都是目标对象。目标对象被AOP所关注。AOP会注意目标对象的变动,随时准备向目标对象"注入切面"。
- 织入(Weaving):织入是将切面功能应用到目标对象的过程。AOP织入的方式有3种:编译时(Compile time)织入、类加载时期(Classload time)织入、执行期(Runtime)织入。Spring AOP一般多见于执行期织入。
AOP原理
Spring AOP默认使用JDK动态代理。如果被代理的类没有实现接口,则使用CGLIB动态代理。
JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
入门示例
pom引入aspectjweaver依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
注意:我们的示例是使用@Aspect注解,需要AspectJ 的aspectjweaver.jar库。此示例是在系列文章的基础上继续的,所以其他依赖项请自行补足。
编写需要织入切面的bean
package com.yyoo.boot.aop.beans;
import org.springframework.stereotype.Component;
@Component
public class TestBean1 {
public void test1(String str){
System.out.println("参数str:"+str);
}
}
编写切面类
package com.yyoo.boot.aop.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component // 需要把@Aspect(切面)类也交由Spring容器管理
public class AspectBean1 {
// 切入点表达式(此处表示com.yyoo.boot.aop.beans包下的所有方法)
@Pointcut("execution(* com.yyoo.boot.aop.beans..*.*(..))")
public void pointCut(){}
// 前置通知
@Before("com.yyoo.boot.aop.aspect.AspectBean1.pointCut()")
public void around(){
System.out.println("切面执行程序");
}
}
编写AppConfig
package com.yyoo.boot.aop;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
// 配置类中没有定义@bean所以我们没有添加@Configuration注解
@ComponentScan("com.yyoo.boot.aop")
@EnableAspectJAutoProxy // 使用@EnableAspectJAutoProxy注解启用@AspectJ支持
public class AppConfig {
}
示例程序
package com.yyoo.boot.aop;
import com.yyoo.boot.aop.beans.TestBean1;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Demo1 {
@Test
public void test(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
TestBean1 bean1 = context.getBean(TestBean1.class);
System.out.println(bean1);
bean1.test1("当前参数");
}
}
程序结构图
执行结果
com.yyoo.boot.aop.beans.TestBean1@76b1e9b8
切面执行程序
参数str:当前参数
我们示例中的切面类定义的切点(@Pointcut)注解了一个空方法,实际上我们的切面类可以简化为如下使用
package com.yyoo.boot.aop.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component // 需要把@Aspect(切面)类也交由Spring容器管理
public class AspectBean1 {
// 前置通知(直接月切点表达式一起使用)
@Before("execution(* com.yyoo.boot.aop.beans..*.*(..))")
public void around(){
System.out.println("切面执行程序");
}
}
切入点表达式
execution:用来匹配连接点的执行方法,这是Spring中使用的最主要的切入点定义方法
- execution(public * *(..)):匹配任意公共方法。
- execution(* set*(..)):匹配任何以"set"开头的方法。
- execution(* com.yyoo.boot.service.*.*(..)):匹配service包中的任意方法。
- execution(* com.yyoo.boot.service..*.*(..)):匹配service包及其子包中的任意方法。
within:通过类型来匹配连接点的执行方法
- within(com.yyoo.boot.service.*):匹配service包中的任意方法
- within(com.yyoo.boot.service…*):匹配service包及其子包中的任意方法。
this:用于匹配特定类型的连接点,但它是代理对象的范围内进行匹配
- this(com.yyoo.boot.service.AccountService):匹配代理对象中类型为AccountService的Java对象内的连接点。
target:也是用于匹配特定类型的连接点,但它是在目标对象的范围内进行匹配
- target(cn.hxex.springcore.service.AccountService):匹配目标对象中类型为AccountService的Java对象内的连接点
args:通过参数的类型来匹配特定的连接点
- args(java.io.Serializable):匹配只有一个参数并且该参数实现了Serializable接口的连接点
通过注解指定连接点
@target:匹配目标对象中的连接点
- @target(org.springframework.transaction.annotation.Transactional):匹配使用了@Transactional注解的目标对象中的连接点
@args:用于匹配参数实现特定注解的连接点
- @args(com.yyoo.boot.service.Classified):匹配只有一个参数并且该参数实现了@Classified注解的连接点
@within:匹配有指定注解的目标对象类型中的连接点
- @within(org.springframework.transaction.annotation.Transactional):匹配与指定了@Transactional注解的对象中类型相同的对象中的连接点
@annotation:用于匹配具有指定类型注解的连接点
- @annotation(org.springframework.transaction.annotation.Transactional):匹配任何有@Transactional注解的连接点
运算符
切入点表达式也可以使用&&(and)、||(or) 和!(negation) 运算符,如:
execution(* set*(…)) && execution(* com.yyoo.boot.aop.beans….(…))
Spring AOP advice通知类型
- @Before:前置通知,在切点执行之前执行
- @AfterReturning:返回后通知,在切点方法返回之后(在return之后)执行
- @AfterThrowing:异常后通知,在切点方法抛出异常后执行
- @After:后置通知,在切点方法有结果后(无论是异常结果还是正常结果)返回
- @Around:环绕通知,我们可以在切点方法执行前后添加代码逻辑。
@Before前置通知
@Before("execution(* com.yyoo.boot.aop.beans..*.*(..))")
public void before(){
System.out.println("切面执行程序");
}
执行结果
com.yyoo.boot.aop.beans.TestBean1@f78a47e
切面执行程序
参数str:当前参数
@AfterReturning返回后通知
@AfterReturning("execution(* com.yyoo.boot.aop.beans..*.*(..))")
public void afterReturning(){
System.out.println("切面执行程序");
}
执行结果
com.yyoo.boot.aop.beans.TestBean1@f78a47e
参数str:当前参数
切面执行程序
在切面中获取返回结果
修改TestBean1的方法如下
public String test1(String str){
System.out.println("参数str:"+str);
return "rs-" + str;
}
给我们的示例方法加入返回值
修改切面代码
@AfterReturning(value = "execution(* com.yyoo.boot.aop.beans..*.*(..))",returning = "rs")
public void afterReturning(String rs){
System.out.println("切面执行程序");
System.out.println("方法返回结果:"+rs);
}
注意:returning属性的名称要与方法上对应的参数名称一致
执行结果
com.yyoo.boot.aop.beans.TestBean1@6892b3b6
参数str:当前参数
切面执行程序
方法返回结果:rs-当前参数
@AfterThrowing异常后通知
我们再次修改TestBean1的方法
public String test1(String str){
System.out.println("参数str:"+str);
int a = 3/0;
return "rs-" + str;
}
就添加了一个除数为0的代码
@AfterThrowing(value = "execution(* com.yyoo.boot.aop.beans..*.*(..))",throwing = "ex")
public void afterThrowing(Exception ex){
System.out.println("切面执行程序");
System.out.println("方法返回结果:"+ex);
}
执行结果
com.yyoo.boot.aop.beans.TestBean1@2a265ea9
参数str:当前参数
切面执行程序
方法返回结果:java.lang.ArithmeticException: / by zero
异常打印....
@After后置通知
@After("execution(* com.yyoo.boot.aop.beans..*.*(..))")
public void after(){
System.out.println("切面执行程序");
}
执行结果
com.yyoo.boot.aop.beans.TestBean1@740cae06
参数str:当前参数
切面执行程序
异常打印....
@Around环绕通知
我们先将TestBean1的除数为0异常去掉
public String test1(String str){
System.out.println("参数str:"+str);
return "rs-" + str;
}
编写切面方法
@Around("execution(* com.yyoo.boot.aop.beans..*.*(..))")
public void around(ProceedingJoinPoint pjp){
System.out.println("切面前置逻辑代码");
try {
pjp.proceed(); // 执行原方法
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("切面后置逻辑代码");
}
执行结果
com.yyoo.boot.aop.beans.TestBean1@f78a47e
切面前置逻辑代码
参数str:当前参数
切面后置逻辑代码
pjp.proceed();这里还有个重载的方法pjp.proceed(Object[]);如果我们要在切面类中覆盖原来的参数,可以使用该方法
@Around("execution(* com.yyoo.boot.aop.beans..*.*(..))")
public void around(ProceedingJoinPoint pjp){
System.out.println("切面前置逻辑代码");
try {
pjp.proceed(new Object[]{"更改后的参数"}); // 执行原方法
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("切面后置逻辑代码");
}
执行结果
com.yyoo.boot.aop.beans.TestBean1@f78a47e
切面前置逻辑代码
参数str:更改后的参数
切面后置逻辑代码
@AfterReturning、@AfterThrowing、@After的区别
@AfterReturning:只在切点方法正确执行返回之后才执行。
@AfterThrowing:只在切点方法抛出异常后才执行。
@After:无论切点方法是否正确返回均会执行(类似于异常的finally块)
切面方法的参数JoinPoint
所有类型的通知,其通知方法的第一个参数为JoinPoint(如果要使用JoinPoint,那么它必须是第一个参数)。只有环绕通知@Around第一个参数是ProceedingJoinPoint,ProceedingJoinPoint是JoinPoint的子类。
JoinPoint常用方法
- getArgs():返回方法参数。
- getThis(): 返回代理对象。
- getTarget(): 返回目标对象。
- getSignature():返回所建议的方法的描述。
示例
@Around("execution(* com.yyoo.boot.aop.beans..*.*(..))")
public void afterReturning(ProceedingJoinPoint pjp){
System.out.println("切面前置逻辑代码");
try {
pjp.proceed(new Object[]{"更改后的参数"}); // 执行原方法
Object[] args = pjp.getArgs();
if(args != null){
for(int i = 0; i< args.length;i++){
System.out.print("\t参数["+i+"]:"+args[i]);
}
}else {
System.out.print("\t无参数");
}
System.out.println();
System.out.println(pjp.getSignature().getName());
System.out.println(pjp.getTarget());
System.out.println(pjp.getThis());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("切面后置逻辑代码");
}
本章只介绍了注解方式实现的Spring AOP,目前使用的更多的方式。Spring也提供了xml配置的方式,有兴趣可以自行到官网查看https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-schema