1 Spring AOP中的基本概念
(1)连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
通俗的讲:
层于层之间调用的过程中,目标层中可供调用的方法,就称之为连接点。
(2)切入点(Pointcut):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。
通俗的讲:
在连接点的基础上增加上切入规则选择出需要进行增强的连接点这些基于切入规则选出来的连接点就称之为切入点。
(3)切面(Aspect):狭义上就是当spring拦截下切入点后将这些切入点交给处理类进行功能的增强,这个处理类就称之为切面。广义上来讲将spring底层的代理切入点和处理类加在一起实现的对层与层之间调用过程进行增强的机制称之为切面。
(4)通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知(通知的类型将在后面部分进行讨论)。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
通俗的讲:
在spring底层的代理拦截下切入点后,将切入点交给切面类,切面类中就要有处理这些切入点的方法,这些方法就称之为通知(也叫增强方法)。针对于切入点执行的过程,通知还分为不同的类型,分别关注切入点在执行过程中的不同的时机。
(5)目标对象(Target Object): 被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
通俗的讲:
就是真正希望被访问到的对象。spring底层的动态代理对他进行了代理,具体能不能真的访问到目标对象,或在目标对象真正执行之前和之后是否做一些额外的操作,取决于切面。
2 切入点表达式
在spring配置文件中配置切面,通知,切入点规则:
<!-- 配置AOP -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect ref="firstAspect">
<!-- 配置通知 -->
<aop:before method="before" pointcut="within(com.yyy.service.UserServiceImpl)"/>
</aop:aspect>
</aop:config>
(1)bean表达式(重点)
bean表达式一般应用于类级别,实现粗粒度的切入点定义
bean(”userServiceImpl")指定一个userServiceImpl类中所有方法。
bean("*ServiceImpl")指定所有后缀为ServiceImpl的类中所有方法。
说明:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内部的名字应该是spring容器中某个bean的name。
<aop:pointcut expression="bean(userServiceImpl)" id="pc01"/>
(2)within表达式(了解)
通过类名进行匹配粗粒度的切入点表达式
within(包名.类名)
在within表达式中可以使用*号匹配符,匹配指定包下所有的类,注意,只匹配当前包,不包括当前包的子孙包。
<!-- 配置AOP -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="within(com.yyy.service.*)" id="pc01"/>
<!-- 配置切面 -->
<aop:aspect ref="firstAspect">
<!-- 配置通知 -->
<aop:before method="before" pointcut-ref="pc01"/>
</aop:aspect>
</aop:config>
在within表达式中也可以用*号匹配符,匹配包
<!-- 配置AOP -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="within(com.yyy.*.*)" id="pc01"/>
<!-- 配置切面 -->
<aop:aspect ref="firstAspect">
<!-- 配置通知 -->
<aop:before method="before" pointcut-ref="pc01"/>
</aop:aspect>
</aop:config>
在within表达式中也可以用…*号匹配符,匹配指定包下及其子孙包下的所有的类
<!-- 配置AOP -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="within(com.yyy..*)" id="pc01"/>
<!-- 配置切面 -->
<aop:aspect ref="firstAspect">
<!-- 配置通知 -->
<aop:before method="before" pointcut-ref="pc01"/>
</aop:aspect>
</aop:config>
(3)execution()表达式(了解)
细粒度的切入点表达式,可以以方法为单位定义切入点规则
语法:
execution(返回值类型 包名.类名.方法名(参数类型,参数类型…))
例子:
<aop:pointcut expression="execution(void com.yyy.service.*.add())" id="pc01"/>
<aop:pointcut expression="execution(* com.yyy.service.*.add())" id="pc01"/>
<aop:pointcut expression="execution(* com.yyy.service.*.add())" id="pc01"/>
<aop:pointcut expression="execution(* com.yyy.service.*.add(int))" id="pc01"/>
<!-- 切出指定包及其子孙包下所有的类中的任意方法,参数数量及类型不限,返回值类型不限 -->
<aop:pointcut expression="execution(* com.yyy..*.add(..))" id="pc01"/>
(4)@annotation表达式(重点)
@annotaion表达式应用于方法级别,实现细粒度的切入点表达式定义
<aop:pointcut expression="@annotation(com.yyy.RequiredCache)" id="pc01"/>
其中RequiredCache是我们自定义的注解,此切入点表示匹配有此注解描述的方法。
3 Spring的五大通知类型
(1)前置通知
在目标方法执行之前执行执行的通知
前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象和目标方法相关的信息。
注意,如果接收JoinPoint,必须保证其为方法的第一个参数,否则报错。
配置方法:
<aop:before method="before" pointcut-ref="pc01"/>
@Component
public class FirstAspect{
public void before(JoinPoint jp){
Class clz = jp.getTarget().getClass();
Signature signature = jp.getSignature();
String name = signature.getName();
System.out.println("目标对象:["+clz+"],方法签名:["+name+"]";
}
}
(2)环绕通知
在目标方法执行之前和之后都可以执行额外代码的通知。
在环绕通知中必须显式的调用目标方法,否则目标方法不会执行。
这个显式调用是通过ProceedingJoinPoint来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,这个参数必须处在环绕通知的第一个形参位置。
**ProceedingJoinPoint是JoinPoint的子类,要注意,只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint。
<aop:before method="around" pointcut-ref="pc01"/>
@Component
public class FirstAspect{
public Object around(ProceedingJoinPoint jp) throws Throwable{
System.out.println("调用之前");
Object obj = jp.proceed(); //显示的调用目标方法
System.out.println("调用之后");
}
}
环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null。
环绕通知有
控制目标方法是否执行
目标方法执行之前或之后执行额外代码
控制是否返回返回值
改变返回值
的能力
环绕通知虽然有这样的能力,但一定要慎用,要小心不要破坏了软件分层的“高内聚 低耦合”的目标。
(3)后置通知
在目标方法执行之后执行的通知。
在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息,但是这个参数必须处在参数列表的第一个。
另外,可以通过配置参数获取目标方法的返回值
<aop:before method="afterReturn" pointcut-ref="pc01" returning="msg"/>
public void afterReturn(JoinPoint jp,Object msg){
}
(4)异常通知
在目标方法抛出异常时执行的通知
可以配置传入JoinPoint获取目标对象和目标方法相关信息,但必须处在参数列表第一位。
另外,还可以配置参数,让异常通知接收到目标方法抛出的异常对象
<aop:before method="afterThrow" pointcut-ref="pc01" throwing="e"/>
public void afterReturn(JoinPoint jp,Throwable e){
}
(5)最终通知
**在目标方法执行之后执行的通知。**和后置通知不同之处在于,后置通知是在方法正常返回后执行的通知,如果方法没有正常返-例如抛出异常,则后置通知不会执行。而最终通知无论如何都会在目标方法调用过后执行,即使目标方法没有正常的执行完成。另外,后置通知可以通过配置得到返回值,而最终通知无法得到。
<aop:before method="after" pointcut-ref="pc01" throwing="e"/>
最终通知也可以额外接收一个JoinPoint参数,来获取目标对象和目标方法相关信息,但一定要保证必须是第一个参数。
4 五种通知执行的顺序
(1)在目标方法没有抛出异常的情况下:
前置通知
环绕通知的调用目标方法之前的代码 //取决于配置顺序
目标方法
环绕通知的调用目标方法之后的代码*
后置通知
最终通知 //取决于配置顺序
(2)在目标方法抛出异常的情况下:
前置通知
环绕通知的调用目标方法之前的代码 //取决于配置顺序
目标方法抛出异常
异常通知
最终通知 //取决于配置顺序
(3)如果存在多个切面:
多切面执行时,采用了责任链设计模式。
切面的配置顺序决定了切面的执行顺序,多个切面执行的过程,类似于方法调用的过程,在环绕通知的proceed()执行时,去执行下一个切面或如果没有下一个切面执行目标方法,从而达成了如下的执行过程:
如果目标方法抛出异常:
(4)五种通知的常见使用场景
5 SpringAOP的原理
Spring在创建bean时,除了创建目标对象bean之外,会根据aop的配置,生成目标对象的代理对象,将其存储,之后获取bean时得到的其实是代理对象,在代理对象中,根据配置的切入点规则,决定哪些方法不处理直接执行目标方法,哪些方法拦截后进行增强,需要增强的方法拦截后根据配置调用指定切面中的指定通知执行增强操作。
Spring自动为目标对象生成代理对象,默认情况下,如果目标对象实现过接口,则采用java的动态代理机制,如果目标对象没有实现过接口,则采用cglib动态代理。
开发者可以可以在spring中进行配置,要求无论目标对象是否实现过接口,都强制使用cglib动态代理。
<!-- 配置AOP -->
<aop:config proxy-target-class="true">
<!-- 配置切入点 -->
<aop:pointcut expression="within(com.yyy..*)" id="pc01"/>
<!-- 配置切面 -->
<aop:aspect ref="firstAspect">
<!-- 配置通知 -->
<aop:before method="before" pointcut-ref="pc01"/>
</aop:aspect>
</aop:config>
6 AOP的注解方式实现
spring也支持注解方式实现AOP,相对于配置文件方式,注解配置更加的轻量级,配置、修改更加方便,是目前最流行的方式。
(1)开启AOP的注解配置方式
<!-- aop注解配置 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
(2)配置切面和切入点规则
@Component
@Aspect
public class FirstAspect{
@Before("execution(* com.yyy.service.*.*(..))")
public void before(){
System.out.println("前置通知");
}
}
(3)如果一个切面中多个通知重复使用同一个切入点表达式,则可以将该切入点表达式单独定义,后续引用,注意,在当前切面中通过注解定义的切入点只在当前切面中起作用,其他切面看不到。
@Component
@Aspect
public class FirstAspect{
@Pointcut("execution(* com.yyy.service.*.*(..))")
public void pc01(){
}
@Before("pc01()")
public void before(){
System.out.println("前置通知");
}
//此切入点表达式表示要切出com.yyy.service包下任意类的任意方法并且要求此方法必须有注解,注解类型必须为ax对应的类型Trans
@Around("pc01()"&& "@annotation(ax)")
public Object around(ProceedingJoinPoint jp,Trans ax) throws Throwable{
//1.获取目标对象
Object obj = pjp.getTarget();
//2.获取目标方法
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();//接口上的方法
//3.获取实现类上的目标方法
Method instMethod = obj.getClass().getMethod(method.getName(), method.getParameterTypes());
}
//在后置通知的注解中,也可以额外配置一个returning属性,来指定一个参数名接受目标方法执行后的返回值
@AfterReturning(value="pc01()",return="msg")
public void afterReturning(String msg){
System.out.println("目标方法执行的返回值:"+msg);
}
@AfterThrowing(value="pc01()",throwing="e")
public void afterThrow(Throwable e){
System.out.println("目标方法抛出的异常:"+e.getMessage());
}
}