面試問到AOP就該這樣回答

前言

  相信各位小夥伴在準備面試的時候,AOP都是沒法繞過的一個點,常常能看到動態代理、JDK動態代理、CGLIB動態代理這樣的字眼。其實動態代理是代理模式的一種。代理模式有靜態代理、強制代理、動態代理。因此在認識AOP以前須要瞭解代理模式。java

代理模式定義

  代理模式(Proxy Pattern):爲其餘對象提供一種代理以控制這個對象的訪問。面試

  • Subject抽象主題角色,也叫作抽象主題類。能夠是抽象類也能夠是接口,是一個最普通的業務類型定義,無特殊要求。
  • RealSubject具體主題角色,也叫作被委託角色,被代理角色,是業務邏輯的具體執行者。
  • Proxy代理主題角色,也叫作委託類、代理類。他負責對真實角色的應用,把全部抽象主題類定義的方法限制委託給真實主題角色實現,而且在真實主題角色處理完畢先後作到預處理和藹後工做。

代理模式的優勢

  • 職責清晰
  • 高擴展性
  • 智能化

UML

image

個人理解

  跳板機不論是對於運維老哥仍是對於咱們來說,都是平常的工做中不可或缺的一個工具。爲了保證生產服務器的安全,咱們是沒法經過xshell等工具直接進行鏈接的。若是須要操做生產的服務器,則須要經過跳板機。而且跳板機還能記錄咱們的操做,用來作安全審計。防止出現,某位老哥一氣之下反手就是個sudo rm -rf /*直接涼涼。shell

  咱們回過頭看看代理模式的定義:爲其餘對象提供一種代理以控制這個對象的訪問。實際上跳板機就是生產服務器的一個代理Proxy,爲了實現控制生產服務器的訪問權限。你須要經過跳板機來操做生產服務器。數據庫

  • 爲其餘對象提供一種代理以控制這個對象的訪問
  • 給你提供一個跳板機來訪問生產服務器,目的是來控制生產服務器的訪問

  Proxy的職責:Proxy是對真實角色的應用,把全部抽象主題類定義的方法限制委託給真實主題角色實現。而且在真實主題角色處理完畢先後作到預處理善後工做。你經過操做跳板機。跳板機將你輸入的命令在生產服務器上進行執行。而且能記錄下執行的命令和執行的結果。編程

代碼演示

Server設計模式

public interface Server {

    /**
     * 執行
     * @param command 命令
     */
    void exec(String command);

}

ProdServer安全

public class ProdServer implements Server {

    @Override
    public void exec(String command){
        System.out.println(command + ":執行成功");
    }

}

JumpServer服務器

public class JumpServer {

    private Server server;

    public JumpServer(Server server) {
        this.server = server;
    }

    public void exec(String command){
        System.out.println("xxx在:" + LocalDateTime.now() + " 執行了:" + command);
        server.exec(command);
        System.out.println("xxx在:" + LocalDateTime.now() + " 執行完了:"+ command + " 結果是XXX");
    }

}

Client網絡

public class Client {

    public static void main(String[] args) {
        JumpServer jumpServer = new JumpServer();
        jumpServer.exec("pwd");
    }
}

運行結果框架

xxx在:2020-04-04T16:43:19.277 執行了:pwd
pwd:執行成功
xxx在:2020-04-04T16:43:19.278 執行完了:pwd 結果是XXX

  經過上面的代碼,簡單的實現了代理模式。在網絡上代理服務器設置分爲透明代理普通代理

  • 透明代理就是用戶不用設置代理服務器地址,就能夠直接訪問.也就是說代理服務器對用戶來講是透明的,不用知道它存在的。
  • 普通代理則是須要用戶本身設置代理服務器的IP地址,用戶必須知道代理的存在。

  當運維老哥給了一臺服務器的帳號和密碼,你成功登陸,並完成了相應的操做。你覺得給你的是生產的服務器。實際就多是個跳板機。爲了安全起見,是不可能將實際服務器的IP讓你知道的。很顯然跳板機對你來說就是透明的。

因此咱們調整一下代碼,將其變成普通代理

JumpServer

public class JumpServer {

    private Server server;

    public JumpServer(Server server) {
        this.server = server;
    }

    public void exec(String command){
        server.exec(command);
    }

}

Client

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();
        JumpServer jumpServer = new JumpServer(prodServer);
        jumpServer.exec("pwd");
    }
    
}

執行結果

xxx在:2020-04-04T16:52:23.282 執行了:pwd
pwd:執行成功
xxx在:2020-04-04T16:52:23.283 執行完了:pwd 結果是XXX

強制代理

  對於現實狀況,咱們能夠經過不開放公網訪問的權限來實現,強制使用代理操做服務器。咱們能夠用代碼簡單的模擬下。

JumpServer

public class ProdServer implements Server {

    private JumpServer jumpServer;

    @Override
    public void exec(String command){
        hasProxy();
        System.out.println(command + ":執行成功");
    }

    private void hasProxy(){
        if(jumpServer == null){
            throw new RuntimeException("請使用跳板機!");
        }
    }

    public JumpServer setJumpServer() {
        this.jumpServer = new JumpServer(this);
        return this.jumpServer;
    }
}

Client未設置跳板機

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();
        prodServer.exec("pwd");
    }

}

不設置跳板機運行結果

Exception in thread "main" java.lang.RuntimeException: 請使用跳板機!
    at proxy.pattern.tmp.ProdServer.hasProxy(ProdServer.java:21)
    at proxy.pattern.tmp.ProdServer.exec(ProdServer.java:15)
    at proxy.pattern.tmp.Client.main(Client.java:13)

Client設置跳板機

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();
        prodServer.setJumpServer().exec("pwd");
    }

}

運行結果

xxx在:2020-04-05T15:01:10.944 執行了:pwd
pwd:執行成功
xxx在:2020-04-05T15:01:10.944 執行完了:pwd 結果是XXX

這個時候須要訪問生產服務器,就須要先設置跳板機了,才能進行操做。

動態代理

  對靜態代理來講,咱們須要手動生成代理類。可是若是須要代理的類太多了,那這個確定是不可取的。因此咱們可使用JDK動態代理來幫咱們完成工做。

JDK動態代理

  JDK動態代理利用攔截器(攔截器必須實現InvocationHanlder)加上反射機制生成一個實現代理接口的匿名類,在調用具體方法前調用InvokeHandler來處理。

JumpServerInvocationHandler

public class JumpServerInvocationHandler implements InvocationHandler {

    private Object target;

    public JumpServerInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target,args);
    }
}

Client

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();

        JumpServerInvocationHandler handler = new JumpServerInvocationHandler(prodServer);
        ClassLoader classLoader = prodServer.getClass().getClassLoader();
        Server proxy = (Server) Proxy.newProxyInstance(classLoader, new Class[]{Server.class}, handler);

        proxy.exec("pwd");
    }

}

測試結果

pwd:執行成功

增長前置通知和後置通知

Advice

public interface Advice {

    void exec();

}

BeforeAdvice

public class BeforeAdvice implements Advice {
    @Override
    public void exec() {
        System.out.println("執行前置通知");
    }
}

AfterAdvice

public class AfterAdvice implements Advice {
    @Override
    public void exec() {
        System.out.println("執行後置通知");
    }
}

JumpServerInvocationHandler

public class JumpServerInvocationHandler implements InvocationHandler {

    private Object target;

    public JumpServerInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 執行前置通知
        new BeforeAdvice().exec();

        Object ret = method.invoke(target, args);

        // 執行後置通知
        new AfterAdvice().exec();
        return ret;

    }
}

Client

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();

        JumpServerInvocationHandler handler = new JumpServerInvocationHandler(prodServer);
        ClassLoader classLoader = prodServer.getClass().getClassLoader();
        Server proxy = (Server) Proxy.newProxyInstance(classLoader, new Class[]{Server.class}, handler);

        proxy.exec("pwd");
    }

}

測試結果

執行前置通知
pwd:執行成功
執行後置通知
看到這裏有沒有點AOP的感受了。

CGLIB動態代理

  CGLIB動態代理利用ASM開源包,對代理對象類的class文件加載進來,經過修改其字節碼生成子類來處理,須要使用MethodInterceptor接口來進行實現。

JumpServerMethodInterceptor

public class JumpServerMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        new BeforeAdvice().exec();

        Object ret = methodProxy.invokeSuper(obj, args);

        new AfterAdvice().exec();

        return ret;
    }
}
注意的點:是使用 invokeSuper()而不是 invoke()

Client

public class Client {

    public static void main(String[] args) {

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ProdServer.class);
        enhancer.setCallback(new JumpServerMethodInterceptor());

        ProdServer proxy = (ProdServer) enhancer.create();

        proxy.exec("pwd");
    }

}

測試結果

