CodeGym /Java Blog /Toto sisi /Java 中的動態代理
John Squirrels
等級 41
San Francisco

Java 中的動態代理

在 Toto sisi 群組發布
你好!今天我們將考慮一個相當重要且有趣的話題:Java 中動態代理類的創建。這不是很簡單,所以我們將嘗試使用示例來弄明白 :) 那麼,最重要的問題是:什麼是動態代理以及它們的用途是什麼?代理類是原始類之上的一種“附加組件”,它允許我們在必要時更改原始類的行為。“改變行為”是什麼意思,它是如何運作的?考慮一個簡單的例子。假設我們有一個Person接口和一個實現這個接口的 簡單Man類

public interface Person {

   public void introduce(String name);
  
   public void sayAge(int age);
  
   public void sayWhereFrom(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("My name is " + this.name);
   }

   @Override
   public void sayAge(int age) {
       System.out.println("I am " + this.age + " years old");
   }

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

       System.out.println("I'm from " + this.city + ", " + this.country);
   }

   // ...getters, setters, etc.
}
我們的Man類有 3 個方法:introduce、sayAge 和 sayWhereFrom。想像一下,我們把這個類作為現成 JAR 庫的一部分,我們不能簡單地重寫它的代碼。但是我們也需要改變它的行為。例如,我們不知道可能會在我們的對像上調用哪個方法,但我們希望我們的人說“嗨!” (沒有人喜歡不禮貌的人)調用任何方法時。 動態代理 - 2遇到這種情況怎麼辦?我們需要一些東西:
  1. 調用處理器

這是什麼? 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("Hi!");
       return null;
   }
}
我們只需要實現一個接口方法:invoke()。而且,順便說一句,它做了我們需要的事情:它攔截了對我們對象的所有方法調用並添加了必要的行為(在invoke()方法內,我們向控制台輸出“Hi!”)。
  1. 原始對象及其代理。
我們為它創建了原始的Man對象和一個“附加組件”(代理):

import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());

   }
}
這看起來不是很簡單!我專門為每一行代碼添加了註釋。讓我們仔細看看發生了什麼。在第一行中,我們簡單地創建我們將為其創建代理的原始對象。以下兩行可能會給您帶來困難:

 // Get the class loader from the original object
ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();
實際上,這裡並沒有什麼特別的事情發生 :) 在第四行中,我們使用了特殊的Proxy類及其靜態的newProxyInstance()方法:

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
這個方法只是創建我們的代理對象。我們將在上一步中收到的有關原始類的信息(其ClassLoader及其接口列表)以及先前創建的InvocationHandler對像傳遞給該方法。最主要的是不要忘記將我們原來的arnold對像傳遞給調用處理程序,否則將沒有任何東西可以“處理”:) 我們最終得到了什麼?我們現在有一個代理對象:proxyArnold。它可以調用Person接口的任何方法。為什麼?因為我們在這裡給了它所有接口的列表:

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
現在它知道了Person接口 的所有方法。此外,我們向代理傳遞了一個配置為與arnold對像一起使用的PersonInvocationHandler對象:

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
現在,如果我們在代理對像上調用Person接口 的任何方法,我們的處理程序會攔截該調用並改為執行它自己的invoke()方法。讓我們嘗試運行main()方法!控制台輸出:

Hi!
出色的!我們看到調用了PersonInvocationHandler()invoke()方法,而不是原來的Person.introduce()方法:

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

   System.out.println("Hi!");
   return null;
}
“你好!” 顯示在控制台上,但這並不是我們想要的行為:/我們試圖實現的是首先顯示“Hi!” 然後調用原來的方法本身。換句話說,方法調用

proxyArnold.introduce(arnold.getName());
應該顯示“嗨!我叫阿諾德”,而不是簡單的“嗨!” 我們怎樣才能做到這一點?這並不復雜:我們只需要對我們的處理程序和invoke()方法採取一些自由:) 注意傳遞給此方法的參數:

public Object invoke(Object proxy, Method method, Object[] args)
invoke ()方法可以訪問最初調用的方法及其所有參數(Method 方法、Object[] args)。換句話說,如果我們調用proxyArnold.introduce(arnold.getName())方法以便調用invoke()方法而不是introduce()方法,那麼在該方法中我們可以訪問原始的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("Hi!");
       return method.invoke(person, args);
   }
}
現在在invoke()方法中,我們添加了對原始方法的調用。如果我們現在嘗試運行之前示例中的代碼:

import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());
   }
}
然後我們會看到現在一切正常:) 控制台輸出:

Hi! My name is Arnold
你什麼時候可能需要這個?實際上,經常。“動態代理”設計模式在流行技術中被積極使用……哦,對了,忘了說動態代理是一種設計模式!恭喜,你又學到了一個!:) 動態代理 - 3例如,它被積極地用於與安全相關的流行技術和框架中。想像一下,您有 20 個方法只能由登錄到您的程序的用戶執行。使用您學到的技術,您可以輕鬆地向這 20 種方法添加檢查以查看用戶是否輸入了有效憑據,而無需在每種方法中復制驗證碼。 或者假設您想創建一個記錄所有用戶操作的日誌。使用代理也很容易做到這一點。即使是現在,您也可以簡單地向我們上面的示例添加代碼,以便在您調用invoke()時顯示方法名稱,這將生成我們程序的超級簡單日誌 :) 最後,請注意一個重要的限制。代理對象使用接口,而不是類。為接口創建代理。看看這段代碼:

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
這裡我們專門為Person接口 創建了一個代理。如果我們嘗試為該類創建一個代理,即更改引用類型並嘗試轉換為Man類,它將無法工作。

Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

proxyArnold.introduce(arnold.getName());
線程“main”中的異常 java.lang.ClassCastException:com.sun.proxy.$Proxy0 無法轉換為 Man 擁有接口是絕對必要的。代理與接口一起工作。這就是今天的全部:) 好吧,現在解決一些任務就好了!:) 直到下一次!
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION