这里写目录标题
前言
项目中除了纵向的业务逻辑线外,通常我们还需要处理一些模块共用的逻辑,比如权限认证、性能统计等。这些逻辑通常是‘横向’的,贯穿多个业务模块。这时候 AOP 就派上用场了。
一、AOP 简单介绍
面向切面编程,简称 AOP(Aspect Oriented Programming)。是指将切面逻辑按一定的方式织入到特定的业务模块中,从而在执行业务逻辑的过程的同时完成切面逻辑。
AOP 术语
通知(Advice)
需要完成的切面逻辑叫做通知。
连接点(Join point)
就是spring中允许使用通知的地方,基本上就是方法(方法执行前、执行后、异常抛出等)。
切点(Poincut)
其实就是筛选出的连接点,如果说通知定义了切面的动作的话,切点则定义了执行的地点或者执行的时机。
切面(Aspect)
通知和切点的结合,通知和切点共同定义了切面的全部内容,它是干什么的,什么时候在哪执行
引入(Introduction)
在不改变一个现有类代码的情况下,为该类添加属性和方法。可以无需修改源码,让它们具有新的行为或状态。
目标(target)
被通知的业务对象。
织入(Weaving)
切面逻辑可以在多个时间点添加到目标业务逻辑:
- 编译期:切面在目标类编译时被织入
- 类加载期:切面在目标类加载到JVM时被织入,这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码
- 运行期:切面在应用运行的某个时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP就是以这种方式织入切面的。
AOP 切点的主要类型
类型 | 说明 |
---|---|
@Before | 切面在业务模块代码执行之前执行,其不能阻止业务模块的执行,除非抛出异常 |
@AfterReturning | 切面在业务模块代码执行之后执行 |
@AfterThrowing | 切面在业务模块抛出指定异常后执行 |
@After | 切面在所有的 Advice 执行完成后执行,无论业务模块是否抛出异常,类似于finally的作用 |
@Around | 切面包裹业务模块的执行,其可以传入一个 ProceedingJoinPoint 用于调用业务模块的代码,调用前逻辑和调用后逻辑都可以在该方法中编写,也可以根据一定的条件阻断业务模块的调用 |
@DeclareParents | 在属性声明上使用,主要用于为指定的业务模块添加新的接口和相应的实现 |
执行顺序
around(befor invoke) => before => around (after invoke)=> after => afterReturning
二、Spring AOP 表达式
1. 整体示例
@Aspect
@component
public class MyAspect {
@Around("{切入点表达式}")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("this is before around advice");
Object result = pjp.proceed();
System.out.println("this is after around advice");
return result;
}
}
2. 声明切面类
@Aspect 注解用在类定义上,指明当前类是切面类,并且该注解中可以指定当前类是何种实例化方式,主要有三种:singleton、perthis和pertarget。
3. 切入点表达式
3.1 类型
3.1.1 execution 匹配方法
语法:[modifiers-pattern] <ret-type-pattern> [declaring-type-pattern] <name-pattern><(param-pattern)> [throws-pattern]
execution 表达式可以指定方法返回类型,类名,方法名和参数名等与方法相关的部件。Spring 中大部分需要使用 AOP 的业务场景达到方法级别即可,因此 execution 表达式的使用是最为广泛的。如下是 execution 表达式的语法:
表达式项说明(问号表示当前项可选):
表达式项 | 说明 |
---|---|
modifiers-pattern | 方法的可见性,如 public,protected |
ret-type-pattern | 方法的返回值类型,如 int,void 等 |
declaring-type-pattern | 方法所在类的全路径名,如 com.spring.Aspect |
name-pattern | 方法名类型,如 buisinessService() |
param-pattern | 方法的参数类型,如 java.lang.String |
throws-pattern | 方法抛出的异常类型,如 java.lang.Exception |
示例
//表示返回值为任意类型,com.spring.service.BusinessObject类中参数个数为零的所有方法
execution(* com.spring.service.BusinessObject.*())
//表示返回值为任意类型,在com.spring.service包中,以Business为开头的类中参数个数为零方法:
execution(* com.spring.service.Business*.*())
//表示匹配返回值为任意类型,并且是com.spring.service包及其子包下的任意类的名称为businessService的方法,而且该方法不能有任何参数
execution(* com.spring.service..*.businessService())
//使用..表示任意个数的参数,需要注意,表示参数的时候可以在括号中事先指定某些类型的参数,而其余的参数则由..进行匹配:
execution(* com.spring.service.BusinessObject.businessService(java.lang.String,..))
3.1.2 within 匹配类
语法:within(declaring-type-pattern)
within 参数为全路径的类名(可使用通配符),匹配当前表达式的所有类中的方法都将被当前方法环绕。
示例
//匹配 aop.Apple 中的所有方法
@Around(value = "within(aop.Apple)")
//匹配 com.spring.service 包下的所有类,不包括子包中的类
@Around(value = "within(com.spring.service.*)")
//匹配 com.spring.service 包及子包下的所有类
@Around(value = "within(com.spring.service..*)")
3.1.3 args 匹配方法参数
语法:args(param-pattern)
args 表达式的作用是匹配指定参数类型和指定参数数量的方法,需要注意的是,args 指定的参数必须是全路径的。
示例
//匹配所有只有一个参数,并且参数类型是java.lang.String类型的方法
args(java.lang.String)
//匹配第一个参数为java.lang.String,最后一个参数为java.lang.Integer,并且中间可以有任意数量和类型参数的方法
args(java.lang.String,..,java.lang.Integer)
3.1.4 this 和 target 匹配代理对象
语法:this(declaring-type-pattern)
语法:target(declaring-type-pattern)
this
匹配调用当前切点表达式所指代对象方法的对象target
匹配切点表达式指定类型的对象。
以上两种方式只有在目标对象实现接口,且 Spring 采用 JDK 动态代理时才有区别。由于 JDK 动态代理生成的代理对象与目标对象只是实现了相同的接口,而非相同类型的对象,因此不符合 this 语义。而 target 描述的就是目标对象,因此是符合 target 语义的。
示例
@Component
class Apple implements Fruit{...}
// 使用 target 表达式时, 符合语义,会执行切入逻辑。
@Around("target(aop.Apple)")
Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("this is before around advice.");
Object proceed = pjp.proceed(pjp.getArgs());
System.out.println("this is after around advice.");
return proceed;
}
输出:
this is before around advice.
aop.Apple.desc method invoked…
this is after around advice.
// 使用 this 表达式时, 不符合语义。
@Around("this(aop.Apple)")
Object around(ProceedingJoinPoint pjp) throws Throwable {...}
注意:spring 2.3.4 默认使用的是 CGLib 方式代理。需要在配置文件中配置 spirng.aop.proxy-target-class = false输出:
aop.Apple.desc method invoked…
3.2 注解匹配
通过使用注解的类、方法或参数等实现切入点匹配
3.2.1 @within 匹配带有指定注解的类
语法:@within(annotation-type)
3.2.2 @annotation 匹配带有指定注解的方法
语法:@annotation(annotation-type)
3.2.3 @args 匹配使用注解的类作为参数的方法
语法:@args(annotation-type)
3.3 通配符的使用
表达式项中可以使用通配符实现批量匹配,类似于正则表达式的使用。
*
通配符,用于匹配单个单词,或者是以某个词为前缀或后缀的单词。..
通配符,表示0个或多个项,主要用于declaring-type-pattern
和param-pattern
中
如果用于 declaring-type-pattern 中,则表示匹配当前包及其子包,
如果用于 param-pattern 中,则表示匹配0个或多个参数。
Introduction(引入)
@DeclareParents也称为Introduction(引入),可以动态的为指定的类引入新的属性和方法。
语法:@DeclareParents(value = “TargetType”, defaultImpl = WeaverType.class
示例
interface IDescriber {
void desc();
}
class DescriberImpl implements IDescriber {
@Override
void desc() {
println "this is an introduction describer."
}
}
//目标类,未实现接口 IDescriber
@Component
class Apple {
void eat(String msg) {
System.out.println("aop.Apple.eat method invoked.." + msg);
}
}
//AOP 类织入
@Aspect
@Component
class MyAspect {
@DeclareParents(value = "aop.Apple", defaultImpl = DescriberImpl.class)
private IDescriber describer;
}
@SpringBootApplication
class AppleApp {
public static void main(String[] args) {
ConfigurableApplicationContext run = new SpringApplicationBuilder(AppleApp.class).web(WebApplicationType.NONE).run(args);
// 获取的 apple bean 被织入了 desc 方法
IDescriber apple = (IDescriber) run.getBean("apple");
apple.desc();
}
}
输出:
this is an introduction describer.