1. 程式人生 > 實用技巧 >Spring-04-代理模式和AOP

Spring-04-代理模式和AOP

八、AOP和代理模式

8.1 AOP介紹

  • 什麼是AOP?關鍵詞:解耦

    • 可以通過預編譯方式和執行期動態代理實現在不修改原始碼的情況下給程式動態統一新增功能的一種技術
    • 利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。
    • AOP可以看做是OOP的衍生
  • 詳細闡述

    • 如圖所示,一個常見的Web業務執行流程中

      • 一個WEB業務,被分成了DAO層、Service層等。
      • 這麼做的好處就是明確了每一層的職責, 對於這一層的來說,我只需要做好關於我自己的事情就好了,然後把物件交給下一層。至於下一層會幹什麼和我沒有關係。(高內聚,低耦合)
    • web業務的分層開發,可以看做是將一個業務流程,用刀切開,每一層之間就是一個切面。程式設計中,物件與物件之間,方法與方法之間,模組與模組之間也都是是一個個切面。

    • 假設我們在某一層中,需要做這麼一些處理:記錄日誌,安全性檢驗,許可權驗證等。那麼對於這一層的的業務來說,也相當於被分成了這麼幾層

      • 從這個流程圖中可以看到,真正的業務處理中,執行業務的功能程式碼只佔了一小部分,取餘的都是一些附加功能程式碼。當這個這個業務多次重複執行的時候,這部分附加功能可能完全都是一樣的,沒有必要在重複寫一遍程式碼。

      • 我們可以將這些附加的功能全部抽取出來,讓這個業務只專注於自己的業務處理。

      • 就像下圖展示的一樣,我們將附加功能抽取出來,通過Spring將這些附加功能在注入到需要呼叫的地方,這樣,業務就可以專心的處理自己的業務了,不需要關注其他功能的處理。這樣,就可以算作一次簡單的AOP

      • 上面操作的流程,就叫做AOP。 附加功能的抽取就是切面,呼叫附加功能的地方就是切入點

8.2 代理模式

  • 提到了AOP,就不得不提代理模式。這是因為Spring中AOP的底層實現就是基於代理模式實現的。所以掌握好代理模式的設計思想,是掌握AOP的基礎

  • 那麼什麼是代理模式?

    • 代理模式,也叫委託模式,其定義是給某一個物件提供一個代理物件,並由代理物件控制對原物件的引用。它包含了三個角色:
      • Subject:抽象主題角色。可以是抽象類也可以是介面,是一個最普通的業務型別定義。
      • RealSubject:具體主題角色,也就是被代理的物件,是業務邏輯的具體執行者。
      • Proxy:代理主題角色。負責讀具體主題角色的引用,通過真實角色的業務邏輯方法來實現抽象方法,並在前後可以附加自己的操作。
    • 現實生活中的演員就是一個很好的例子
      • 一般來說,演員主要的工作就是演戲,其他的事情就交給了經紀人去做。讓經紀人來負責安排演員演什麼戲,拿多少錢,安排檔期等等除了演戲之外的瑣事
      • 對於這個例子。演員的主要業務就是演戲,那麼演戲就是Subject;處理各種大事小事的經紀人就是Proxy;負責演戲的演員就是RealSubject
  • 代理模式的優缺點

    • 優點:分工明確,低耦合,擴充套件性強
    • 缺點:加入代理會導致效能的降低,實現代理需要額外的工作
  • 代理模式的應用場景

    • 1、遠端代理。 2、虛擬代理。 3、Copy-on-Write 代理。 4、保護(Protect or Access)代理。 5、Cache代理。 6、防火牆(Firewall)代理。 7、同步化(Synchronization)代理。 8、智慧引用(Smart Reference)代理。
  • 如何實現代理模式?

    • 增加一箇中間層,實現與被代理類的組合

8.3 靜態代理

  • 定義一個最普通的業務型別,Subject

    public interface Actor {
        public void act();
    }
    
  • 定義一個實現了業務的具體執行者,也就是RealSubject

    public class Star implements Actor {
    
        private String name;
    
        public Star(String name) {
            this.name = name;
        }
    
        @Override
        public void act() {
            System.out.println(name + "來演戲");
        }
    }
    
  • 定義代理類,也就是Proxy,代理類負責其他和演戲無關的事情的處理

    public class Agent implements Actor {
    
        private Star star;
    
        public void setStar(Star star) {
            this.star = star;
        }
    
        private void arrange() {
            System.out.println("安排檔期");
        }
    
        private void signContact() {
            System.out.println("簽約");
        }
    
        private void paid() {
            System.out.println("獲取片酬");
        }
    
        @Override
        public void act() {
            arrange();
            signContact();
            star.act();
            paid();
        }
    }
    
  • 執行測試

    @Test
    public void test01() {
        Agent agent = new Agent();
        agent.setStar(new Star("劉德華"));
        agent.act();
    }
    

  • 小結:

    • 代理類Agent中的一些處理方法通常會設定成對外不可見的。

    • 當外界需要找演員來演戲的時候,就只需要把演員丟給Agent,然後Agent會來安排其他的事情。

    • 雖然看上去是呼叫了Agent的act方法,但是最後關於act的實現還是是用了Star類中的act的實現。

    • 所以對於劇組(使用者)看不出來是Agent,所以說Agent是Star的代理類

