1. 概述
AOP(Aspect Oriented Programming)面向切面编程,指在程序运行期间,将某段代码动态的切入到指定方法的指定位置(方法的开始、结束、异常等)进行运行的这种编程方式。其中,动态代理可以实现AOP,但动态代理的缺点:
- 较为复杂,书写困难
- 若目标对象没有实现任何接口, jdk默认的动态代理无法为其创建代理对象
通过Spring实现的AOP功能,其底层就是动态代理,其优点:
- 简便的创建动态代理
- 实现简单,而且没有强制要求目标对象必须实现接口
2. AOP的术语
注:切面类的通知方法须为public
3. AOP使用步骤
导包
commons-logging-1.1.3.jar
spring-aop-4.0.0.RELEASE.jar
spring-beans-4.0.0.RELEASE.jar
spring-context-4.0.0.RELEASE.jar
spring-core-4.0.0.RELEASE.jar
spring-expression-4.0.0.RELEASE.jar
Spring支持面向切面编程(AOP)的包:
基础版:spring-aspects-4.0.0.RELEASE.jar
加强版:(即使目标对象没有实现任何接口也能创建动态代理)
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
写配置
- 将目标类和切面类(封装了通知方法(在目标方法执行前后执行的方法))加入到ioc容器中
- 告诉Spring哪个是切面类——使用
@Aspect
注解 - 告诉Spring切面类里的方法何时何地运行
- 开启基于注解的AOP模式
<!-- 开启基于注解的AOP功能;aop名称空间-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
Spring有5个通知注解
@Before
——前置通知:在目标方法之前运行;
@After
——后置通知:在目标方法结束之后
@AfterReturning
——返回通知:在目标方法正常返回之后
@AfterThrowing
——异常通知:在目标方法抛出异常之后运行
@Around
——环绕通知
try{
@Before
method.invoke(obj,args);
@AfterReturning
}catch(e){
@AfterThrowing
}finally{
@After
}
于是有:
测试
@Aspect
@Component
public class LogUtils {
@Before(value="execution(public int com.guigu.implement.MyCalculator.*(int, int))")
public static void logStart(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("【"+name+"】方法开始执行,用的参数列表【"+Arrays.asList(args)+"】");
}
@AfterReturning(value="execution(public int com.guigu.implement.MyCalculator.*(int, int))",returning="result")
public static void logReturn(JoinPoint joinPoint, Object result){
System.out.println("【"+joinPoint.getSignature().getName()+"】方法正常执行完成,计算结果是:"+result);
}
@AfterThrowing(value="execution(public int com.guigu.implement.MyCalculator.*(int, int))",throwing="exception")
public static void logException(JoinPoint joinPoint, Exception exception){
System.out.println("【"+joinPoint.getSignature().getName()+"】方法执行出现异常了,异常信息是:" + exception + ";这个异常已经通知测试小组进行排查");
}
@After(value="execution(public int com.guigu.implement.MyCalculator.*(int, int))")
public static void logEnd(JoinPoint joinPoint) {
System.out.println("【"+joinPoint.getSignature().getName()+"】方法最终结束了");
}
}
细节一:
- 从ioc容器中拿到目标对象,如果继承了接口,一定用其接口类型,不要用其本类;因为AOP的底层是动态代理,容器中保存的组件是他的代理对象——另一个实现了接口的对象
- 从ioc容器中拿到目标对象,如果没有继承接口,那么容器中保存的组件本类类型的对象
- 也可以通过id从ioc容器中拿到目标对象,其id默认目标类首字母小写
细节二:
切入点表达式
固定格式:
execution(访问权限符 返回值类型 方法全类名(参数表))
(1)*
通配符
- 匹配一个或者多个字符:
execution(public int com.atguigu.impl.MyMath*r.*(int, int))
- 匹配任意一个参数:第一个是
int
类型,第二个参数任意类型;例如:execution(public int com.atguigu.impl.MyMath*.*(int, *))
(且匹配的参数数量为2) - 只能匹配一层路径
- 权限位置不能写
*
;权限位置是可选的,因为也只能切public
..
通配符
- 匹配任意多个参数,任意类型参数
- 匹配任意多层路径:
execution(public int com.atguigu..MyMath*.*(..))
最精确的:
execution(public int com.atguigu.impl.MyMathCalculator.add(int,int))
最模糊的(不写):
execution(* *.*(..))
(2)逻辑表达式
&&
——切入的位置同时满足这两个表达式:
execution(public int com.atguigu..MyMath*.*(..))&&execution(* *.*(int,int))
||
——满足任意一个表达式即可、!
——只要不是这个位置都切入
细节三:
通知方法的执行顺序;
try{
@Before
method.invoke(obj,args);
@AfterReturning
}catch(e){
@AfterThrowing
}finally{
@After
}
正常执行: @Before
(前置通知)→@After
(后置通知)→@AfterReturning
(正常返回)
异常执行: @Before
(前置通知)→@After
(后置通知)→@AfterThrowing(方法异常)
细节四:
我们可以在通知方法运行的时候,拿到目标方法的详细信息;
- 只需要为通知方法的参数列表上写一个参数
JoinPoint joinPoint
——封装了当前目标方法的详细信息 - 告诉Spring哪个参数是用来接收异常的——使用
@AfterReturning
的throwing="exception"
属性 - 告诉Spring个参数是用来接收返回值的——使用
@AfterReturning
的returning
属性 Exception exception
——指定通知方法可以接收哪些类型的异常
细节五:
Spring对通知方法的要求不严格,唯一要求的就是方法的参数列表一定正确,因为通知方法是Spring利用反射调用的,每次方法调用须确定方法的参数列表;
参数表上的每一个参数,Spring都得知道是什么?
JoinPoint:认识
不知道的参数一定告诉Spring这是什么?
细节六:
- 使用
@Order(int)
注解改变多个切面执行顺序,数值越小,优先级越高 - 环绕通知只影响当前切面
4. 环绕通知
- 环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点
- 对于环绕通知来说,连接点的参数类型必须是
ProceedingJoinPoint
。它是JoinPoint
的子接口,允许控制何时执行,是否执行连接点 - 在环绕通知中需要明确调用
ProceedingJoinPoint
的proceed()
方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行 - 环绕通知优先于普通通知执行
- 注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用
joinPoint.proceed();
的返回值,否则会出现空指针异常
@Around("hahaMyPoint()")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
String name = pjp.getSignature().getName();
Object proceed = null;
try {
//@Before
System.out.println("【环绕前置通知】【"+name+"方法开始】");
//就是利用反射调用目标方法,即method.invoke(obj,args)
proceed = pjp.proceed(args);
//@AfterReturing
System.out.println("【环绕返回通知】【"+name+"方法返回,返回值"+proceed+"】");
} catch (Exception e) {
//@AfterThrowing
System.out.println("【环绕异常通知】【"+name+"】方法出现异常,异常信息:"+e);
//为了让外界能知道这个异常,这个一定要抛出
throw new RuntimeException(e);
} finally{
//@After
System.out.println("【环绕后置通知】【"+name+"】方法结束");
}
//反射调用后的返回值也须返回
return proceed;
}
[普通前置]
{
try{
环绕前置
环绕执行:目标方法执行
环绕返回
}catch(){
环绕出现异常
}finally{
环绕后置
}
}
[普通后置]
[普通方法返回/方法异常]
5. 以XML方式配置Aspect
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<!-- 基于注解的AOP步骤;
1、将目标类和切面类都加入到ioc容器中。@Component
2、告诉Spring哪个是切面类。@Aspect
3、在切面类中使用五个通知注解来配置切面中的这些通知方法都何时何地运行
4、开启基于注解的AOP功能
-->
<!-- 开启基于注解的AOP功能;aop名称空间-->
<!-- 基于配置的AOP-->
<bean id="myMathCalculator" class="com.atguigu.impl.MyMathCalculator"></bean>
<bean id="BValidateApsect" class="com.atguigu.utils.BValidateApsect"></bean>
<bean id="logUtils" class="com.atguigu.utils.LogUtils"></bean>
<!-- 需要AOP名称空间 -->
<aop:config>
<aop:pointcut expression="execution(* com.atguigu.impl.*.*(..))" id="globalPoint"/>
<!-- 普通前置 ===== 目标方法 =====(环绕执行后置/返回)====s普通后置====普通返回 -->
<!-- 指定切面:@Aspect -->
<aop:aspect ref="logUtils" order="1">
<!-- 配置哪个方法是前置通知;method指定方法名
logStart@Before("切入点表达式")
-->
<!-- 当前切面能用的 -->
<aop:around method="myAround" pointcut-ref="mypoint"/>
<aop:pointcut expression="execution(* com.atguigu.impl.*.*(..))" id="mypoint"/>
<aop:before method="logStart" pointcut="execution(* com.atguigu.impl.*.*(..))"/>
<aop:after-returning method="logReturn" pointcut-ref="mypoint" returning="result"/>
<aop:after-throwing method="logException" pointcut-ref="mypoint" throwing="exception"/>
<aop:after method="logEnd" pointcut-ref="mypoint"/>
</aop:aspect>
<aop:aspect ref="BValidateApsect" order="3">
<aop:before method="logStart" pointcut-ref="globalPoint"/>
<aop:after-returning method="logReturn" pointcut-ref="globalPoint" returning="result"/>
<aop:after-throwing method="logException" pointcut-ref="globalPoint" throwing="exception"/>
<aop:after method="logEnd" pointcut-ref="globalPoint"/>
</aop:aspect>
<!-- 在切面类中使用五个通知注解来配置切面中的这些通知方法都何时何地运行 -->
</aop:config>
<!--注解:快速方便
配置:功能完善;重要的用配置,不重要的用注解;
-->
</beans>
对比:
- 注解方式——快速方便
- 配置方式——功能完善;所以重要的用配置,不重要的用注解;
. AOP的应用场景
- AOP加日志保存到数据库
- AOP做权限验证、安全检查
- AOP做事务控制