实验环境:
1.IOC容器中保存的是组件的代理对象:
1.1、IOC容器中保存的是组件的代理对象:
代理对象和目标对象唯一的关联就是接口类:
Calculator bean = ioc.getBean(Calculator.class);
所以用接口类型去获取代理对象
//所以以上代码只能获取接口对象,而不能去获取目标对象MyMathCalculator.java
//接口一般不加在容器中
1.2、cglib为没有接口的组件也可以创建代理对象:
//没有接口,参数就用本类类型,也可以创建代理对象
MyMathCalculator bean = ioc.getBean(MyMathCalculator.class);
2.切入点表达式写法(通配符):
- 一个切入点表达式的写法:
@Before("execution(public int com.atguigu.impl.MyMathCalculator.*(int, int))")
public static void logStart(){
System.out.println("[]" + "方法开始执行了,用的参数列表是:[]");
}
/**
* 切入点表达式的写法:
* 固定格式:execution(访问权限符 返回值类型 方法全类名(参数表))
* 通配符:* 和 ..
* 1) * :
* 1.表示可以匹配一个或者多个字符,execution(public int com.atguigu.impl.MyMath*.*(int, int))
* 2.匹配任意一个参数,execution(public int com.atguigu.impl.MyMathCalculator.*(int, *))
* 3.在路径里,只能匹配一层路径。execution(public int com.atguigu.*.MyMathCalculator.*(int, int))
* 4.权限位置不可以用,返回值类型可以用
* 2) .. :
* 1.匹配任意多个参数和任意类型参数:execution(public int com.atguigu.impl.MyMathCalculator.*(..))
* 2.匹配任意多层路径:execution(public int com.atguigu..MyMathCalculator.*(int, int))
*
* 最模糊的:execution(* *.*(..))千万别这么写
* 表达式里面还可以用:&&、||、!
* 例:execution(public int com.atguigu..MyMathCalculator.*(int, int) && public int com.atguigu..MyMathCalculator.*(int, int))
*/
3.通知方法的执行顺序:
/**
* try{
* @Before
* method.invoke(obj, args);
* @AfterReturning
* }catch(e){
* @AfterThrowing
* }finally{
* @After
* }
* 通知方法的执行顺序:
* 正常执行:@Before(前置通知)——》@After(后置通知)——》@AfterReturning(正常返回)
* 异常执行:@Before(前置通知)——》@After(后置通知)——》@AfterThrowing(方法异常)
*/
@Test
public void test02(){
Calculator bean = ioc.getBean(Calculator.class);
bean.div(1, 0);
}
4.JoinPoint获取目标方法的信息:
- 只需要为通知方法的参数列表上写一个参数,joinPoint。
/**
* 写入目标方法的详细信息:
* 只需要为通知方法的参数列表上写一个参数,joinPoint
* JoinPoint joinPoint:封装了当前目标方法的详细信息
*/
@Before("execution(public int com.atguigu.impl.MyMathCalculator.*(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) +"]");
}
5.throwing/returning来指定哪个参数用来接收异常:
5.1使用returning来接受返回值:
/**
* @param joinPoint
* @param result
* 使用spring来告诉这个result来接受返回值。
*/
@AfterReturning(value = "execution(public int com.atguigu.impl.MyMathCalculator.*(int, int))", returning = "result")
public static void logReturn(JoinPoint joinPoint, Object result){
Object[] args = joinPoint.getArgs();//获取目标方法运行时使用的参数
Signature signature = joinPoint.getSignature();//获取到方法签名
String name = signature.getName();
System.out.println("["+ name +"]" + "方法正常执行完成了,计算结果是:"+result);
}
//想在目标方法异常时执行
@AfterThrowing(value = "execution(public int com.atguigu.impl.MyMathCalculator.*(int, int))", throwing = "e")
public static void logException(JoinPoint joinPoint, Exception e){
Object[] args = joinPoint.getArgs();//获取目标方法运行时使用的参数
Signature signature = joinPoint.getSignature();//获取到方法签名
String name = signature.getName();
System.out.println("["+ name +"]"+ "出现了异常,异常原因是:"+"["+e.getCause()+"]");
}
5.2使用throwing来接受异常:
/**
* @param joinPoint
* @param result
* 使用spring来告诉这个result来接受返回值。
*/
//想在目标方法异常时执行
@AfterThrowing(value = "execution(public int com.atguigu.impl.MyMathCalculator.*(int, int))", throwing = "e")
public static void logException(JoinPoint joinPoint, Exception e){
Object[] args = joinPoint.getArgs();//获取目标方法运行时使用的参数
Signature signature = joinPoint.getSignature();//获取到方法签名
String name = signature.getName();
System.out.println("["+ name +"]"+ "出现了异常,异常原因是:"+"["+e.getCause()+"]");
}
6.Spring对通知方法的约束(参数表一定正确):
- spring对于通知方法的什么返回值类型不是太注意,但是参数列表一定正确。
//就像下面的异常写法,很容易出错,空指针异常太小了
@AfterThrowing(value = "execution(public int com.atguigu.impl.MyMathCalculator.*(int, int))", throwing = "e")
public static void logException(JoinPoint joinPoint, NoPointException e){
Object[] args = joinPoint.getArgs();//获取目标方法运行时使用的参数
Signature signature = joinPoint.getSignature();//获取到方法签名
String name = signature.getName();
System.out.println("["+ name +"]"+ "出现了异常,异常原因是:"+"["+e+"]");
}
7.抽取可重用的切入点表达式:
/**
* 抽取可重用的切入点表达式
* 1.随便声明一个没有实现返回值void的空方法
* 2.给方法上标注@Pointcut注解
*/
@Pointcut("execution(public int com.atguigu.impl.MyMathCalculator.*(int, int))")
public void hahaMyPoint(){}
@Before("hahaMyPoint()")
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 = "hahaMyPoint()", returning = "result")
public static void logReturn(JoinPoint joinPoint, Object result){
Object[] args = joinPoint.getArgs();//获取目标方法运行时使用的参数
Signature signature = joinPoint.getSignature();//获取到方法签名
String name = signature.getName();
System.out.println("["+ name +"]" + "方法正常执行完成了,计算结果是:"+result);
}
@AfterThrowing(value = "hahaMyPoint()", throwing = "e")
public static void logException(JoinPoint joinPoint, Exception e){
Object[] args = joinPoint.getArgs();//获取目标方法运行时使用的参数
Signature signature = joinPoint.getSignature();//获取到方法签名
String name = signature.getName();
System.out.println("["+ name +"]"+ "出现了异常,异常原因是:"+"["+e+"]");
}
@After("hahaMyPoint()")
public static void logEnd(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();//获取目标方法运行时使用的参数
Signature signature = joinPoint.getSignature();//获取到方法签名
String name = signature.getName();
System.out.println("["+ name +"]" + "方法执行结束了。");
}
8.环绕通知@Around:
/**
* 抽取可重用的切入点表达式
* 1.随便声明一个没有实现返回值void的空方法
* 2.给方法上标注@Pointcut注解
*/
@Pointcut("execution(public int com.atguigu.impl.MyMathCalculator.*(int, int))")
public void hahaMyPoint(){}
/**
* @Around:是spring中最强大的通知方法,动态代理。
* try{
* @Before,前置通知
* method.invoke(obj, args);
* @AfterReturning,返回通知
* }catch(e){
* @AfterThrowing,异常通知
* }finally{
* @After,后置通知
* }
* 四合一就是环绕通知;环绕通知里面有一个参数,ProceedingJoinPoint(进程通知)
*/
@Around("hahaMyPoint()")
public Object myArround(ProceedingJoinPoint pjp) throws Throwable {
//获取目标方法运行时使用的参数
Object[] args = pjp.getArgs();
String name = pjp.getSignature().getName();
Object proceed = null;
try {
System.out.println("[环绕前置通知],"+name+"方法开始");
//利用反射调用目标方法,相当于method.invoke(obj, args);
proceed = pjp.proceed(args);
System.out.println("[环绕返回通知],"+name+"方法返回,返回值:"+ proceed);
} catch (Exception e) {
System.out.println("[环绕异常通知],异常信息:"+e);
} finally {
System.out.println("[环绕后置通知],"+name+"方法最终结束。");
}
//反射调用后的返回值一定返回出去
return proceed;
}
9.环绕通知的执行顺序,抛出异常让其他通知感受到:
- 如果环绕通知和普通通知一起运行:
- 正常情况下:
- 异常情况下:
注意:环绕通知是优于普通通知执行
- 若两者混合使用,注意执行顺序为:
{
try{
【环绕前置】
//【普通前置】
环绕执行:目标方法执行
【环绕返回】
} catch(){
【环绕异常】
} fianlly{
【环绕后置】
}
}
【普通后置】
【普通方法返回/方法异常返回】
环绕前置---普通前置---目标方法执行---环绕正常返回/出现异常---环绕后置---普通后置---普通返回或异常
注意:环绕通知会吃掉异常,为了让外界知道这个异常,一定要把这个异常跑出去。
@Around("hahaMyPoint()")
public Object myArround(ProceedingJoinPoint pjp) throws Throwable {
//获取目标方法运行时使用的参数
Object[] args = pjp.getArgs();
String name = pjp.getSignature().getName();
Object proceed = null;
try {
System.out.println("[环绕前置通知],"+name+"方法开始");
//利用反射调用目标方法,相当于method.invoke(obj, args);
proceed = pjp.proceed(args);
System.out.println("[环绕返回通知],"+name+"方法返回,返回值:"+ proceed);
} catch (Exception e) {
System.out.println("[环绕异常通知],异常信息:"+e);
//抛出异常
throw new RuntimeException(e);
} finally {
System.out.println("[环绕后置通知],"+name+"方法最终结束。");
}
//反射调用后的返回值一定返回出去
return proceed;
}
10.多切面运行顺序:
-
两个切面类:
-
写了两个切面方法,这两个切面方法在测试类中运行时的结果是:
-
我们发现先开始的切面方法后结束,后开始的切面方法先结束。
-
包裹的先后顺序导致的:
-
而这里面的包裹顺序看的是,切面类第一个首字母的大小,L大于V所以LogUtils.java切面类包裹在最外面。
@Order(1)//使用order可以改变切面顺序,数值越小优先级越高。
-
在LogUtils.java切面类中加入环绕通知:
-
所以环绕通知只是影响当前切面。