AOP 面向切面的编程
一、面向切面的编程需求的产生
- 代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。
- 代码分散: 以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块。
二、实现面向切面的编程
- 将需要实现AOP的类注入到Spring容器中,例如:
1 package com.neuedu.aop; 2 3 import org.springframework.stereotype.Component; 4 5 @Component 6 public class RawCaculator implements MathCaculator{ 7 8 @Override 9 public int add(int i, int j) { 10 int rs=i+j; 11 System.out.println(i+"+"+j+"="+rs); 12 return rs; 13 } 14 15 @Override 16 public int sub(int i, int j) { 17 int rs=i-j; 18 System.out.println(i+"-"+j+"="+rs); 19 return rs; 20 } 21 22 @Override 23 public int mul(int i, int j) { 24 int rs=i*j; 25 System.out.println(i+"*"+j+"="+rs); 26 return rs; 27 } 28 29 @Override 30 public int div(int i, int j) { 31 int rs=i/j; 32 System.out.println(i+"/"+j+"="+rs); 33 return rs; 34 } 35 36 }
- 实现切面类
- 使用前置通知、后置通知、返回通知、异常通知实现切面类,注入到Spring容器中
1 package com.neuedu.aop; 2 3 import static org.hamcrest.CoreMatchers.nullValue; 4 5 import java.util.Arrays; 6 import java.util.List; 7 8 import org.aspectj.lang.JoinPoint; 9 import org.aspectj.lang.ProceedingJoinPoint; 10 import org.aspectj.lang.Signature; 11 import org.aspectj.lang.annotation.After; 12 import org.aspectj.lang.annotation.AfterReturning; 13 import org.aspectj.lang.annotation.AfterThrowing; 14 import org.aspectj.lang.annotation.Around; 15 import org.aspectj.lang.annotation.Aspect; 16 import org.aspectj.lang.annotation.Before; 17 import org.springframework.core.annotation.Order; 18 import org.springframework.stereotype.Component; 19 20 @Component 21 //@Aspect表明当前类是一个切面类 22 @Aspect 23 //@Order表示切面执行顺序,value值越小优先级越高 24 @Order(value=50) 25 public class CaculatorAspect { 26 @Before(value = "execution(public int com.neuedu.aop.RawCaculator.*(int, int))") 27 public void showBeginLog(JoinPoint point){ 28 //System.out.println("【日志】【前置通知】"); 29 //获得参数列表: 30 Object[] args = point.getArgs(); 31 List<Object> asList = Arrays.asList(args); 32 //获得方法名: 33 Signature signature = point.getSignature(); 34 String name = signature.getName(); 35 System.out.println("【日志】【前置通知】 目标方法名为:"+name+"参数为:"+asList); 36 } 37 @AfterThrowing(value = "execution(public int com.neuedu.aop.RawCaculator.*(..))" ,throwing="ex") 38 public void showThrowing(JoinPoint point,Exception ex){ 39 //System.out.println("【日志】【异常通知】"); 40 System.out.println("【日志】【异常通知】 异常信息:"+ex); 41 } 42 @After(value = "execution(public int com.neuedu.aop.RawCaculator.*(..))") 43 public void showAfter(){ 44 System.out.println("【日志】【后置通知】"); 45 } 46 @AfterReturning(value = "execution(* * .*(..))",returning="result") 47 public void showAfterReturning(JoinPoint point,Object result){ 48 //System.out.println("【日志】【返回通知】"); 49 System.out.println("【日志】【返回通知】 目标方法的返回值为"+result); 50 } 51 }
- 使用前置通知、后置通知、返回通知、异常通知实现切面类,注入到Spring容器中
- 将需要实现AOP的类注入到Spring容器中,例如:
使用黄色背景标注的代码是声明的通知,其中包括通知类型和切入点表达式,以下就是对通知类型的介绍,切入点表达式在最后
@Before 前置通知:在方法执行之前执行的通知
@After 后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候
@AfterReturning 返回通知:
-
-
-
-
- 无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
- 在返回通知中访问连接点的返回值:
- 在返回通知中,只要将returning属性添加到@AfterReturning注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称
- 必须在通知方法的签名中添加一个同名参数。在运行时Spring AOP会通过这个参数传递返回值
- 原始的切点表达式需要出现在pointcut属性中
-
-
-
@AfterThrowing 异常通知:只在连接点抛出异常时才执行异常通知.:
-
-
-
- 将throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。
-
- 如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行
-
- 将throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。
-
-
2.使用环绕通知实现切面类,注入到Spring容器中
1 package com.neuedu.aop; 2 3 import java.util.Arrays; 4 import java.util.List; 5 6 import org.aspectj.lang.ProceedingJoinPoint; 7 import org.aspectj.lang.Signature; 8 import org.aspectj.lang.annotation.Around; 9 import org.aspectj.lang.annotation.Aspect; 10 import org.springframework.core.annotation.Order; 11 import org.springframework.stereotype.Component; 12 13 @Component 14 //@Aspect表明当前类是一个切面类 15 @Aspect 16 @Order(value=40) 17 public class secondAspect { 18 @Around(value = "execution(* * .*(..))") 19 public Object Around(ProceedingJoinPoint point){ 20 Object result=null; 21 Object[] args = point.getArgs(); 22 List<Object> asList = Arrays.asList(args); 23 Signature signature = point.getSignature(); 24 String name = signature.getName(); 25 try{ 26 System.out.println("【事物日志】【前置通知】 目标方法名为:"+name+"参数为:"+asList); 27 try { 28 result=point.proceed(args); 29 } finally{ 30 System.out.println("【事物日志】【后置通知】"); 31 } 32 System.out.println("【事物日志】【返回通知】 目标方法的返回值为"+result); 33 }catch (Throwable e) { 34 System.out.println("【事物日志】【异常通知】 异常信息:"+e); 35 } 36 return result; 37 } 38 }
@Around 环绕通知:
-
-
-
-
- 环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
-
对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
-
在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
-
注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。
-
-
-
3.实现测试类
package junit.test; import static org.junit.Assert.*; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.neuedu.aop.MathCaculator; import com.neuedu.aop.RawCaculator; public class TestCaculator { @Test public void test() { ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); //MathCaculator bean = ioc.getBean(RawCaculator.class); 不可用 (代表类 类型不一样) //rawCaculator即使使用注解时代表类的id;又是xml配置文件里的id MathCaculator bean = (MathCaculator) ioc.getBean("rawCaculator"); bean.add(10, 5); System.out.println(); bean.sub(14, 5); System.out.println(); bean.mul(8, 7); System.out.println(); bean.div(10, 0); } }
三、关于切入点表达式
1.作用:
通过表达式的方式定位一个或多个具体的连接点。
2.语法细节:
1)切入点表达式的语法格式:execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))
2)举例说明:
表达式 |
execution(* com.atguigu.spring.ArithmeticCalculator.*(..)) |
含义 |
ArithmeticCalculator接口中声明的所有方法。 第一个“*”代表任意修饰符及任意返回值。 第二个“*”代表任意方法。 “..”匹配任意数量、任意类型的参数。 若目标类、接口与该切面类在同一个包中可以省略包名。 |
表达式 |
execution(public * ArithmeticCalculator.*(..)) |
含义 |
ArithmeticCalculator接口的所有公有方法 |
表达式 |
execution(public double ArithmeticCalculator.*(..)) |
含义 |
ArithmeticCalculator接口中返回double类型数值的方法 |
表达式 |
execution(public double ArithmeticCalculator.*(double, ..)) |
含义 |
第一个参数为double类型的方法。 “..” 匹配任意数量、任意类型的参数。 |
表达式 |
execution(public double ArithmeticCalculator.*(double, double)) |
含义 |
参数类型为double,double类型的方法 |
3.重用切入点:
1)
-
-
-
在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现
- 在Aspect切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。
- 切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
-
-
2)示例代码:
1 @Component 2 //@Aspect表明当前类是一个切面类 3 @Aspect 4 //@Order表示切面执行顺序,value值越小优先级越高 5 @Order(value=50) 6 public class CaculatorAspect { 7 //重用切入点 8 @Pointcut("execution(* * .*(..))") 9 private void LoggingOperation(){ 10 11 } 12 @Before(value = "LoggingOperation()") 13 public void showBeginLog(JoinPoint point){ 14 //获得参数列表: 15 Object[] args = point.getArgs(); 16 List<Object> asList = Arrays.asList(args); 17 //获得方法名: 18 Signature signature = point.getSignature(); 19 String name = signature.getName(); 20 System.out.println("【日志】【前置通知】 目标方法名为:"+name+"参数为:"+asList); 21 } 22 @AfterThrowing(value = "LoggingOperation()" ,throwing="ex") 23 public void showThrowing(JoinPoint point,Exception ex){ 24 System.out.println("【日志】【异常通知】 异常信息:"+ex); 25 } 26 @After(value = "LoggingOperation()") 27 public void showAfter(){ 28 System.out.println("【日志】【后置通知】"); 29 } 30 @AfterReturning(value = "LoggingOperation()",returning="result") 31 public void showAfterReturning(JoinPoint point,Object result){ 32 System.out.println("【日志】【返回通知】 目标方法的返回值为"+result); 33 } 34 }