type
status
date
slug
summary
tags
category
icon
password
一、AOP是什么
AOP(Aspect Oriented Programming,面向切面编程),是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP 主要使用在日志记录,性能统计,安全控制等场景,使用AOP可以使得业务逻辑各部分之间的耦合度降低,只专注于各自的业务逻辑实现,从而提高程序的可读性及维护性。
比如,我们需要记录项目中所有对外接口的入参和出参,以便出现问题时定位原因,在每一个对外接口的代码中添加代码记录入参和出参当然也可以达到目的,但是这种硬编码的方式非常不友好,也不够灵活,而且记录日志本身和接口要实现的核心功能没有任何关系。
此时,我们可以将记录日志的功能定义到1个切面中,然后通过声明的方式定义要在何时何地使用这个切面,而不用修改任何1个外部接口。
二、AOP 的概念
AOP 的概念有的来自 aopalliance,有的来自 AspectJ,也有的是 spring-aop 原创。这些概念构成 spring-aop 设计图的基础。
- 切面(Aspect):Aspect 声明类似于 Java 中的类声明,事务管理是AOP一个最典型的应用。在AOP中,切面一般使用 @Aspect 注解来使用,在XML 中,可以使用 aop:aspect 来定义一个切面。
- 连接点(Join Point):一个在程序执行期间的某一个操作,就像是执行一个方法或者处理一个异常。在Spring AOP中,一个连接点就代表了一个方法的执行。
- 通知(Advice):在切面中(类)的某个连接点(方法出)采取的动作,会有四种不同的通知方式: around(环绕通知),before(前置通知),after(后置通知), exception(异常通知),return(返回通知)。许多AOP框架(包括Spring)将建议把通知作为为拦截器,并在连接点周围维护一系列拦截器。
- 切入点(Pointcut):表示一组连接点,通知与切入点表达式有关,并在切入点匹配的任何连接点处运行(例如执行具有特定名称的方法)。由切入点表达式匹配的连接点的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。
- 介绍(Introduction):introduction可以为原有的对象增加新的属性和方法。例如,你可以使用introduction使bean实现IsModified接口,以简化缓存。
- 目标对象(Target Object):由一个或者多个切面代理的对象。也被称为"切面对象"。由于Spring AOP是使用运行时代理实现的,因此该对象始终是代理对象。
- AOP代理(AOP proxy):由AOP框架创建的对象,在Spring框架中,AOP代理对象有两种:JDK动态代理和CGLIB代理
- 织入(Weaving):是指把增强应用到目标对象来创建新的代理对象的过程,它(例如 AspectJ 编译器)可以在编译时期,加载时期或者运行时期完成。与其他纯Java AOP框架一样,Spring AOP在运行时进行织入。
AOP 中织入的三种时期
- 编译期: 切面在目标类编译时被织入,这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
- 类加载期: 切面在目标类加载到 JVM 时被织入,这种方式需要特殊的类加载器( ClassLoader ),它可以在目标类引入应用之前增强目标类的字节码。
- 运行期: 切面在应用运行的某个时期被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP 采用的就是这种织入方式。
AOP 的两种实现方式
- 静态代理(AspectJ 实现):AspectJ 是一个采用Java 实现的AOP框架,它能够对代码进行编译(一般在编译期进行),让代码具有AspectJ 的 AOP 功能,AspectJ 是目前实现 AOP 框架中最成熟,功能最丰富的语言。ApectJ 主要采用的是编译期静态织入的方式。在这个期间使用 AspectJ 的 acj 编译器(类似 javac)把 aspect 类编译成 class 字节码后,在 java 目标类编译时织入,即先编译 aspect 类再编译目标类。
- 动态代理(Spring AOP实现):Spring AOP 是通过动态代理技术实现的,而动态代理是基于反射设计的。Spring AOP 采用了两种混合的实现方式:JDK 动态代理和 CGLib 动态代理。
三、AspectJ
基于XML的声明式存在一些不足,需要在 Spring 配置文件配置大量的代码信息,为了解决这个问题,Spring 使用了 AspectJ 框架为 AOP 的实现提供了一套注解。
Spring AOP 和 AspectJ 是什么关系?
- AspectJ是更强的 AOP 框架,实现了自己的编译器,是实际意义的AOP标准。Spring AOP 使用纯 Java 实现,没有自己的编译器。
- Spring 支持 AspectJ 的注解,并使用AspectJ来做切入点解析和匹配。但是,AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ的编译器或者织入器(weaver)。
- Spring 只支持方法级别的连接点,如果需要字段级别或者构造器级别的连接点,可以利用 AspectJ 来补充 Spring AOP 的功能。
注解名称 | 解释 |
@Aspect | 用来定义一个切面。 |
@pointcut | 用于定义切入点表达式。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。 |
@Before | 用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。 |
@AfterReturning | 用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式。 |
@Around | 用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 |
@After-Throwing | 用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定-一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。 |
@After | 用于定义最终final 通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 |
这里面定义了五种通知类型和 pointcut 切点表达式。5种类型的通知如下:
- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,不论是正常返回还是异常退出
- 返回通知(After-returning):在目标方法成功执行之后调用通知
- 异常通知(After-throwing):在目标方法抛出异常后调用通知
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
下面重点介绍下 Pointcut 表达式的使用。
Pointcut 表达式
Pointcut 表达式主要由三部分组成。
- designators:指示器
- wildcards:通配符
- operators:操作运算符
指示器
指示器的作用就是通过什么样的方式来匹配 java 类的哪些方法。 Spring AOP 中目前只有执行方法这一个连接点,Spring AOP 支持的 AspectJ 切入点指示符如下:
指示器 | 描述 |
execution() | 用于匹配方法执行的连接点 |
within() | 用于匹配指定的类及其子类中的所有方法 |
this() | 匹配可以向上转型为this指定的类型的代理对象中的所有方法 |
target() | 匹配可以向上转型为target指定的类型的目标对象中的所有方法 |
args() | 用于匹配运行时传入的参数列表的类型为指定的参数列表类型的方法 |
@within() | 用于匹配持有指定注解的类的所有方法 |
@target() | 用于匹配的持有指定注解目标对象的所有方法 |
@args() | 用于匹配运行时 传入的参数列表的类型持有 注解列表对应的注解的方法 |
@annotation() | 用于匹配持有指定注解的方法 |
bean | bean(Bean的id或名字通配符)匹配特定名称的Bean对象 |
execution
匹配方法切入点。根据表达式描述匹配方法,是最通用的表达式类型,可以匹配方法、类、包。
name-pattern 和 param-pattern 是必选的, 其它部分都是可选的。
- modifier:匹配修饰符,public, private 等,省略时匹配任意修饰符
- ret-type:匹配返回类型,使用 * 匹配任意类型
- declaring-type:匹配目标类,省略时匹配任意类型
.
匹配包下的所有类..
匹配包及其子包的所有类。..
出现在类名中时,后面必须跟
- name-pattern:匹配方法名称,使用
表示通配符
匹配任意方法
set*
匹配名称以 set 开头的方法set*
匹配名称包含 set 的方法
- param-pattern:匹配参数类型和数量
()
匹配没有参数的方法(..)
匹配有任意数量参数的方法(*)
匹配有一个任意类型参数的方法(*,String)
匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型
- throws-pattern:匹配抛出异常类型,省略时匹配任意类型
使用示例:
within & @within
匹配指定类型。匹配指定类的任意方法,不能匹配接口。
使用示例:
this
匹配代理对象实例的类型,匹配在运行时对象的类型。
使用示例:
target & @target
匹配目标对象实例的类型,匹配 AOP 被代理对象的类型。
使用示例:
三种表达式匹配范围如下:
表达式匹配范围 within this target 接口 ✘ ✔ ✔ 实现接口的类 ✔ 〇 ✔ 不实现接口的类 ✔ ✔ ✔
args & @args
匹配方法参数类型和数量,参数类型可以为指定类型及其子类。
使用示例:
@annotation
匹配方法是否含有注解。
切点表达式中的参数类型,可以和通知方法的参数通过名称绑定,表达式中不需要写类或注解的全路径,而且能直接获取到切面拦截的参数或注解信息。适用于@annotation、@within、@target、@args
bean
通过 bean 的 id 或名称匹配,支持 * 通配符。
使用示例:
通配符
通配符 | 说明 |
* | 匹配任何数量字符 |
.. | 匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数 |
+ | 匹配指定类型的子类型;仅能作为后缀放在类型模式后边 |
操作符
使用
&&
、||
和 !
来组合多个切点表达式,表示多个表达式“与”、“或”和“非”的逻辑关系。
这可以用来组合多种类型的表达式,来提升匹配效率。怎样编写一个好的切点表达式?
要使切点的匹配性能达到最佳,编写表达式时,应该尽可能缩小匹配范围,切点表达式分为三大类:
- 类型表达式:匹配某个特定切入点,如
execution
- 作用域表达式:匹配某组切入点,如
within
- 上下文表达式:基于上下文匹配某些切入点,如
this
、target
和@annotation
一个好的切点表达式应该至少包含前两种(类型和作用域)类型。
作用域表达式匹配的性能非常快,所以表达式中尽可能使用作用域类型。
上下文表达式可以基于切入点上下文匹配或在通知中绑定上下文。
单独使用类型表达式或上下文表达式比较消耗性能(时间或内存使用)。
四、AOP 实战
- Author:mcbilla
- URL:http://mcbilla.com/article/ee088216-d837-4897-8b6e-6371d6cff1ef
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts