深入理解spring(代理模式源碼1)五

(一)AOP 原理解析

衆生周知,AOP實現原理是基於動態代理。

什麼是代理?

增強一個類,或者一個對象的功能。就可以說是代理。
如:買火車票? app 12306 就是一個代理,他代理了火車站。

java實現代理的兩種方式:

代理的名詞:

代理對象 :增強後的對象
目標對象 :被增強的對象
他們的身份不是絕對的,會根據情況發生變化

1.1靜態代理
繼承

代理對象繼承目標對象,重寫需要增強的方法。完成靜態代理!調用即可。。面向接口編程就是靜態代理
繼承方式代理會有一個問題。每次增強都需要繼承。。如果需要多次(如下聚合)增強,會產生很多代理類。相對複雜。
在這裏插入圖片描述

聚合

目標對象,和代理對象實現同一個接口。並且代理對象包含 目標對象;
核心示例代碼:
傳入目標對象,對目標對象增強!切換不同的類,就可以對不同的類完成代理。

//代理對象1
public class UserDaoLog implements UserDao {
    //裝飾者模式,傳入對象
    private UserDao dao;
    public UserDaoLog(UserDao dao){
        this.dao=dao;
    }
    /**
     * 對目標對象做增強處理
     */
    @Override
    public void query() {
        System.out.println("打印日誌。");
        dao.query();
    }
 //代理對象2
public class UserDaoTime implements UserDao {
    //裝飾者模式,傳入對象
    private UserDao dao;
    public UserDaoTime(UserDao dao){
        this.dao=dao;
    }

    /**
     * 對目標對象做增強處理
     */
    @Override
    public void query() {
        System.out.println("打印時間。");
        dao.query();
    }
 }
    //傳入目標對象完成代理,要代理其他類,只需要切換對象。
    如果老闆說,既要完成打印時間,又要完成打印日誌該怎麼辦呢?
public static void main(String[] args) {
        UserDao dao = new UserDaoImpl();
//        UserDao prox = new UserDaoTime(dao);
        UserDao prox = new UserDaoLog(dao);
        prox.query();
    }

思考:如果我們要完成多重代理呢?改怎麼辦?(既要完成打印時間,又要完成打印日誌)

1.1.1 多重代理(多次增強!)

對多個代理對象,代理。。完成雙重代理。(如下代碼片段,只需要傳入 UserDaoLog+UserDaoImpl)
然後再對他們做代理。。繼承的方式 就會產生很多類。聚合實現起來就更加簡單。

    public static void main(String[] args) {
        UserDao traget = new UserDaoLog(new UserDaoImpl());
        UserDao prox = new UserDaoTime(traget);
        prox.query();
    }
    ---------------------控制檯輸出------------------------
    打印時間。
	打印日誌。
	假裝查詢數據庫
假設這個時候,老闆要求先打印日誌,再打印時間呢?
    public static void main(String[] args) {
        UserDao traget = new UserDaoTime(new UserDaoImpl());
        UserDao prox = new UserDaoLog(traget);
        prox.query();
    }
    ---------------------控制檯輸出------------------------
	打印日誌。
	打印時間。
	假裝查詢數據庫
缺點:如上,我們只是對一個類(userDao)做代理,如果對多個類做代理呢?就算用聚合的方式一樣會產生很多類。只不過比繼承少一點,如上每個類做一次雙重代理,就需要兩個代理類。這個問題該怎麼解決呢?
總結:如果在不確定的情況下,儘量不要使用靜態代理。因爲一但寫代碼就會產生類。一但產生類,就會寫到爆炸。那什麼場景會用到靜態代理呢?JDK中的IO流,BufferIO流,其實都是使用的裝飾者模式。就是靜態代理的方式。

1.2 手寫動態代理

面試官:什麼是動態代理?
初中級開發:基於java反射,增強啊,調用者和實現者之間解耦啊。張口就來了
網上博客:1:抽象類接口,2:被代理類(具體實現抽象接口的類),3:動態代理類:實際調用被代理類的方法和屬性的類。InvocationHandler 方法啊等等等,都是隻懂皮毛
思考:先拋開以上的所有想法,如果我們不想創建任何類的情況下要增強一個類,該怎麼辦?

我們是不是得先有一個類?去繼承或者實現目標對象?才能去反射?可是我們連類都沒有,拿什麼去反射?所以反射只是動態代理的冰山一角。那麼現在我們不能在項目中去創建一個類,該怎麼辦?可以把這個類定義在代碼中嗎?不管可不可以我們先來試一下

如下:假設我們要對UserDaoImpl 做代理。如下圖所示,我們已經得到了要代理UserDaoImpl的所有內容

public class UserDaoImpl implements UserDao{
    public void query(){
        System.out.println("假裝查詢數據庫");
    }
    public String query(String aa){
        return aa;
    }
  }

 public static void main(String[] args) {
        //獲取UserDaoImpl實現的接口 我們這是山寨版的動態代理,暫時只處理第一個接口
        Class targetInf = new UserDaoImpl().getClass().getInterfaces()[0];
        //獲取到接口的方法
        Method methods[] = targetInf.getDeclaredMethods();
        //定義換行
        String line = "\n";
        //定義tab空格
        String tab = "\t";
        //獲取到實現的接口名字 這裏就是UserDao
        String infName = targetInf.getSimpleName();
        String content = "";
        //我們要寫一個類的類容 第一行是不是就是外面的包名?這裏就寫的叼一點google
        String packageContent = "package com.google;" + line;
        //第二行 是不是我們要導包?類.getName是不是就得到了類的全路徑 com.luban.dao.UserDao
        String importContent = "import " + targetInf.getName() + ";" + line;
        //創建一個類,類名爲$Proxy 實現了接口UserDao
        String clazzFirstLineContent = "public class $Proxy implements " + infName + "{" + line;
        //定義私有變量 private UserDao target;
        String filedContent = tab + "private " + infName + " target;" + line;
        //定義構造方法 傳入 UserDao target 並且給私有成員變量賦值
        String constructorContent = tab + "public $Proxy (" + infName + " target){" + line
                + tab + tab + "this.target =target;"
                + line + tab + "}" + line;
        String methodContent = "";
        //遍歷接口的所有方法
        for (Method method : methods) {
            //獲取到方法的返回類型
            String returnTypeName = method.getReturnType().getSimpleName();
            //獲取到方法名稱
            String methodName = method.getName();
            // 獲取到方法的入參類型,如Sting.class String.class
            Class argse[] = method.getParameterTypes();
            String argsContent = "";
            String paramsContent = "";
            int flag = 0;
            for (Class arg : argse) {
                //遍歷入參類型數組,獲取到入參的對象名稱 如 String
                String temp = arg.getSimpleName();
                System.out.println(temp);
                //拼裝參數 如 ,String p0,Sting p1,
                argsContent += temp + " p" + flag + ",";
                //拼裝形參(需要調用父類方法用到) 如 , p0, p1,
                paramsContent += "p" + flag + ",";
                flag++;
            }
            //截取掉參數的最後一個逗號
            if (argsContent.length() > 0) {
                argsContent = argsContent.substring(0, argsContent.lastIndexOf(",") - 1);
                paramsContent = paramsContent.substring(0, paramsContent.lastIndexOf(",") - 1);
            }
            //我們有了 返回類型,有了方法名,有了入參,還有了調用父類的方法。就可以拼裝 實現的方法
            methodContent += tab + "public " + returnTypeName + " " + methodName + "(" + argsContent + ") {" + line;
            //判斷返回值不等於void return 
         if (!returnTypeName.equals("void")) {
                        methodContent+=tab + tab + "System.out.println(\"帶參數測試增強打印log\");" + line
                                + tab + tab + "return target." + methodName + "(" + paramsContent + ");" + line
                                + tab + "}" + line;
                    }else {
                        methodContent+= tab + tab + "System.out.println(\"測試假裝增強打印log\");" + line
                                +tab + tab + "target." + methodName + "(" + paramsContent + ");" + line
                                + tab + "}" + line;
                    }
        }
        //最後把全部加起來。就是外面一個類裏面的內容
        content = packageContent + importContent + clazzFirstLineContent + filedContent + constructorContent + methodContent + "}";
        System.out.println(content);
    }
---------------- ---------------------控制檯輸出-------------------------------------------------
 package com.google;
	import com.luban.dao.UserDao;
	public class $Proxy implements UserDao{
		private UserDao target;
		public $Proxy (UserDao target){
			this.target =target;
		}
		public void query() {
			System.out.println("測試假裝增強打印log");
			target.query();
		}
		public String query(String p) {
			System.out.println("測試假裝增強打印log");
			return target.query(p);
		}
	}
如上圖,我們已經得到了要代理UserDaoImpl代碼內容,得到了代碼內容我們是不是還得將內容產生一個.java文件?是不是還得編譯.java文件得到.class文件?是不是還得new 這個對象 才能完成代理?
思考一下,我們怎麼把內容變成.java文件呢?是不是可以用IO流的字符流?
1、代碼接上面部分。(伸手黨拷貝的同學注意了)如下,我們把內容寫到G盤上產生.java文件。並且編譯它。
		//代碼接上面部分+
		File file1 =new File("G:\\com\\google");
        File file =new File("G:\\com\\google\\$Proxy.java");
            //判斷是否是一個目錄
            if (!file1.isDirectory())
                //創建目錄
                file1.mkdirs();
            if (!file.exists())
                //創建文件
                file.createNewFile();
            //將字符串 寫出到磁盤
            FileWriter fw = new FileWriter(file);
            fw.write(content);
            fw.flush();
            fw.close();
        //引用java編譯器。編譯java文件
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
        //傳入要編譯的文件
        Iterable units = fileMgr.getJavaFileObjects(file);
        JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
        t.call();
        fileMgr.close();
在G盤上就產生了一個 $Proxy.java的文件與.class文件,內容如下。

![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200527223305394.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2RpYW5odWExODY4MjQxMTgzMQ==,size_16,color_FFFFFF,t_70

2.接下來我們.java 文件也有了,.class文件也有了,我們怎麼得到這個對象呢?這些代碼都在我們的本地磁盤中,怎麼加載到我們的項目中呢?還記得我們的類加載器嘛?這裏就派上用場了。

1、代碼接上面部分。(伸手黨拷貝的同學注意了)

//創建需要加載.class的地址
        URL[] urls = new URL[]{new URL("file:G:\\\\")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        //加載class對象
        Class clazz = urlClassLoader.loadClass("com.google.$Proxy");
        //反射獲取到構造方法 構造方法帶參數,所以傳入接口對象
        Constructor constructor = clazz.getConstructor(targetInf);
        //構造方法傳入目標對象 就拿到了增強後的代理對象
        UserDao proxy = (UserDao)constructor.newInstance(new UserDaoImpl());
        System.out.println(proxy);
        //代理對象調用無參方法
        proxy.query();
        //代理對象調用有參方法
        System.out.println(proxy.query("哈哈哈哈哈哈哈哈測試成功"));
---------------- ---------------------控制檯輸出-------------------------------------------------
com.google.$Proxy@482f8f11
測試假裝增強打印log
假裝查詢數據庫
帶參數測試增強打印log
哈哈哈哈哈哈哈哈測試成功

總結:上面我們分析了,靜態代理與動態代理。並且手寫了一個山寨版本的動態代理。大家應該對動態代理有了 不一樣的理解。大家可能覺得很low 那麼JDK的動態代理是否也是這樣做的呢?其實JDK的動態代理 原理大致上也是這樣做的。下一篇,我們會來解析 JDK的動態代理源碼。。

項目源碼地址:源碼地址
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章