8.4 動態代理

  • 既然有靜態代理,那麼就勢必會有動態代理

  • 為什麼會出現動態代理?

    • 對於靜態代理來說,代理的是一個RealSubject。我們繼續沿用靜態代理的情景。
      • 我們先橫向擴充套件業務。以前只有Star去演戲,這個時候我們有了Dancer可以去跳舞,有了Joker去表演喜劇,等一系列的擴充套件。
      • 這個時候我們可以看出,由於我們的Agent代理的角色是在程式碼中寫死的Star,那麼我們想要代理Dancer和Joker就勢必要重新編寫他們的代理類,
      • 這意味著什麼?這意味著每有一個新的實現類被建立,靜態代理類就要相應的增加一個。這樣就大大增加了工作量
    • 那麼能不能將代理類設計成代理的是一個Interface ,這樣對於相同介面的不同實現類,就不要重新在編寫一個代理類了。
    • 在這樣的需求下,動態代理出現了
  • 動態代理和靜態代理的區別

  • 靜態代理,代理的是一個實現了具體業務的類;而動態代理,代理的是一個介面,所有實現了這個介面的類都可以丟給動態代理

  • 動態代理分為兩類 : 一類是基於介面動態代理 , 一類是基於類的動態代理

    • 基於介面的動態代理----JDK動態代理

    • 基於類的動態代理--cglib

    • 現在用的比較多的是 javasist 來生成動態代理

實現基於介面的動態代理類

  • 必要的知識

    • 介面 InvocationHandler 是由代理例項的呼叫處理程式實現的介面。

      • 每個代理例項都有一個關聯的呼叫處理程式。在代理例項上呼叫方法時,對該方法呼叫進行編碼,並將其分派到其呼叫處理程式的invoke方法。
    • Proxy 提供靜態方法來建立物件,這些物件的行為類似於介面例項,但允許自定義方法呼叫。

      • Proxy類是在執行時建立的類,該類實現指定的介面列表(稱為代理介面)。
      • 代理例項是Proxy類的例項。每個代理例項都有一個關聯的呼叫處理程式物件,該物件實現介面InvocationHandler。呼叫處理程式將適當地處理編碼的方法呼叫,並且返回的結果將作為代理例項上方法呼叫的結果返回。
  • 定義一個普通的業務,這裡沿用靜態代理的Subject

    public interface Actor {
        public void act();
    }
    
  • 實現這個業務的具體實現類,Star,Dancer,Joker

    public class Star implements Actor {
    
        private String name;
    
        public Star(String name) {
            this.name = name;
        }
    
        @Override
        public void act() {
            System.out.println(name + "來演戲");
        }
    }
    
    public class Dancer implements Actor {
        private String name;
    
        public Dancer(String name) {
            this.name = name;
        }
    
        @Override
        public void act() {
            System.out.println(name + "跳舞");
        }
    }	
    
    public class Joker implements Actor {
    
        private String name;
    
        public Joker(String name) {
            this.name = name;
        }
    
        @Override
        public void act() {
            System.out.println(name + "表演喜劇");
        }
    }
    
  • 實現動態代理

    package com.pbx.dynamicProxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    
    /**
     * @author BruceXu
     * @date 2020/11/22
     */
    public class DynamicAgent implements InvocationHandler {
    
        private Actor actor;
    
        public void setActor(Actor actor) {
            this.actor = actor;
        }
    
        public Actor getActor() {
            return (Actor) Proxy.newProxyInstance(this.getClass().getClassLoader(), actor.getClass().getInterfaces(), this);
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            arrange();
            signContact();
            Object res = method.invoke(actor, args);
            paid();
            return res;
        }
    
        private void arrange() {
            System.out.println("安排檔期");
        }
    
        private void signContact() {
            System.out.println("簽約");
        }
    
        private void paid() {
            System.out.println("獲取片酬");
        }
    }
    
    • 對於這個動態代理類,我們並沒有想建立靜態代理一樣去建立一個動態代理的類。我們實際編寫的是一個動態處理器,真正的代理物件,在執行時由JDK通過反射去創建出來
  • 測試

    @Test
    public void test01() {
        DynamicAgent agent = new DynamicAgent();
        agent.setActor(new Dancer("芭蕾舞演員"));
        Actor proxiedActor = agent.getProxy();
        proxiedActor.act();
        System.out.println("===========================");
        agent.setActor(new Star("芭蕾舞演員"));
        proxiedActor = agent.getProxy();
        proxiedActor.act();
        System.out.println("===========================");
        agent.setActor(new Joker("芭蕾舞演員"));
        proxiedActor = agent.getProxy();
        proxiedActor.act();
    }
    

