Spring AOP应用

Spring AOP应用

建议阅读AOP相关文章顺序:

这里直接对 Spring AOP 以注解方式实现和配置文件方式实现的应用。

本人写的代码仓库位置:gitee.com/old_yogurt/...

主要是基于spring 源码的5.0做的;想看Spring AOP源码所有类功能归纳说明,流程流转看下面的文章;

gitee.com/old_yogurt/... 工程中对多数AOP 代码做的注释,每个package-info.java都有对该包中类功能的归类说明注释。

最精彩的 第六节对于通知循环的解释。

一、注解、配置的专业术语介绍

这三个是我们写代码看得见摸得着的:

  • 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。

    ​ (BeforeAdvice 前置通知,AfterAdvice 后置通知等,这些就是通知。)

  • 切入点(PointCut): 可以插入增强处理的 连接点

  • 切面(Aspect): 切面是 通知切点 的结合。

这三个说实话,不看源码,都是在瞎说:

  • 连接点(JoinPoint): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。

    这个连接点术语 不易说明,看源码才能多少明白,就是拦截器链 执行整个通知的流转中,方法调用的**点**。

  • 引介(Introduction):引介是一种特殊的增强,引介允许我们向现有的类添加新的方法或者属性。

    ​ 这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。

    ​ 想看 这个功能的话,可以看gitee.com/old_yogurt/... 我这个项目里的 aopintroduction 包中,关于引介增强器的demo。

  • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。(把切面应用到目标对象并创建新的代理对象的过程)

  • 增强器(Advisor):也是 通知 + 切入点的结合;但是增强器也可以是一个拦截器,引介(也可以叫做一个增强器,也是一个拦截器);这些都是本人在看源码时候自身体会,可能也不太对。

二、表达式介绍

AspectJ指示器 描述
arg() 限制连接点 匹配参数为指定类型 的执行方法
@arg() 限制连接点 匹配参数由指定注解标注 的执行方法
execution() 用于 匹配是连接点 的执行方法
this() 限制连接点 匹配AOP代理的Bean引用 为指定类型 的类
target() 限制连接点 匹配目标对象为指定类型 的类
@target() 限制连接点 匹配特定的执行对象,这些对象对应的类要具备指定类型的注解
within() 限制连点 匹配指定的类型
@within() 限制连点 匹配指定注解所标注的类型 (当使用 Spring AOP时,方法定义在由指定的注解所标注的类里)
@annotation 限制匹配带有指定注解连接点

网上摘的图;切入点表达式

三、依赖引入

可以参看本人 a-spring-test-pro项目的中 .gradle文件内容

只用 spring-context、spring-aop两个工厂就行了。

java 复制代码
dependencies {
    compile(project(":spring-context"))
    compile(project(":spring-aop"))
}

四、注解方式应用

(demo所在 a-spring-test-proaop包中)

目标接口、实现类:

java 复制代码
public interface IAnimal {
    String tails();
}

@Service
public class Cat implements IAnimal {
    @Override
    public String tails() {
        System.out.println("小猫的尾巴长");
        return "小猫";
    }
}

@Service
public class Dog implements IAnimal {
    @Override
    public String tails() {
        System.out.println("小狗的尾巴短");
        return "小狗";
    }
}

注解启动自动代理:

java 复制代码
@Configuration
@ComponentScan(basePackages = "aop")  // 要扫描调包
// proxyTargetClass = true 指定proxyTargetClass来强制执行CGLIB代理,或者指定一个或多个接口来使用JDK动态代理。这会源码中介绍
@EnableAspectJAutoProxy(proxyTargetClass = true) 
public class AspectConfig {
}

切面类:

java 复制代码
@Aspect
@Component
public class AnimalAspectJ {

	@Pointcut("execution(* aop.service.IAnimal.tails(..))")
	public void pointCut(){

	}

	@Before("pointCut()")
	public void before(){
		System.out.println("小猫小狗都有尾巴 before...");
	}

	@After("pointCut()")
	public void after(){
		System.out.println("小猫小狗都有尾巴 after...");
	}

	@AfterReturning("pointCut()")
	public void afterReturning() {
		System.out.println("小猫小狗都有尾巴 afterReturning...");
	}

	@AfterThrowing("pointCut()")
	public void afterThrowing() {
		System.out.println("小猫小狗都有尾巴 afterThrowing...");
	}


	/**
	 * 该通知会将目标方法封装起来,
	 * 并且 Around before ... -> 在 @Before前;
	 * 	   Around after ...  -> 在 @After前.
	 * @param pj
	 */
	@Around("pointCut()")
	public void xxx(ProceedingJoinPoint pj) {
		try {
			System.out.println("Around before ...");
			pj.proceed();
			System.out.println("Around after ...");
		} catch (Throwable throwable) {
			throwable.printStackTrace();
		}
	}
}

测试类:

