轉載AOP

jim2007325040發表於2018-04-30

Spring之AOP篇:

AOP框架是Spring的一個重要組成部分.但是Spring IOC 並不依賴於AOP,這就意味著你有權力選擇是否使用AOP,AOP作為Spring IOC容器的一個補充,使它成為一個強大的中介軟體解決方案。

一、AOP(Aspect-Oriented Programming)

    AOP的目標:橫切關注點  這種問題用物件導向很難解決

  AOP是用來解決什麼問題的呢?
     橫越多個模組的業務關注點,比如記錄日誌,事務管理
 1、AOP的解決方案——攔截
   1)由誰來攔截?--代理類Proxy 和本類實現同一個介面
          就像是這樣:action發出命令--代理類(處理一些操作)--本類
   2)攔截的過程:
       I.將Proxy(如UserServiceProxy)交給呼叫者(如UserAction),呼叫者呼叫需要的方法
       II.Proxy先攔截該方法,執行攔截邏輯
       III.然後執行呼叫者真正的實現類(如UserServiceImp)
   3)核心概念:
     a、代理物件Proxy:是核心,負責攔截,由工具自動建立
     b、攔截器(Interceptor):實現攔截邏輯的物件
     c、目標物件(Target):是Proxy所代理的物件,是已有的類
  以上三個類實現了一個“金三角”
     Proxy--攔截-->Interceptor--實現攔截邏輯--處理目標物件-->Target
 2、代理如何實現?
      有兩種方式:
        1)JDK動態代理(使用這個比較多)
            由JDK自帶的動態程式碼生成技術,可以對實現了介面的類進行處理
        2)CGLib
     對沒有實現任何介面的類進行處理
 這兩種方式的共同點:(都是在後臺自動生成的)
  在程式執行期間,動態的生成程式碼並進行動態編譯和載入

     
    問題:Spring的AOP是如何實現代理的?是自己實現的嗎?
        注:Spring借用了JDK動態代理和CGLib來實現代理物件。
     Spring進行如下判斷來實現代理:
  i、如果目標類實現了介面,Spring則呼叫JDK動態代理;
  ii、反之,呼叫CGLib
     
      Proxy是核心:
      建立Proxy的方法:ProxyFactoryBean
 <bean id="arithProxy"
         class="org.springframework.aop.framework.ProxyFactoryBean">
       <property name="target" ref="arith"/>
       <property name="interceptorNames">
       <list>
        <!--<value>logBefore</value>
           <value>logAfter</value>
           <value>logThrows</value> -->
       <value>logAround</value>
      </list>
     </property>

      JDK動態代理的實現方式:(工廠模式)
  UserDao userDao = new UserDaoImp();
  UserDao proxy = (UserDao)
  Proxy.newProxyInstance(userDao.getClass().getClassLoader(),
    userDao.getClass().getInterfaces(),
    new LogHandler(userDao));

二、Spring中的AOP方式(在Spring中將攔截稱為通知)
   攔截邏輯放在Advice之中,Advice按時間將攔截器分為四類:
      1)Before Advice:方法執行之前進行攔截
    public class LogBeforeAdvice implements MethodBeforeAdvice{

 //Method method攔截的方法
 //Object[] args 方法中的引數
 //target 目標物件
 @Override
 public void before
 (Method method, Object[] args, Object target)
   throws Throwable {
  MyLogger logger = new MyLogger();
  System.out.println("Methods:"+method.getName()+
    "  begins, args:"+Arrays.toString(args));
 }
}
    2)After Advice:方法執行之後進行攔截
  public class LogAfterAdvice implements AfterReturningAdvice{
 @Override
 public void afterReturning(Object resultValue, Method method, Object[] args,
   Object target) throws Throwable {
  MyLogger logger = new MyLogger();
  System.out.println("Methods:"+method.getName()+
    "  ends, result:"+resultValue);
 }
}
   3)AfterThrowing Advice:方法異常結束之後進行攔截
  public class LogThrowingAdvice implements ThrowsAdvice {
 
 //ThrowsAdvice 沒有方法,但方法名必須是afterThrowing
 public void afterThrowing(IllegalArgumentException e)
 throws Throwable{
  MyLogger logger = new MyLogger();
  logger.log("IllegalArgumentException!");
 }
}
   4)Around Advice:環繞通知
  public class LogAroundAdvice implements MethodInterceptor {

 //MethodInvocation相當於Method的包裝
 @Override
 public Object invoke(MethodInvocation methodInvocation)
 throws Throwable {
  MyLogger logger = new MyLogger();
  try {
   //methodInvocation.getMethod()獲得方法
   //methodInvocation.getArguments()獲得方法的引數
   logger.log("before:"+
     methodInvocation.getMethod().getName()+
     ", args:"+methodInvocation.getArguments());
   
   //在環繞通知裡面,必須執行目標方法!!!!!!!!!!!!!!
   Object result = methodInvocation.proceed();
   logger.log("ends:"+
     methodInvocation.getMethod().getName());
   return result;
  } catch (Exception e) {
   logger.log("Exception:"+e.getMessage());
   throw e;
  }
 }

  注意:雖然環繞通知包含了另外三種,但還是要依據業務邏輯來選擇,這樣有利於程式碼的程式設計量
       並且環繞通知最好不要和另外三種混用,並且並許執行目標方法proceed().

 也可以大致分成兩組——
 i、BeforeAdvice, AfterReturning, Throwing
 ii、AroundAdvice:需要在內部負責呼叫目標類的方法。
      注意:AroundAdvice應單獨使用


<!-- Target Object -->
<bean id="arith" class="myspring.calculator.ArithmeticCalculatorImp"></bean>

<!-- Advice -->
<bean id="logBefore" class="myspring.aop.LogBeforeAdvice"/>
<bean id="logAfter" class="myspring.aop.LogAfterAdvice"/>
<bean id="logThrows" class="myspring.aop.LogThrowingAdvice"/>
<bean id="logAround" class="myspring.aop.LogAroundAdvice"/>

<!-- Proxy -->
<bean id="arithProxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="arith"/>
<property name="interceptorNames">
  <list>
    <!-- <value>logBefore</value>
    <value>logAfter</value>
    <value>logThrows</value> -->
    <value>logAround</value>
  </list>
</property>

</bean>

在test類中這樣用:ApplicationContext ac =
   new ClassPathXmlApplicationContext("aop-base.xml");
  IArithmeticCalculator proxy =
   (IArithmeticCalculator)ac.getBean("arithProxy");

     

 面試中可能會問到:
          proxyTargetClass屬性
  將其加入到代理bean中去,則Spring將呼叫CGLib來實現建立Proxy物件。
                   (無論Target有無實現介面)
      形如
     <bean id="" class="....ProxyFactoryBean">
    .....
       <property name="proxyTargetClass" value="true" />
     </bean>
 三、Spring AOP基本方式  AspectJ Spring2.5之後用的

Spring對AspectJ的支援
      應用於有多個target,不想一個一個的配置時,用AspectJ     
1)POJO類(Aspect類),使用@Aspect 
  基本資訊:Advice和Pointcut  
2)定義Bean:在Bean定義檔案中定義這個Aspect類 
3)啟用Spring的自動代理
  <aop:aspectj-autoproxy />  

   1、JoinPoint(連線點):被攔截的方法
                程式執行過程中的點,就好像方法中的一個呼叫,
  或者一個特別的被丟擲的異常
               在Spring AOP中,一個連線點通常是方法呼叫.
   Spring並不明顯地使用"連線點"作為術語,
  連線點資訊可以通過MathodInvocation的引數穿過攔截器訪問,
  相當於實現org.springframework.aop.Pointcut介面.
 2、JoinPoint物件
  獲取連線點資訊(獲取目標方法以及目標物件的資訊) 
 1)目標方法:
  i、getSignature():方法簽名
    joinPoint.getSignature().getName()
  
  ii、getArgs():方法的實參(方法的實際引數)
                  joinPoint.getArgs()
  列印輸出時:Arrays.toString(joinPoint.getArgs())
 2)目標物件:  getTarget()
    目標物件實際型別:getTarget().getClass()

 3、PointCut(切入點):一組連線點。
                作用:定義了一組被攔截的方法 
  PointCut(切入點):當一個通知被啟用的時候,
  會指定一些連線點.一個AOP框架必須允許開發者指定切入點。
  一個AOP框架必須允許開發者指定切入點:例如使用正規表示式.

  只有引入了切入點,才能很好的進行攔截

  PointCut的意義:可以一次性定義多個類的
    多個方法作為攔截目標(即JoinPoint)
  PointCut代表了Target 一組Target組成了PointCut
  一組Target可以理解為一個類的多個方法,或者多個類的多個方法
  代理、攔截器、目標

  PointCut
   在AspectJ中,切入點是用PointCut Expression來
   定義的。(切入點表示式可以理解為過濾條件)
   
   PointCut表示式
    格式  execution(表示式)
    表示式  一個表示式就是一個匹配規則
     * myspring.calculator.IArithmeticCalculator.add(..)
  *理解為public void myspring.calculator.IArithmeticCalculator.add(int a,int b)
  * 取代public void 通配一切字元   
   Spring自動代理(即Spring自動生成Proxy物件)
    用<aop:aspectj-autoproxy />來啟用

 Pointcut表示式語法
 1)方法級別  —— execution
  execution(* *.*(..)) :所有類的方法
  execution(public * somepackage.SomeClass.someMethod(..))
  總結:execution是對方法簽名進行匹配
 2)物件級別(類級別)—— within
 within(somepackage.*):somepackage包下所有的類
 within(com.dang.service.*Service)
 即 com.dang.service包下所有以Service結尾的類
 within(somepackage..*):somepackage以及所有子包中的類
 within(somepackage.SomeInterface+):SomeInterface介面的所有實現類
 總結:within是對類名(帶包名)進行匹配。 攔截這個類的所有方法 


4、Aspect類的組成  一般都用註解 重點
  1)Advice(AspectJ中的通知型別)
   i、Before
   //@Before("LogPointcuts.logAdd()") 方法1
            @Before("execution(* *.*(..))") 方法2
         public void logBefore(JoinPoint joinPoint){
     logger.log("{before} the method: "
       +joinPoint.getSignature().getName()+
       "  args:"+Arrays.toString(joinPoint.getArgs())); 
 }
             注:方法1和方法2都是可以的,方法1的話需要在定義一個方法.
  在同一個類或不同的類都是可以的 直接呼叫這個方法
                  @Pointcut("execution(* *.*(..))")
          public void logOperation(){}
   ii、AfterReturning
  @AfterReturning(pointcut="execution(* *.*(..))",
   returning="result")
    public void afterReturning(JoinPoint joinPoint,Object result){
   logger.log("{AfterReturning} the method: "
        +joinPoint.getSignature().getName()
        +" result:"+result); 
 }
    注意:方法有兩個引數,在註解中的returning的值必須和Object的值完全一樣
   iii、AfterThrowing
  @AfterThrowing(pointcut="execution(* *.*(..))",
   throwing="e")
    public void afterThrowing(JoinPoint joinPoint,IllegalArgumentException e){
  logger.log("{AfterThrowing} the method: "
       +joinPoint.getSignature().getName()
       +" e:"+e.getMessage());
 }
   iv、After:相當於finally,它在AfterReturning
     和AfterThrowing之後執行。
     它的作用——一般為釋放資源(如關閉Connection)
  @After("execution(* *.*(..))")
 public void after(JoinPoint joinPoint){
  logger.log("{After} method:"+joinPoint.getSignature().getName());
 }

  注意:After是肯定是要在最後執行的,
   v、Around
  @Around("execution(* *.*(..))")
 public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable{
  logger.log("{Arount}Before method:"
    +joinPoint.getSignature().getName());
  try{
                       //一定不要忘記呼叫目標方法
   Object result = joinPoint.proceed();
   
   logger.log("{Arount}After method:"
     +joinPoint.getSignature().getName()
     +" result:"+result);
         //返回目標方法的返回值
   return result;
  }
  catch(IllegalArgumentException e){
   logger.log("{Around}Exception:Illegal Argument"
     +Arrays.toString(joinPoint.getArgs()));
   throw e;
  }
  注意:Around最好不要和其他四種同時使用

  把處理日誌的程式碼放到一起就是切面,模組化(Aspect)

  5.Introduction(引入)
   1)Introduction的定義
  給正在執行的程式中的物件新增方法。
   其實是一種動態技術
   2)問題:如何給ArithmeticCalculatorImp類加入
   求最大值和最小值的方法
   假設:最大值和最小值的方法已經存在,且分別屬於不同的類。
 extends MaxCalculatorImp, MinCalculatorImp 不行,java不允許多繼承
   3)要點:
 a、實現一個POJO類
  定義@DeclareParents來"繼承"的父類
         @DeclareParents(
  value="myspring.calculator.ArithmeticCalculatorImp",
  defaultImpl=MaxCalculatorImp.class
 )
 private MaxCalculator maxCalculator;
 
 @DeclareParents(
   value="myspring.calculator.ArithmeticCalculatorImp",
   defaultImpl=MinCalculatorImp.class
  )
  private MinCalculator minCalculator;   
 b、定義這個Bean:在Bean定義檔案中宣告一下。
         <bean class="myspring.calculator.CalculatorIntroduction"></bean>
    在test類中測試時:ApplicationContext ac =
  new ClassPathXmlApplicationContext("aop.xml");
  
  IArithmeticCalculator cal = (IArithmeticCalculator)
  ac.getBean("arith");
  cal.add(2, 3);
  cal.div(4, 3);
  
  MaxCalculator max = (MaxCalculator)cal;
  max.max(3, 6);
  MinCalculator min = (MinCalculator)cal;
  min.min(3, 6);