執行前置通知
pwd:執行成功
執行後置通知

小結

  經過上面的一大堆的篇幅介紹代理模式就是爲了能更加清晰的理解代理模式很是重要的一個應用場景AOP。

AOP

  AOP(Aspect Oriented Programming)意爲:面向切面編程,經過預編譯方式和運行期間動態代理實現程序功能的統一維護的一種技術。

AOP相關術語

鏈接點(Joinpoint)
  一個類或一段程序代碼擁有一些具備便捷性質的特定點。好比說類的某個方法調用前/調用後、方法拋出異常後。

切點(Pointcut)
  在AOP中用於定位鏈接點。若是將鏈接點做爲數據庫中的記錄,切點即至關於查詢條件。切點和鏈接點不是一對一的關係,一個切點能夠匹配多個鏈接點。

加強(Advice)
  加強是織入目標類鏈接點上的一段程序代碼。

在Spring中加強除了用於描述一段程序代碼外,還能夠擁有另外一個和鏈接點相關的信息,這即是執行點的方位。經過執行點方位信息和切點信息,就能夠找到特定的鏈接。正是由於加強即包含添加到鏈接點上的邏輯,包含定位鏈接點的方位信息,因此Spring提供的加強接口都是帶方位名的。如 BeforeAdviceAfterAdviceAroundAdvice

目標對象(Target)
  須要織入加強邏輯的目標類。好比說在使用AOP的時候配置的請求日誌輸出,目標對象就是對應的controller.

引介(Introduction)
  引介是一種特殊的加強,它爲類添加一些屬性和方法。這樣,即便一個業務類沒有本來沒有實現某個接口,經過AOP能夠動態的爲某些業務類添加接口和實現方法,讓業務類成爲這個接口的實現類。

織入(Weaving)
  織入是將加強添加到目標類的具體鏈接點上的過程。AOP有3種織入方式:

  • 編譯期織入,要求使用特殊的Java編譯器。
  • 類裝載期織入,要求使用特殊的類裝載器。
  • 動態代理織入,在運行期,爲目標類添加加強生成子類的方式。
    毫無疑問Spring是採用動態代理織入。

代理(Proxy)
  一個類被AOP織入加強後,就產生了一個結果類,它是融合了原類和加強邏輯的代理類。

切面(Proxy)
  切面由切點和加強(引介)組成,它即包括很切邏輯的定義,也包括鏈接點的定義。SpringAOP就是負責實施切面的框架,他將切面所定義的橫切邏輯織入切面所指定的鏈接點中。

咱們能夠這樣回答

  AOP翻譯過來是:面向切面編程是一種設計思想。主要由鏈接點,切點,加強、切面組成。AOP依託於代理模式進行實現,因此AOP擁有代理模式的特性。能夠在不改變原有類的狀況下,能動態的添加某些功能。因此說比較適合來實現,打印請求日誌,權限校驗,等功能。針對不一樣的場景,AOP能夠選擇使用JDK動態代理或CGLIB代理來實現。因爲CGLIB建立出來的代理類運行速度快於JDK動態代理,可是建立的過程太慢,因此能夠將其建立出來的代理類交由IOC容器進行管理,免去了重複建立代理沒必要要的性能開銷,來提升運行速度。

主要針對,AOP是什麼、由什麼組成、適合用場景、如何實現,不一樣實現的區別這些點去總結回答。

切點和切面的區別?

  切面包含切點,切點和加強組成了切面。SpringAOP經過切面將邏輯特定織入切面所指定的鏈接點中。

CGLIB 和 JDK 動態代理的區別

  jdk動態代理只能對實現了接口的類生成代理,而不能針對類。cglib是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法。簡而言之就是JDK動態代理基於接口實現,cglib基於類繼承。由於是繼承,因此該類或方法不能使用final進行修飾。

  在性能上,有研究代表cglib所建立的代理對象的性能要比jdk建立的高10倍,可是呢cglib建立代理對象時所花費的時間要比jdk8倍。因此單例的代理對象或者具備實例池的代理,無效頻繁的建立對象,比較適合採用cglib,反正適合採用jdk

參考書籍

  • 設計模式之禪道第二版
  • 精通Spring 4.x企業應用開發實戰

結尾

  若是以爲對你有幫助,能夠多多評論,多多點贊哦,也能夠到個人主頁看看,說不定有你喜歡的文章,也能夠隨手點個關注哦,謝謝。

相關文章
相關標籤/搜索