java 复制代码
public class AppTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
           				new AnnotationConfigApplicationContext(AspectConfig.class);

        Dog dog = context.getBean("dog", Dog.class);
        //Cat cat = (Cat) context.getBean("cat");
        dog.tails();
        //cat.tails();
    }
}

测试结果:

java 复制代码
Around before ...
小猫小狗都有尾巴 before...
小狗的尾巴短
Around after ...
小猫小狗都有尾巴 after...
小猫小狗都有尾巴 afterReturning...

五、配置方式应用

(demo所在 a-spring-test-proxmlaop包中)

目标接口、目标类:

java 复制代码
/**
 * 汉堡
 */
public interface HamburgerService {
	public void steak();
}

@Service
public class HamburgerServiceImpl implements HamburgerService{
	@Override
	public void steak() {
		System.out.println("中间夹牛排~~~");
	}
}

切面类:

java 复制代码
public class HamburgerAspectConfig {
	public void before() {
		System.out.println("before 面包片 ...");
	}

	public void after() {
		System.out.println("after 面包片 ...");
	}

	public void afterReturning() {
		System.out.println("afterReturning 芝士片 ...");
	}

	public void afterThrowing(){
		System.out.println("afterThrowing 汉堡烤");
	}

	public void around(ProceedingJoinPoint pj) {
		try {
			System.out.println("Around 西红柿片 ...");
			pj.proceed();
			System.out.println("Around 西红柿片 ...");
		} catch (Throwable throwable) {
			throwable.printStackTrace();
		}
	}
}

配置文件:这里直接连 配置文件头也复制进来

java 复制代码
<?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" xmlns:cache="http://www.springframework.org/schema/cache"
	   xmlns:tx="http://www.springframework.org/schema/cache"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
	   					   http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

	<!-- 注入bean -->
	<bean id="hamburger" class="xmlaop.service.HamburgerServiceImpl"></bean>
	<!-- 切面 -->
	<bean id="hamburgerAspectConfig" class="xmlaop.aspect.HamburgerAspectConfig"></bean>


	<aop:config proxy-target-class="false">
		<!-- 切入点 expression这个表达式的方法,就是切入点-->
		<aop:pointcut id="hamburgerPointcut" expression="execution(* xmlaop.service.HamburgerService.steak())"/>

		<!-- 切面-->
		<aop:aspect id="hamburgerAspect" ref="hamburgerAspectConfig">
			<!--前置通知-->
			<aop:before pointcut-ref="hamburgerPointcut" method="before"></aop:before>
			<!--后置通知-->
			<aop:after pointcut-ref="hamburgerPointcut" method="after"></aop:after>
			<!--返回通知-->
			<aop:after-returning pointcut-ref="hamburgerPointcut" method="afterReturning"/>
			<!--异常通知 : 只有在目标方法抛出异常时才执行-->
			<aop:after-throwing pointcut-ref="hamburgerPointcut" method="afterThrowing"/>

			<!--这特喵,这环绕,在xml配置里面还会受位置影响,影响执行结果,这个环绕通知如果放在 前置通知前面,那么环绕通知会先执行。-->
			<!--放在这里,会在前置通知执行完之后再执行-->
			<!--环绕通知-->
			<aop:around pointcut-ref="hamburgerPointcut" method="around"/>
		</aop:aspect>
	</aop:config>
</beans>

测试类:

java 复制代码
public class XmlAopConfigTest {
	public static void main(String[] args) {
		ClassPathXmlApplicationContext context =
				new ClassPathXmlApplicationContext("spring-config.xml");

		HamburgerService hamburgerService = (HamburgerService)context.getBean("hamburger");
		hamburgerService.steak();
	}
}

测试结果:

发现这里有两种情况:

  • 情况一:

    (注意这里的 before 面包片 ... 输出在 Around 西红柿片 ...前面);说明前置通知先输出,再输出环绕通知。

    这种情况出现的原因是:配置文件中 <aop:around pointcut-ref="hamburgerPointcut" method="around"/> 代码位置 前置通知的下面

    java 复制代码
    before 面包片 ...
    Around 西红柿片 ...
    中间夹牛排~~~
    Around 西红柿片 ...
    afterReturning 芝士片 ...
    after 面包片 ...
  • 情况二:

    (注意这里的 before 面包片 ... 输出在 Around 西红柿片 ... 后面);说明环绕通知输出在前置通知前面。

    这种情况出现的原因是:配置文件中 <aop:around pointcut-ref="hamburgerPointcut" method="around"/> 代码位置 前置通知的上面

    java 复制代码
    Around 西红柿片 ...
    before 面包片 ...
    中间夹牛排~~~
    Around 西红柿片 ...
    after 面包片 ...
    afterReturning 芝士片 ...

六、总结

​ 这也说明了,配置文件方式,环绕通知的顺序会受到位置的影响(进而影响了 前置通知)

