深入了解Spring(三) - AOP
在学习AOP前可以先了解一下Java动态代理
https://www.cnblogs.com/binwenhome/p/13025480.html
AOP概述
- 简介
-
AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 对OOP(面向对象编程)的补充.
- 面向对象: 纵向继承机制, 在该类中写代码.
- 面向切面: 横向抽取机制, 把某些代码抽取到另外一个类中, 再作用到该类上.
-
AOP编程操作的主要对象主要是切面, 而切面用于模块化横切关注点(公共功能)
-
来存储公共功能的类就叫切面.
-
- AOP的好处
- 每个事务逻辑位于一个位置, 代码不分散, 便于维护和升级.
- 业务模块更简洁, 只包含核心代码.
- AOP术语
- 横切关注点
- 从每个方法中抽取出来的同一类非核心业务.
- 切面(Aspect)
- 封装横切关注点信息的类, 每个关注点体现为一个通知方法.
- 通知(Advice)
- 切面必须要完成的各个具体工作.
- 目标(Target)
- 被通知的对象
- 代理(Proxy)
- 向目标对象应用通知后创建的代理对象.
- 连接点(JoinPoint)
-
横切关注点在程序代码中的具体体现, 对应程序执行的某个特定位置. 例如: 类某个方法调用前、调用后、方法捕获到异常后等
-
在应用程序中可以使用横纵两个坐标来定位一个具体的连接点.
-
- 切入点(pointcut)
-
要把切面作用到目标对象的一种方式, 即为使用切面的条件, 是个表达式.
-
AOP可以通过切入点定位到特定的连接点. 切点通过org.springframework.aop.Pointcut接口进行描述, 它使用类和方法作为连接点的查询条件.
-
- 图解
- AspectJ(切面)
- 简介
- 是Java社区里最完整, 最流行的AOP框架.
- Spring2.0以上, 可用基于AspectJ注解或XML配置的AOP.
- 在Spring中启用AspectJ注解支持.
- 导入依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency>
- 在配置文件中开启AspectJ的自动代理功能.
<aop:aspectj-autoproxy />
- 在Spring5中, 默认使用AspectJ动态代理, 所以不用加相关的jar了.
- 导入依赖
- 用AspectJ注解声明切面
- 要在Spring中声明AspectJ切面, 只需要在IOC容器中将切面声明为bean实例.
- 当在SpringIOC容器中初始化AspectJ切面之后, SpringIOC容器就会为那些与AspectJ切面相匹配的bean创建代理.
- 在AspectJ注解中, 切面只是一个带有@Aspect注解的Java类.
- 通知是标注有某种注解的简单的Java方法.
- AspectJ支持5种类型的通知注解
- @Before: 前置通知, 在方法执行前执行.
- @After: 后置通知, 在方法执行之后执行.
- @AfterRunning: 返回通知, 在方法返回结果之后执行.
- @AfterThrowing: 异常通知, 在方法抛出异常之后执行.
- @Around: 环绕通知, 围绕着方法执行.
- 示例代码
- 注意: AOP也是基于IOC实现的, 所以需要IOC完成自动装配工作, 故仍需加入@Component注解.
- 使用配置类
- 在配置类中加入@EnableAspecctJAutoProxy, 开启aspectj的自动代理功能.
@EnableAspectJAutoProxy @Configuration @ComponentScan({"test"}) public class MainConfigOfAOP { //... }
- 在配置类中加入@EnableAspecctJAutoProxy, 开启aspectj的自动代理功能.
- aop.xml
<!-- 开启aspectj的自动代理功能 --> <aop:aspectj-autoproxy /> <!-- 扫描 --> <context:component-scan base-package="aop" />
- 结构
@Component public class MathImpl implements MathI { //各个可执行方法 //横切关注点在方法中. }
//切面里封装横切关注点 @Component @Aspect //标注当前类为切面 public class MyLoggerAspect { //在切面中写各种通知, 切入点表达式加到通知上. }
AOP细节
- 切入点表达式
-
作用: 通过表达式的方式定位一个或多个具体的连接点.
-
语法细节
-
execution(权限修饰符 返回类型 全类名 方法名 参数列表)
@Before(value = "execution(public int aop.MathImpl.add(int, int))")
- 几种特殊形式
execution(* aop.Calculator.* (..)) Calculator接口中的所有方法 第一个"*"表示任意修饰符, 任意返回值. 第二个"*"表示任意方法 ".."匹配任意数量, 任意类型的参数. execution(public * aop.Calculator.*(..)) Calculator接口的所有公有方法 execution(public double aop.Calculator.*(..)) Calculator接口的中返回值double类型的方法 execution(public * aop.Calculator.*(double, ..)) Calculator接口中的公有方法且第一个参数为double类型 execution(public * Calculator.*(double, double)) 参数为double, double的方法
- 还可以通过"&&", "||", "!"等操作符结合起来
execution(* *.add(int, ..)) || execution(* *.sub(int, ..)) !execution(* *.add(int, ..))
-
- 当前连接点细节
-
切入点表达式通常都会是从宏观上定位一组方法, 和具体某个通知的注解结合起来就能够确定对应的连接点, 那么就一个具体的连接点而言, 我们可能会关心这个连接点的一些具体信息.
- JoinPoint
- 通知
- 概述
-
通知: 在具体的连接点上要执行的操作.
-
一个切面可以包括多个通知.
-
通知所使用的注解的值往往是切入点表达式.
-
- 前置通知
-
方法执行前执行的通知.
-
使用@Before注解
/** * 切面里定义了通知, 而通知要想作用于连接点, 需要通过切入点表达式(只能定位到要作用的方法) * 而写的各种通知代表要作用的位置. */ @Before(value = "execution(* aop.MathImpl.*(..))") public void beforeMeethod(JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); //获取方法参数 String name = joinPoint.getSignature().getName(); //获取方法名 System.out.println("method: " + name + ", arguments: " + Arrays.toString(args)); }
-
- 后置通知
- 连接点完成之后执行的, 作用于方法的finally语句块, 即有无异常都会执行.
- 使用@After注解
@After(value = "execution(* aop.MathImpl.*(..))") public void afterMethod() { System.out.println("后置通知"); }
- 返回通知
-
如果只想在连接点返回的时候记录日志, 应使用返回通知代替后置通知.
-
使用@AfterReturning注解, 在返回通知中访问连接点的返回值
-
在返回通知中, 只要将returning属性添加到@AfterReturning注解中, 就可以访问连接点的返回值. 该属性的值即为用来传入返回值的参数名称.
- 必须在通知方法的签名中添加一个同名参数. 在运行时Spring AOP会通过这个参数传递返回值.
/** * @AfterReturning: 将方法标注为返回通知, 作用于方法执行之后. * 可通过returning设置接受方法返回值的变量名. * 要想在方法中使用, 必须在方法的形参中设置和变量名相同的参数名的参数, 类型为Object */ @AfterReturning(value = "execution(* aop.MathImpl.*(..))", returning = "result") public void afterReturning(JoinPoint joinPoint, Object result) { String name = joinPoint.getSignature().getName(); System.out.println("method: " + name + ", result: " + result); }
-
-
- 异常通知
-
只在连接点抛出异常时才执行异常通知.
-
将throwing属性添加到@AfterThrowing注解中, 才可以访问连接点抛出的异常. Throwable是所有错误和异常类的顶级父类, 所以在异常通知方法可以捕获到任何错误和异常.
-
如果只对某种特殊的异常类型感兴趣, 可以将参数声明为其他异常的参数类型. 然后通知就只在抛出这个类型及其子类的异常时才被执行.
/** * @AfterThrowing: 将方法标注为异常通知. * 异常通知: 当方法抛出异常时作用. * 通过throwing设置接受方法返回的异常信息. * 在参数列表中可通过具体的异常类型, 来对指定的一场进行操作 */ @AfterThrowing(value = "execution(* aop.MathImpl.*(..))", throwing = "e") public void afterThrowing(Exception e) { System.out.println("有异常了, message: " + e.getMessage()); }
-
- 环绕通知
-
环绕通知是所有通知类型中功能最为强大的, 能够全面地控制连接点,甚至可以控制是否执行连接点.
-
对于环绕通知来说, 连接点的参数类型必须是ProceedingJoinPoint. 它是JoinPoint的子接口, 允许控制何时执行, 是否执行连接.
-
在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法, 如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行.
-
注意: 环绕通知的方法需要返回目标方法执行之后的结果, 即调用point.proceed(); 的返回值, 否则会出现空指针异常.
@Around(value = "execution(* aop.MathImpl.*(..))") public Object arounMethod(ProceedingJoinPoint point) { Object result = null; try { //前置通知 System.out.println("method: " + point.getSignature().getName() + ", arguments: " + Arrays.toString(point.getArgs())); result = point.proceed(); //返回通知 System.out.println("method: " + point.getSignature().getName() + ", result: " + result); } catch (Throwable throwable) { //异常通知 System.out.println("有异常了, message: " + throwable.getMessage()); } finally { //后置通知 System.out.println("后置通知"); } return result; }
-
- 重用切入点定义
- 同一个切入点表达式可能在多个通知中重复出现, 可通过@Pointcut注解将切入点声明为简单的方法.
- 其他通知就可以通过方法名称引入该切入点.
@Pointcut(value = "execution(* aop.MathImpl.*(..))") public void test() { } @Before(value = "test()") public void beforeMethod(JoinPoint joinPoint) { //... }
- 指定切面的优先级
-
在同一个连接点上应用多个切面时, 除非明确指定, 否则优先级不确定.
-
切面的优先级可以通过@Order注解指定.
- @Order注解中值(非负数)越小, 优先级越高. 默认值为int的最大值.
@Component @Order(0) @Aspect //标注当前类为切面 public class MyLoggerAspect { //... }
- 使用举例
//核心类
@Component public class MathImpl implements MathI { @Override public int add(int i, int j) {
//先执行前置通知 int result = i + j; return result;
//后置通知
//返回通知 } @Override public int sub(int i, int j) { int result = i - j; return result; } @Override public int mul(int i, int j) { int result = i * j; return result; } @Override public int div(int i, int j) { int result = i / j; return result; } } //测试 public class TestBySpring { public static void main(String[] args) { //初始化容器 ClassPathXmlApplicationContext cac = new ClassPathXmlApplicationContext("conf/aop.xml");
//注意, 这里不要自己创建对象, 要从容器中取, 否则无法启用AOP功能. MathI math = cac.getBean("mathImpl", MathI.class); System.out.println(math.getClass().getName()); int div = math.div(4, 1); System.out.println(div); cac.close(); } }
以XML方式配置切面
- 除了使用AspectJ注解声明切面, Spring也支持在bean配置文件中声明切面, 通过aop名称空间中的xml元素完成的.
- 配置细节
-
在bean配置文件中, 所有的SpringAOP配置都必须定义在<aop:config>元素内部, 对于每个切面而言, 都要创建一个<aop:aspect>元素来为具体的切面实现引用后端bean实例.
-
<aop:aspect>中以ref属性标明具体的切面.
-
<aop:before>等, 以method属性标明方法, 以pointcut属性标明切入点表达式.
<aop:config> <aop:aspect ref="myLoggerAspect"> <aop:before method="beforeMethod" pointcut="execution(* aop.MathImpl.*(..))" /> </aop:aspect> </aop:config>
-
- 声明切入点
-
切入点使用<aop:pointcut>元素声明.
-
切入点可定义在<aop:aspect>或<aop:config>下.
-
定义在<aop:aspect>下, 只对当前切面有效.
-
定义在<aop:config>下, 对所有切面有效.
-
-
通知元素需要使用<pointcut-ref>来引用切入.
-
基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点.
<aop:config> <aop:aspect ref="myLoggerAspect"> <aop:pointcut id="cut" expression="execution(* aop.MathImpl.*(..))"/> <aop:before method="beforeMethod" pointcut-ref="cut" /> </aop:aspect> </aop:config>
-