JavaRush /Java Blog /Random-TW /Java 中的動態代理

Java 中的動態代理

在 Random-TW 群組發布
你好!今天我們將討論一個相當重要且有趣的主題—在Java中建立動態代理類別。這不太簡單,所以讓我們嘗試透過範例來弄清楚:) 那麼,最重要的問題是:什麼是動態代理以及它們的用途是什麼?代理類別是原始類別的一種“上層建築”,它允許我們在必要時更改其行為。改變行為意味著什麼?它是如何運作的?讓我們來看一個簡單的例子。假設我們有一個介面Person和一個Man實作該介面的 簡單類
public interface Person {

   public void introduce(String name);

   public void sayAge(int age);

   public void sayFrom(String city, String country);
}

public class Man implements Person {

   private String name;
   private int age;
   private String city;
   private String country;

   public Man(String name, int age, String city, String country) {
       this.name = name;
       this.age = age;
       this.city = city;
       this.country = country;
   }

   @Override
   public void introduce(String name) {

       System.out.println("Меня зовут " + this.name);
   }

   @Override
   public void sayAge(int age) {
       System.out.println("Мне " + this.age + " years");
   }

   @Override
   public void sayFrom(String city, String country) {

       System.out.println("Я из города " + this.city + ", " + this.country);
   }

   //..геттеры, сеттеры, и т.д.
}
我們班Man有3種方法:自我介紹,說你的年齡,說你來自哪裡。假設我們收到此類作為現成 JAR 庫的一部分,並且不能簡單地獲取和重寫其程式碼。然而,我們需要改變他的行為。例如,我們不知道將在我們的物件上呼叫哪個方法,因此我們希望該人在呼叫其中任何一個方法時首先說「Hello!」。(沒有人喜歡不禮貌的人)。 動態代理 - 1遇到這樣的情況我們該怎麼辦呢?我們需要一些東西:
  1. InvocationHandler

這是什麼?可以直譯為「呼叫攔截器」。這非常準確地描述了其目的。 InvocationHandler是一個特殊的接口,允許我們攔截對物件的任何方法呼叫並添加我們需要的附加行為。我們需要製作自己的攔截器——也就是創建一個類別並實作這個介面。這很簡單:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {

private Person person;

public PersonInvocationHandler(Person person) {
   this.person = person;
}

 @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

       System.out.println("Hello!");
       return null;
   }
}
我們只需要實作一個介面方法 - invoke()。事實上,它做了我們需要的事情——它攔截對我們物件的所有方法呼叫並添加必要的行為(這裡我們invoke()在方法內的控制台上列印「Hello!」)。
  1. 原始物件及其代理。
讓我們建立一個原始物件Man和它的「上層結構」(代理):
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       //Создаем оригинальный an object
       Man vasia = new Man("Vasya", 30, "Санкт-Петербург", "Россия");

       //Получаем загрузчик класса у оригинального an object
       ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

       //Получаем все интерфейсы, которые реализует оригинальный an object
       Class[] interfaces = vasia.getClass().getInterfaces();

       //Создаем прокси нашего an object vasia
       Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

       //Вызываем у прокси an object один из методов нашего оригинального an object
       proxyVasia.introduce(vasia.getName());

   }
}
看起來很不簡單啊!我專門為每一行程式碼寫了一條註解:讓我們仔細看看那裡發生了什麼事。

在第一行中,我們只需建立要為其建立代理的原始物件。以下兩行可能會讓您感到困惑:
//Получаем загрузчик класса у оригинального an object
ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

//Получаем все интерфейсы, которые реализует оригинальный an object
Class[] interfaces = vasia.getClass().getInterfaces();
但這裡確實沒有什麼特別的:)要建立代理,我們需要ClassLoader原始物件的(類別載入器)和原始類別(即)實作的所有介面的列表Man。如果您不知道它是什麼ClassLoader,您可以閱讀這篇關於將類別載入到 JVM 的文章或Habré 上的這篇文章,但先不要太在意它。請記住,我們將獲得一些額外的信息,然後我們將需要這些資訊來建立代理物件。在第四行,我們使用一個特殊的類別Proxy及其靜態方法newProxyInstance()
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
這個方法只是創建我們的代理物件。我們向該方法傳遞有關我們在上一步中收到的原始類別的資訊(它ClassLoader及其介面清單),以及我們先前建立的攔截器的物件 - InvocationHandler'a. 最重要的是不要忘記將我們的原始物件傳遞給攔截器vasia,否則它將沒有任何東西可以「攔截」:)我們最終得到了什麼?我們現在有了一個代理對象vasiaProxy。它可以呼叫任何介面方法Person。為什麼?因為我們向它傳遞了所有介面的列表 - 這裡:
//Получаем все интерфейсы, которые реализует оригинальный an object
Class[] interfaces = vasia.getClass().getInterfaces();