​ 但是其实我们对比看 注解方式配置时候发现,配置文件方式情况二和注解方式相同。

​ 其实,在我们配置这些的时候,Spring AOP 会为我们按通知类型排序的,因为当我们执行目标方法的时候,其实执行的是代理方法(被Spring AOP代理的),会有一个拦截器链(通知链)

​ 还有,注解方式 通知注解的顺序,不会影响输出结果。

​ 因为(我擦,好难解释),有一个类 ReflectiveAspectJAdvisorFactory,该类里面有一个 METHOD_COMPARATOR属性,就是通知比较器,会在获取注解方式的通知的时候( ReflectiveAspectJAdvisorFactory#getAdvisorMethods )进行排序的。(后面写 AOP 源码执行流程的时候可能会写这个排序吧。)

​ 对比一下 ReflectiveMethodInvocation类中 process()方法中的 interceptorsAndDynamicMethodMatchers属性 ,执行器链中通知的顺序,注意真正的执行顺序是倒序执行的,从index 5开始的 到 index 0 。

  • 注解中的:不论通知注解的顺序都是下面的结果:

  • 配置文件方式:

    • 情况一:(环绕通知在 前置通知后面)

    • 情况二:(环绕通知在 前置通知前面)

  • 上面说的是配置文件方式,环绕通知的顺序会受到位置的影响(进而影响了 前置通知);另外三个通知是不会受到排序影响的,为什么呢? 还是从每个通知的处理入手。

    • 先说 @After 通知,找到源码里面的 AspectJAfterAdvice类的 invoke方法,在后置通知时候,调用的该方法:

      注意,mi.proceed()是递归(后面将源码流程再讲递归);这里只要知道@After 通知方法会在 finally块 里面执行,也就是一定是最后执行的即可。

      java 复制代码
      @Override
      public Object invoke(MethodInvocation mi) throws Throwable {
          try {
              // 执行方法
              // 调用拦截器链 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed()
              return mi.proceed();
          }
          finally {
              // 注意这里是 finally块中
              // 调用 org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod() 通知基础类的方法
              // 执行通知方法
              invokeAdviceMethod(getJoinPointMatch(), null, null);
          }
      }
    • 然后,@AfterThrowing ;在catch块中执行,就不多说了,和上面一样

      java 复制代码
      @Override
      public Object invoke(MethodInvocation mi) throws Throwable {
          try {
              // 执行方法
              // 调用拦截器链 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed()
              return mi.proceed();
          }
          catch (Throwable ex) {
              // 注意这里是在 catch块中
              if (shouldInvokeOnThrowing(ex)) {
                  // 调用 org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod() 通知基础类的方法
                  // 执行通知方法
                  invokeAdviceMethod(getJoinPointMatch(), null, ex);
              }
              throw ex;
          }
      }
    • 再然后 @AfterReturning,看 AspectJAfterReturningAdvice

      java 复制代码
      @Override
      public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, 
                                 @Nullable Object target) throws Throwable {
          // 这里会有判断,当方法调用结束之后,才回进入if里面 执行后置返回通知
          if (shouldInvokeOnReturnValueOf(method, returnValue)) {
              // 调用 org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod() 通知基础类的方法
              // 执行通知方法
              invokeAdviceMethod(getJoinPointMatch(), returnValue, null);
          }
      }

上面说的这些方法都是在 拦截器链(或者叫通知链)执行的,整个就是在 ReflectiveMethodInvocation类中 process()方法 调用,递归的过程。(想了解还要看源码执行流程,本人会在 Spring AOP源码执行流程文章里面写),这里先贴一个递归调用流程,从[1] → [7],层次往里面走,然后再从 [7]->[1]往外出,注意看invoke()方法的类型。

相关推荐
学java的小菜鸟啊16 分钟前
第五章 网络编程 TCP/UDP/Socket
java·开发语言·网络·数据结构·网络协议·tcp/ip·udp
zheeez19 分钟前
微服务注册中⼼2
java·微服务·nacos·架构
程序员-珍23 分钟前
SpringBoot v2.6.13 整合 swagger
java·spring boot·后端
徐*红31 分钟前
springboot使用minio(8.5.11)
java·spring boot·spring
聆听HJ31 分钟前
java 解析excel
java·开发语言·excel
AntDreamer35 分钟前
在实际开发中,如何根据项目需求调整 RecyclerView 的缓存策略?
android·java·缓存·面试·性能优化·kotlin
java_heartLake40 分钟前
设计模式之建造者模式
java·设计模式·建造者模式
G皮T40 分钟前
【设计模式】创建型模式(四):建造者模式
java·设计模式·编程·建造者模式·builder·建造者
niceffking44 分钟前
JVM HotSpot 虚拟机: 对象的创建, 内存布局和访问定位
java·jvm
菜鸟求带飞_1 小时前
算法打卡:第十一章 图论part01
java·数据结构·算法