文章目录
使用AOP可以实现在不侵入到目标方法的情况下,动态的将某一模块加入IoC容器中。AOP的应用场景一般在
日志
、权限验证
、安全检查
、事务控制
。
一、IoC容器中保存的是组件的代理对象
假设使用动态代理后,从容器中获取bean的类型,可以发现,获得的是一个代理类型,说明AOP的底层就是动态代理,Ioc容器中保存的是组件的代理对象。
@ContextConfiguration(locations = "classpath:applicationContext.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class AOPTest {
@Autowired
Calculator ioc;
@Test
public void test() {
System.out.println(ioc.getClass());
}
}
补充:如果从IoC容器中使用类型来获取到目标对象,一定要使用它的接口类型来获取。如果没有接口,Spring的cglib会自动为我们创建代理对象。
二、切入点表达式的写法
固定格式:execution(访问权限符
返回值类型
方法全类名(参数表)
)
通配符:
(1)*
:
- 匹配一个或多个字符。如:
"execution(public int com.gql.impl.myMathCalculator.*.Math*(int, int))"
- 匹配任意一个参数。如:
"execution(public int com.gql.impl.myMathCalculator.*(int, *))"
- 如果
*
放在路径里,只能匹配一层路径。如:"execution(public int com.gql.impl.myMathCalculator.*(int, int))"
- 权限位置不能写
*
默认就是public的。
(2)..
:
- 匹配任意任意个任意类型参数。如:
"execution(public int com.gql.impl.myMathCalculator.*(..)"
- 匹配任意多层路径。如:
"execution(public int com.gql..myMath*(..)"
记住两种切入点表达式:
最模糊的:execution(* *.*(..))
。代表任意包下任意类的任意方法,
最精确的:execution(public int com.gql.impl.myMathCalculator.add(int,int))
另外:切入点表达式还支持"&&"、"||"、"!"
三、通知方法的执行顺序
正常执行:
- ① @Before(前置通知)
- ② @After(后置通知)
- ③ @AfterReturning(正常返回)
异常执行:
- ①@Before(前置通知)
- ② @After(后置通知)
- ③ @AfterThrowing(方法异常)
四、JoinPoint获取目标方法的信息
在通知方法运行的时候,拿到目标方法的详细信息。只需要在通知方法的参数列表上写一个参数。
JoinPoint joinPoint:封装了当前目标方法的详细信息。
- 获得方法名:joinPoint.getSignature().getName()
- 获得参数列表:Arrays.asList(joinPoint.getArgs())
五、throwing、returning指定哪个参数用来接收异常、返回值
使用returning获得方法返回的结果:
@AfterReturning(value = "execution(public int com.gql.impl.myMathCalculator.*(int, int))", returning = "result")
public static void logReturn(JoinPoint joinPoint, Object result) {
System.out.println("[" + joinPoint.getSignature().getName() + "]方法执行完成,计算结果是[" + result + "]");
}
使用throwing获得异常信息:
@AfterThrowing(value = "execution(public int com.gql.impl.myMathCalculator.*(int, int))", throwing = "e")
public static void logException(JoinPoint joinPoint, Exception e) {
System.out.println("[" + joinPoint.getSignature().getName() + "方法出现异常,异常信息是:");
}
这种写法和Ajax接收服务器数据特别像,只不过要告诉Spring这个变量的作用是接收返回值还是抛出异常。
六、Spring对通知方法的约束
Spring对通知方法的要求不严格,唯一有要求的是方法的参数列表一定不能乱写。 通知方法是Spring利用反射调用的,每次方法调用需要确定这个方法的参数表的值。即参数列表上的每一个参数,Spring都得知道它是什么。
- Spring认识的参数:JoinPoint joinPoint。
- Spring不认识的参数:使用直接中的属性告诉Spring。
补充:在进行接收异常和接收返回值的时候,最好在参数中将异常的范围、返回值的类型写大一点。
七、抽取可重用的切入点表达式
在切面(Aspect)中声明一个无返回类型的空方法,并为方法标注@Pointcut注解。
@Pointcut("execution(public int com.gql.impl.myMathCalculator.*(int, int))")
public void myPoint() {
}
后来的方法需要写表达式时只需要将value的值填写为方法名即可。
八、环绕通知实际上就是动态代理
@Around环绕通知是Spring中最强大的通知,环绕通知实际上就是动态代理。
* try{
* //前置通知
* method.invoke(obj,args);
* //返回通知
* }catch(e){
* //异常通知
* }finally{
* //后置通知
* }
环绕通知,就是上面代码中的四个通知合起来的效果。下面演示环绕通知:
@Around("myPoint()")
public Object myAround(ProceedingJoinPoint pjp) {
Object proceed = null;
String methodName = pjp.getSignature().getName();
try {
System.out.println("环绕通知前:" + methodName + "方法开始执行");
proceed = pjp.proceed(pjp.getArgs());// 相当于method.invoke()
System.out.println("环绕通知后:" + methodName + "方法返回,返回值是" + proceed);
} catch (Throwable e) {
System.out.println(methodName + "方法出现异常,异常信息:" + e);
throw new RuntimeException(e);
} finally {
System.out.println("环绕后置通知:" + methodName + "方法结束");
}
return proceed;
}
上面的环绕通知中:
- pjp.proceed(pjp.getArgs())实际上就是method.invoke(),即通过反射调用了方法。
如果程序正常执行:
如果程序出现异常:
九、同时声明环绕通知和普通通知时的执行顺序
环绕通知优先于普通通知执行,下面说明两者都声明时的执行顺序:
* [普通前置]
* {
* try{
* 环绕:前置
* 目标方法执行
* 环绕:返回
* }catch(){
* 环绕:出现异常
* }finally{
* 环绕:后置
* }
* [普通后置]
* [普通返回/异常]
真正的执行顺序是:
- ①环绕:前置 ②普通前置。(这两个的执行顺序是随机的,不必关注)
- ③目标方法执行
- ④环绕:返回/环绕:出现异常
- ⑤环绕:后置
- ⑥普通后置
- ⑦普通返回/异常
注意:如果环绕通知拿到异常不处理,普通通知就不会拿到异常,则普通通知感受到的方法是正常的,这是由于环绕通知把异常catch掉了。因此,为了让外界能够知道这个异常,环绕通知中的异常一定要抛出去。
十、多切面运行顺序
下图的结果是LogUtils切面
,VaApsect切面
,LogUtils切面中的环绕通知
,同时作用与一个方法时的执行顺序:
- 默认情况下是以开头字母顺序决定哪个切面先执行。
- 可以通过@Order(num)指定切面执行顺序,数值越小优先级越高。
- 牢记一句话:
环绕是在哪个切面内,就只作用于哪个切面内!