//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
現在他已經「認識」了所有的介面方法Person。此外,我們也向代理傳遞了一個配置PersonInvocationHandler為與該物件一起使用的物件vasia
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
現在,如果我們呼叫代理物件上的任何介面方法Person,我們的攔截器將「捕獲」此呼叫並執行其自己的方法invoke()。讓我們嘗試運行該方法main()控制台輸出: 你好! 偉大的!我們看到我們的方法不是真正的方法,而是Person.introduce()被呼叫: invoke()PersonInvocationHandler()
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   System.out.println("Hello!");
   return null;
}
控制台顯示“Hello!” 但這並不完全是我們想要得到的行為:/ 根據我們的想法,應該首先顯示“Hello!”,然後我們調用的方法本身應該會起作用。換句話說,這個方法呼叫:
proxyVasia.introduce(vasia.getName());
應該會輸出到控制台「Hello!我的名字是瓦夏”,而不僅僅是“你好!” 我們怎樣才能做到這一點?沒什麼複雜的:你只需要稍微修改一下我們的攔截器和方法invoke():)注意傳遞給這個方法的參數:
public Object invoke(Object proxy, Method method, Object[] args)
方法invoke()可以存取它所呼叫的方法及其所有參數(方法方法、Object[] args)。換句話說,如果我們呼叫一個方法,並且呼叫的proxyVasia.introduce(vasia.getName())不是方法,而是方法,那麼在該方法中我們可以存取原始方法及其參數!結果,我們可以這樣做: introduce()invoke()introduce()
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {

   private Person person;

   public PersonInvocationHandler(Person person) {

       this.person = person;
   }

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       System.out.println("Hello!");
       return method.invoke(person, args);
   }
}
現在我們已經invoke()在方法中加入了對原始方法的呼叫。如果我們現在嘗試運行前面範例中的程式碼:
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       //Создаем оригинальный an object
       Man vasia = new Man("Vasya", 30, "Санкт-Петербург", "Россия");

       //Получаем загрузчик класса у оригинального an object
       ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

       //Получаем все интерфейсы, которые реализует оригинальный an object
       Class[] interfaces = vasia.getClass().getInterfaces();

       //Создаем прокси нашего an object vasia
       Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

       //Вызываем у прокси an object один из методов нашего оригинального an object
       proxyVasia.introduce(vasia.getName());
   }
}
然後我們會看到現在一切正常:) 控制台輸出: 你好!我叫 Vasya。 您在哪裡需要它?其實很多地方。「動態代理」設計模式在流行技術中被積極使用......順便說一句,我忘了告訴你,Dynamic Proxy它是一種模式!恭喜你,你又學會一個了!:) 動態代理 - 2因此,它被積極地應用於與安全相關的流行技術和框架中。想像一下,您有 20 個方法只能由程式的登入使用者執行。使用您學到的技術,您可以輕鬆地在這 20 個方法中新增檢查,以查看使用者是否輸入了登入名稱和密碼,而無需在每個方法中單獨複製驗證碼。或者,例如,如果您想建立一個記錄所有使用者操作的日誌,使用代理程式也可以輕鬆完成。現在您甚至可以:只需在範例中新增程式碼,以便在呼叫時在控制台中顯示方法的名稱invoke(),您將獲得我們程式的簡單日誌:) 在講座結束時,請注意一個重要的內容限制。建立代理物件發生在介面級別,而不是類別級別。為該介面建立代理。看一下這段程式碼:
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
這裡我們專門為該介面建立一個代理程式Person。如果我們嘗試為該類別建立代理,也就是說,我們更改連結的類型並嘗試強制轉換為該類Man,則什麼都不會起作用。
Man proxyVasia = (Man) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

proxyVasia.introduce(vasia.getName());
在執行緒「main」中出現異常 java.lang.ClassCastException: com.sun.proxy.$Proxy0 無法轉換為 Man 介面的存在是強制性要求。代理工作在介面層級。這就是今天的全部內容:) 作為有關代理主題的附加材料,我可以向您推薦一個精彩的視頻和一篇好文章。好吧,現在解決一些問題就好了!:) 再見!
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION