基本概念
简介
AOP(Aspect-Oriented Programming) 面向切面编程,它是OOP(Object-Oriented Programming)的补充,在OOP中是以class为基础模块,而AOP的基础模块是aspect
AOP概念
- Aspect:切面,就横切关注点的抽象。由pointcut(切入点)和advice(通知)组成
- Join point:连接点,程序执行过程中某些特定的点
- Advice:通知,是指在Join point(连接点)上执行的动作
- Pointcut:切入点,用来匹配Join point(连接点)的,在Spring中所有的方法都可以认为是join point,而pointcut就是通过一定的规则来匹配join point,对匹配上的方法进行增强操作
- Introduction:引入,为类型声明其他方法或字段。Spring AOP允许向任何建议的对象引入新的接口(以及相应的实现)。例如,您可以让bean实现IsModified接口,从而简化缓存。
- Target object:目标对象,指pointcut(切入点)匹配上,需要进行通知的对象。需要注意的是,目标对象不是原对象,而是被织入advice后产生的代理对象
- AOP proxy:AOP代理,类被AOP织入advice后会产生一个新类,这个类是结合了原逻辑与增加逻辑的代理类。AOP代理是JDK动态代理或者CGLIB代理
- Weaving:织入,将切面(Aspect)与其他对象连接起来,形成新的代理对象的过程。
通知(Advice)类型:
- 前置通知(Before advice):在连接点(join point)之前执行的通知,但这个通知不能阻止连接点执行之前的流程(除非抛出异常)
- 后置通知(After returning advice):在连接点(join point)正常执行返回后执行的通知
- 异常通知(After throwing advice):方法抛出异常退出时执行的通知
- 最终通知(After (finally) advice):连接点(join point)执行完成退出时执行的通知,无论正常或异常执行都会执行的通知
- 环绕通知(Around Advice):在连接点(join point)执行前和退出后都会执行的通知,这是最常用的通知
AOP的概念比较多,也比较复杂,理解起来也相对较麻烦。其实,我们只需要搞清楚几个角色,以及他们之间的关系就比较好理解了。我们知道,AOP是用来处理面向切面的编程,主要用于一些横切关注点的处理,这样描述还是比较抽象,不太好理解。我们知道AOP一个典型的使用场景就是日志的处理,按OOP的模式来处理日志,就是每个用到日志的类都要加日志的逻辑,而日志的处理逻辑都是相同的,这样会导致逻辑大量重复,而使用AOP就可以很好的解决这个问题,那么问题来了,AOP要如何解决这个问题?假如你来做这件事,会有哪些事需要做?
- 第一步,我们要定义日志记录要处理哪些事?也就是日志处理的逻辑,这就是AOP里面通知(Advice)要做的事
- 第二步,需要考虑哪些地方可以使用这些日志处理逻辑,也就是AOP中的连接点(join point),就是通知可以使用的范围(注意不是通知真正使用的范围)
- 第三步,日志逻辑要使用到哪些地方,就是AOP中的切入点(point cut),这才是通知真正使用的点,它其实是在连接点(join point)的范围内限定一个使用范围,将通知使用到限定的点(方法)上
- 最后,切面(Aspect)就是切入点(point cut)和通知(advice)共同组成的被称为横切关注点的抽象
实践
@AspectJ 支持
@AspectJ 是使用Java注解实现的AOP编码风格,Spring也支持@AspectJ的编码风格。
开启@AspectJ可以使用Java Configuration方式:
@Configuration
@EnableAspectJAutoProxy
public class Config{
}
或者XML方式:
<aop:aspectj-autoproxy/>
使用Spring AOP,需要做哪些事呢?
1.声明切面
@Component
@Aspect
public class MyAspect {
}
注意,当一个类被标注为Aspect后,此类就不能作为其他的Aspect的通知对象了,因为使用@Aspect会被排除在auto-proxying 机制之外
2.声明pointcut
@Pointcut("execution(* com.proxy.aop.UserService.*(..))")
public void testPointCut(){
}
声明一个point需要做两件事
- 声明一个方法
- 标注切入点标示符(pointcut signature)
上面简单定义了一个pointcut,表示匹配com.proxy.aop.UserService类下所有的方法的执行
类似“execution(* com.proxy.aop.UserService.*(…))”这样的表达式被称为切入点表达式,用来描述通知需要执行的范围
切点标志符(designator)
切点表达式由标志符(designator)和操作参数组成,类似“execution( greetTo(…))”这样的表达式,execution是标志符, 而圆括号里的 greetTo(…) 就是操作参数
execution
匹配连接点的执行, 例如 “execution(* show(…))” 表示匹配所有目标类中的 show() 方法. 这个是最基本的 pointcut 标志符
within
匹配特定包下的所有连接点, 例如 within(com.aop.demo.*) 表示 com.aop.demo 包中的所有连接点, 即包中的所有类的所有方法. 而 within(com.aop.demo.*Mapper) 表示在 com.aop.demo 包中所有以 Mapper结尾的类的所有的连接点
this 与 target
this 的作用是匹配一个 bean, 这个 bean(Spring AOP proxy) 是一个给定类型的实例(instance of). 而 target 匹配的是一个目标对象(target object, 即需要织入 advice 的原始的类), 此对象是一个给定类型的实例(instance of).
bean
匹配 bean 名字为指定值的 bean 下的所有方法
bean(*Mapper) // 匹配名字后缀为 Mapper的 bean 下的所有方法
bean(custService) // 匹配名字为 custService的 bean 下的所有方法
args
匹配参数满足要求的方法
// 匹配只有一个参数 name 的方法
@Before(value = "aspectMethod() && args(name)")
public void doSomething(String name) {
}
// 匹配第一个参数为 name 的方法
@Before(value = "aspectMethod() && args(name, ..)")
public void doSomething(String name) {
}
// 匹配第二个参数为 name 的方法
Before(value = "aspectMethod() && args(*, name, ..)")
public void doSomething(String name) {
}
@annotation
匹配由指定注解所标注的方法
@Pointcut("@annotation(com.aop.demo.checkConfig)") //匹配由注解 checkConfig所标注的方法
public void pointcut() {
}
常见的pointcut表达式
匹配方法签名
// 匹配指定包中的所有的方法
execution(* com.aop.demo.*(..))
// 匹配当前包中的指定类的所有方法
execution(* AppService.*(..))
// 匹配指定包中的所有 public 方法
execution(public * com.aop.demo.*(..))
// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法
execution(public int com.aop.demo.*(..))
// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法
execution(public int com.aop.demo.*(String name, ..))
匹配类型签名
// 匹配指定包中的所有的方法, 但不包括子包
within(com.aop.demo.*)
// 匹配指定包中的所有的方法, 包括子包
within(com.aop.demo..*)
// 匹配当前包中的指定类中的方法
within(UserService)
// 匹配一个接口的所有实现类中的实现的方法
within(UserDao+)
匹配Bean名称
// 匹配以指定名字结尾的 Bean 中的所有方法
bean(*Service)
组合匹配
// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)
// 匹配名字以 Service 结尾, 并且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)
3.声明advice
advice 是和一个 pointcut 表达式关联在一起的, 表示在匹配的 join point 的方法执行的前/后/周围 运行. pointcut 表达式可以是简单的一个 pointcut 名字的引用, 或者是完整的 pointcut 表达式.
@Pointcut("execution(* com.proxy.aop.*(..))")
public void testPointCut(){
}
@Before(value = "testPointCut()")
public void doSomething() {
}
也可以,pointcut和advice一起定义
@Before("execution(* com.proxy.aop.*(..))")
public void doSomething() {
}
环绕通知
环绕通知在一个方法执行之前和之后执行。它使得通知有机会 在一个方法执行之前和执行之后运行。而且它可以决定这个方法在什么时候执行,如何执行,甚至是否执行
环绕通知经常在某线程安全的环境下,你需要在一个方法执行之前和之后共享某种状态的时候使用。 请尽量使用最简单的满足你需求的通知。(比如如果简单的前置通知也可以适用的情况下不要使用环绕通知)
@Around("execution(* com.proxy.aop.*(..))")
public Object doSomething(ProceedingJoinPoint pjp) throws Throwable {
Object retVal = pjp.proceed();
return retVal;
}
其他类型的通知跟@Before的定义差不多