8.5 AOP和代理模式之間的關係

  • AOP和代理模式都是為了解決同一個問題

    • 在不破壞原始程式碼的情況下,對已有程式碼進擴充套件,增加新功能
    • 或者對原有程式碼進行解耦,降低業務處理時各部分的關聯度
  • AOP的底層通過代理模式實現,瞭解代理模式的工作方式,對於學習AOP很有幫助

九、AOP

9.1 AOP概述

  • 什麼是AOP?關鍵詞:解耦

    • 可以通過預編譯方式和執行期動態代理實現在不修改原始碼的情況下給程式動態統一新增功能的一種技術
    • 利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。
    • AOP可以看做是OOP的衍生
  • 詳細闡述

    • 如圖所示,一個常見的Web業務執行流程中

      • 一個WEB業務,被分成了DAO層、Service層等。
      • 這麼做的好處就是明確了每一層的職責, 對於這一層的來說,我只需要做好關於我自己的事情就好了,然後把物件交給下一層。至於下一層會幹什麼和我沒有關係。(高內聚,低耦合)
    • web業務的分層開發,可以看做是將一個業務流程,用刀切開,每一層之間就是一個切面。程式設計中,物件與物件之間,方法與方法之間,模組與模組之間也都是是一個個切面。

    • 假設我們在某一層中,需要做這麼一些處理:記錄日誌,安全性檢驗,許可權驗證等。那麼對於這一層的的業務來說,也相當於被分成了這麼幾層

      • 從這個流程圖中可以看到,真正的業務處理中,執行業務的功能程式碼只佔了一小部分,取餘的都是一些附加功能程式碼。當這個這個業務多次重複執行的時候,這部分附加功能可能完全都是一樣的,沒有必要在重複寫一遍程式碼。

      • 我們可以將這些附加的功能全部抽取出來,讓這個業務只專注於自己的業務處理。

      • 就像下圖展示的一樣,我們將附加功能抽取出來,通過Spring將這些附加功能在注入到需要呼叫的地方,這樣,業務就可以專心的處理自己的業務了,不需要關注其他功能的處理。這樣,就可以算作一次簡單的AOP

      • 上面操作的流程,就叫做AOP。 附加功能的抽取就是切面,呼叫附加功能的地方就是切入點

9.2 Spring中的AOP

  • Spring提供了宣告式事務,同時支援使用者自定義切面

  • Spring中AOP的一些概念,(瞭解就好了,開發的時候用不到)

    • 橫切關注點:跨越應用程式多個模組的方法或功能。即是,與我們業務邏輯無關的,但是我們需要關注的部分,就是橫切關注點。如日誌 , 安全 , 快取 , 事務等等 ....
    • 切面(ASPECT):橫切關注點 被模組化 的特殊物件。即,它是一個類。
    • 通知(Advice):切面必須要完成的工作。即,它是類中的一個方法。
    • 目標(Target):被通知物件。
    • 代理(Proxy):向目標物件應用通知之後建立的物件。
    • 切入點(PointCut):切面通知 執行的 “地點”的定義。
    • 連線點(JointPoint):與切入點匹配的執行點。
  • 下面這張圖很好的說明了各種概念到底是什麼玩意兒

  • SpringAOP中支援的5中Advice型別

9.3 在Spring中實現AOP

  • 在Spring中,有3種不同實現AOP的方式
    • 利用Spring中自帶的Advice API去實現advice
    • 自定義切面類,在切面類中實現各種advice
    • 使用註解開發

方式一:使用Spring API

  • 定義我們的業務介面和我們的業務實現類

    • 業務介面

      public interface UserService {
          void add();
          void delete();
          void update();
          void select();
      }
      
    • 業務實現類

      public class UserServiceImpl implements UserService {
          @Override
          public void add() {
              System.out.println("UserServiceImpl::add()");
          }
      
          @Override
          public void delete() {
              System.out.println("UserServiceImpl::delete()");
          }
      
          @Override
          public void update() {
              System.out.println("UserServiceImpl::update()");
          }
      
          @Override
          public void select() {
              System.out.println("UserServiceImpl::select()");
          }
      }
      
  • 根據需求實現advice

    • 實現一個前置通知

      package com.pbx.advice;
      
      import org.springframework.aop.MethodBeforeAdvice;
      
      import java.lang.reflect.Method;
      
      /**
       * @author BruceXu
       * @date 2020/11/24
       */
      public class beforeLog implements MethodBeforeAdvice {
          @Override
          public void before(Method method, Object[] objects, Object o) throws Throwable {
              System.out.println("在" + o.getClass().getName() + "::" + method.getName() + "執行前");
          }
      }
      
      
    • 實現一個後置通知

      package com.pbx.advice;
      
      import org.springframework.aop.AfterReturningAdvice;
      
      import java.lang.reflect.Method;
      
      /**
       * @author BruceXu
       * @date 2020/11/24
       */
      public class afterLog implements AfterReturningAdvice {
      
          @Override
          public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
              System.out.println("在" + target.getClass().getName() + "::" + method.getName() + "執行後,結果為:" + returnValue);
          }
      }
      
      
  • 在Spring配置檔案中將advice和aspect相關聯

    <!--記得提前匯入aop的約束-->
    
    <!--定義bean-->
    <bean id="userService" class="com.pbx.service.UserServiceImpl"/>
    <bean id="beforeAdvice" class="com.pbx.advice.beforeLog"/>
    <bean id="afterAdvice" class="com.pbx.advice.afterLog"/>
    
    <!--配置aop-->
    <aop:config>
        <aop:pointcut id="point1" expression="execution(* com.pbx.service.UserServiceImpl.*(..))"/>
        <aop:advisor advice-ref="beforeAdvice" pointcut-ref="point1"/>
        <aop:advisor advice-ref="afterAdvice" pointcut-ref="point1"/>
    </aop:config>
    
  • 測試

    @Test
    public void test01() {
        ApplicationContext context = new ClassPathXmlApplicationContext("aop01.xml");
        UserService userService = context.getBean("userService", UserService.class);
        userService.add();
    }
    

方式二:自定義切面類

  • 沿用之前的業務介面和實現類

  • 自定義切面類

    public class MyAspect {
    
        public void before() {
            System.out.println("---- before advice ----");
        }
    
        public void after() {
            System.out.println("---- after advice ----");
        }
    
    }
    
  • 配置AOP

    <!--定義bean-->
    <bean id="MyAspect" class="com.pbx.aspect.MyAspect"/>
    <bean id="userService" class="com.pbx.service.UserServiceImpl"/>
    
    <!--配置AOP-->
    <aop:config>
        <aop:aspect ref="MyAspect">
            <aop:pointcut id="point1" expression="execution(* com.pbx.service.UserServiceImpl.*(..))"/>
            <aop:before method="before" pointcut-ref="point1"/>
            <aop:after method="after" pointcut-ref="point1"/>
        </aop:aspect>
    </aop:config>
    
  • 測試

    @Test
    public void test02() {
        ApplicationContext context = new ClassPathXmlApplicationContext("aop02.xml");
        UserService userService = context.getBean("userService", UserService.class);
        userService.add();
    }
    

方式三:註解實現aop

  • 實現一個使用註解的增強類

    package com.pbx.aspect;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    
    /**
     * @author BruceXu
     * @date 2020/11/24
     */
    
    @Aspect
    public class MyAspect2 {
    
        @Before("execution(* com.pbx.service.UserServiceImpl.*(..))")
        public void before() {
            System.out.println("---- before advice ----");
        }
    
        @After("execution(* com.pbx.service.UserServiceImpl.*(..))")
        public void after() {
            System.out.println("---- after advice ----");
        }
    
        @Around("execution(* com.pbx.service.UserServiceImpl.*(..))")
        public Object surround(ProceedingJoinPoint point) throws Throwable {
            System.out.println("---- before around ----");
    
            System.out.println("方法簽名為:"+point.getSignature());
            Object proceed = point.proceed();
            System.out.println("---- after around ----");
            return proceed;
        }
    
    }
    
  • 配置Spring

    <!--註冊bean-->
    <bean id="userService3" class="com.pbx.service.UserServiceImpl"/>
    <bean id="Aspect2" class="com.pbx.aspect.MyAspect2"/>
    <!--自動代理-->
    <aop:aspectj-autoproxy/>
    
  • 測試

    @Test
    public void test03() {
        ApplicationContext context = new ClassPathXmlApplicationContext("aop03.xml");
        UserService userService = context.getBean("userService3", UserService.class);
        userService.add();
    }