定义
AOP——面向切面编程:是一种编程典范,它通过分离横切关注点来增加程序的模块化。简单说就是AOP可以在不修改现有代码的情况下,对现有代码增加一些功能。
-
横切关注点:软件系统可以看成是由一组关注点组成的,其中,直接的业务关注点,是直切关注点。而为直切关注点提供服务的,就是横切关注点。
-
横切关注点被模块化为特殊的类,这些类称为切面
-
切面也需要完成工作。在 AOP 术语中,切面的工作被称为通知。通知定义了切面内容以及何时使用。除了描述切面要完成的工作,通知还解决何时执行这个工作。
Spring 切面可应用的 5 种通知类型:
Before——在方法调用之前调用通知
After——在方法完成之后调用通知,无论方法执行成功与否
After-returning——在方法执行成功之后调用通知
After-throwing——在方法抛出异常后进行通知
Around——通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
-
连接点:连接点是一个应用执行过程中能够插入一个切面的点。切面代码可以利用这些点插入到应用的正规流程中。连接点更多是理论上的意义,如果不考虑程序的实际功能,你可以把每行代码都认为是一个jointpoint。
-
切点:定义通知被应用在哪些连接点
-
切面:切面是通知和切点的集合,通知和切点共同定义了切面的全部功能——它是什么,在何时何处完成其功能。
-
引入:引入允许我们向现有的类中添加方法或属性
-
织入:织入是将切面应用到目标对象来创建的代理对象过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期中有多个点可以织入。
编译期——切面在目标类编译时期被织入,这种方式需要特殊编译器。AspectJ的织入编译器就是以这种方式织入切面。
类加载期——切面在类加载到JVM ,这种方式需要特殊的类加载器,他可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5 的 LTW 就支持这种织入方式
运行期——切面在应用运行期间的某个时刻被织入。一般情况下,在织入切面时候,AOP 容器会为目标对象动态的创建代理对象。Spring AOP 就是以这种方式织入切面。
AOP实现——AspectJ
步骤:
- 引入AspectJ相关jar包
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring-version}</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.2</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.4</version> </dependency>
2、创建一个类声明为切面,并且放入IOC容器中。如下:创建路径和测试类
具体代码:
AopTest:(具体execution设置方式,另做记录)
package aop; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; /** * @Description:把这个类声明为切面,并放入IOC容器中 * @version: */ @Aspect @Component public class AopTest { // 声明一个前置通知:在目标方法前执行 JoinPoint可有可无,它记录了目标方法的相关信息 @Before("execution(public int aop.TestController.doSomething(int,int))") public void beforeMethod(JoinPoint joinPoint){ System.out.println("方法前执行"); System.out.println(joinPoint); } // 声明一个后置通知:在目标方法后执行(不论是否出现异常)。后置通知不能访问目标方法执行结果 @After("execution(public int aop.TestController.doSomething(int,int))") public void afterMethod(){ System.out.println("方法后执行"); } // 声明一个返回通知:在目标方法执行后,可以得到返回值信息 注解中通过“returning”定义返回值名字,并在方法使用同名参数接收 @AfterReturning(value = "execution(public int aop.TestController.doSomething(int,int))", returning = "result") public void afterRetuningMethod(JoinPoint joinPoint, Object result){ System.out.println("可获取方法返回值"); System.out.println("AfterReturning返回结果:"+result); } // 声明一个异常通知:在目标方法执行异常时,执行此方法 @AfterThrowing("execution(public int aop.TestController.doSomething(int,int))") public void exceptionMethod(){ System.out.println("方法异常时执行"); } }
TestController:
package aop; import org.springframework.stereotype.Component; @Component public class TestController { public int doSomething(int i, int j){ System.out.println("执行乘法运算"); return i*j; } }
配置文件:
<?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:util="http://www.springframework.org/schema/util" 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/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="aop"></context:component-scan> <!--使AspectJ注解起作用,所起作用为:自动匹配的类生成代理对象在调用方法时, (如果方法是切面中通知注解中的目标方法,AOP框架会自动为目标方法所在类生成代理对象, 然后在调用方法前将通知中的代码添加进去)--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
主函数类:
package controller; import aop.TestController; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args){ // 读取配置文件 ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring-bean.xml"); TestController testController = (TestController)ctx.getBean("testController"); int x = testController.doSomething(5,8); System.out.println("结果为:"+x); } }
运行结果:
可以发现,当同时存在前置通知、后置通知、异常通知、返回通知时,需要分别设置execution,为了减少重复代码,我们可以将它抽取出来。使用@Pointcut注解指定切点,各通知通过引用切点即可。具体实现如下:
package aop; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; /** * @Description:把这个类声明为切面,并放入IOC容器中 * @version: */ @Aspect @Component public class AopTest { // @Pointcut定义切点,方法体为空 @Pointcut("execution(public int aop.TestController.doSomething(int,int))") public void pointCutMethod(){ } // 通过方法名指定切点 @Before("pointCutMethod()") public void beforeMethod(JoinPoint joinPoint){ System.out.println("方法前执行"); System.out.println(joinPoint); } @After("pointCutMethod()") public void afterMethod(){ System.out.println("方法后执行"); } @AfterReturning(value = "pointCutMethod()", returning = "result") public void afterRetuningMethod(JoinPoint joinPoint, Object result){ System.out.println("可获取方法返回值"); System.out.println("AfterReturning返回结果:"+result); } @AfterThrowing("pointCutMethod()") public void exceptionMethod(){ System.out.println("方法异常时执行"); } }
除以上几种通知以外,还有一种通知——环绕通知。 环绕通知类似于动态代理的全过程,且环绕通知一定要有返回值,返回值即为目标方法的返回值。环绕通知在目标方法的前后、异常时都会做相应操作。具体实现如下:
package aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; /** * @Description:把这个类声明为切面,并放入IOC容器中 * @version: */ @Aspect @Component public class AopTest { // @Pointcut定义切点,方法体为空 @Pointcut("execution(public int aop.TestController.doSomething(int,int))") public void pointCutMethod(){ } // 环绕通知 必须携带ProceedingJoinPoint类型参数 @Around("pointCutMethod()") public Object round(ProceedingJoinPoint proceedingJoinPoint){ Object result = null; String methodName = proceedingJoinPoint.getSignature().getName(); System.out.println(methodName); try { // 前置通知 System.out.println("begin..."); result = proceedingJoinPoint.proceed(); // 返回通知 System.out.println("return..."+result); } catch (Throwable e) { // 异常通知 System.out.println("error..."); } // 后置通知 System.out.println("after..."); return result; } }
运行结果:
当多个切面指向同一目标方法时,哪个切面先执行?哪个切面后执行?
可以通过使用@Order注解来标识切面的优先级
实际上该注解主要用来控制配置类的加载顺序,也可以使用它来控制切面的优先级,注解有一个int类型的参数,可以不传,默认是最低优先级,参数的值越小,优先级越高。或者通过集成Ordered接口,实现其中的getOrder()方法,getOrder()方法的返回值类型是int,实现时返回值越小优先级越高。
@AfterRetuning是在目标函数执行完毕并返回结果时调用,因此可以从@AfterReturning中获取到方法返回值。示例:
@AfterReturning(value = "execution(* com.example.aop.sbt_aop.test.TestMain.index(..))", returning = "str")
public void afterReturn(String str){
System.out.println("AfterReturning方法执行,返回参数:"+str);
}
上述是afterReturning获取目标方法的返回结果,如果想要传递参数给通知,我们只需要在切点处加对应正则表达式就可以了,示例:
@Before("execution(* TestMain.index(..)) && args(info)")
public void before(String info){
System.out.println(info);
System.out.println("Before方法执行");
}
对于非环绕通知还可以使用还可以使用一个连接点(JoinPoint)类型的参数,通过它也可以获取参数。示例:
@Before("execution(* TestMain.index(..))")
public void before(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();//获取目标方法调用时传入的所有参数
System.out.println(args);
System.out.println("Before方法执行");
}