你的位置:首页 > Java教程

[Java教程]AOP 面向切面的编程

  一、面向切面的编程需求的产生

    1. 代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。
    2. 代码分散: 以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块。

  二、实现面向切面的编程

    1. 将需要实现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  @Override16  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  @Override23  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  @Override30  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 }
      要实现AOP的计算方法类

       

    2. 实现切面类
      1. 使用前置通知、后置通知、返回通知、异常通知实现切面类,注入到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 @Component21 //@Aspect表明当前类是一个切面类22 @Aspect23 //@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  }

         

 

           使用黄色背景标注的代码是声明的通知,其中包括通知类型切入点表达式,以下就是对通知类型的介绍,切入点表达式在最后

            @Before  前置通知:在方法执行之前执行的通知

            @After 后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候

            @AfterReturning   返回通知:

          • 无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
          • 在返回通知中访问连接点的返回值:
            • 在返回通知中,只要将returning属性添加到@AfterReturning注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称
            • 必须在通知方法的签名中添加一个同名参数。在运行时Spring AOP会通过这个参数传递返回值
            • 原始的切点表达式需要出现在pointcut属性中 

            @AfterThrowing 异常通知:只在连接点抛出异常时才执行异常通知.:

        • 将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   @Component14   //@Aspect表明当前类是一个切面类15   @Aspect16   @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.ClassPathimport com.neuedu.aop.MathCaculator;import com.neuedu.aop.RawCaculator;public class TestCaculator { @Test public void test() {  ApplicationContext ioc = new ClassPath);  //MathCaculator bean = ioc.getBean(RawCaculator.class); 不可用 (代表类 类型不一样)  //rawCaculator即使使用注解时代表类的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    }