2014-10-17 81 views
9

我在網上做了一個小型的研究,並回顧了這個網站上的相關主題,但答案是矛盾的:有些人說這是不可能的,其他人說這是可能的,但是很危險。是否可以在沒有外部類的情況下序列化匿名類?

目標是傳遞匿名類的對象作爲RMI方法的參數。由於RMI的要求,這個類必須是可序列化的。這是沒有問題的,它很容易讓類Serializable。

但是我們知道內部類的實例持有對外部類的引用(並且匿名類是內部類)。因此,當我們序列化內部類的實例時,外部類的實例將被序列化以及一個字段。下面是問題出現的地方:外部類不是可序列化的,更重要的是 - 我不想序列化它。我想要做的只是發送匿名類的實例。

簡單的例子 - 這是一個RMI服務與接受Runnable接口的方法:

public interface RPCService {  
    Object call(SerializableRunnable runnable); 
} 

這裏是我想如何調用該方法

void call() { 
    myRpcService.call(new SerializableRunnable() {    
     @Override 
     public Object run { 
      System.out.println("It worked!"); 
     } 
    }   
} 

正如你所看到的,我想要做的是向對方發送一個「動作」 - 系統A描述了應該在系統B上運行的代碼。這就像在Java中發送腳本一樣。

如果可能的話,我可以很容易地看到一些危險的後果:例如,如果我們從Runnable訪問一個字段或捕獲外部類的最終變量 - 我們會陷入麻煩,因爲調用者實例不存在。另一方面,如果我在Runnable中使用安全代碼(編譯器可以檢查它),那麼我看不出有什麼理由來禁止此操作。

所以,如果有人知道,如何writeObject()readObject()方法應在匿名類或如何提及外部類transient或解釋爲什麼它是在Java中不可能正確地重寫,這將是非常有益的。

UPD 另一個重要的事情要考慮:外部類中不存在將要執行的方法(系統B)的環境,這就是爲什麼有關它的信息應該完全排除,以避免NoClassDefFoundError

+0

現在你沒有機會了。除了非常hardcore的方式,如發送類的字節碼到另一邊,並在那裏恢復 – talex 2014-10-17 10:35:18

回答

6

您可以嘗試製作Caller.call() a static方法。

但是,匿名類仍然需要在反序列化序列化實例的上下文中可用。這是不可避免的。

(很難想象這樣一種情況,匿名類可用,但封閉類不可用。)


因此,如果有人能證明,我怎麼能正確地覆蓋我的匿名類的writeObject和readObject方法...

如果您Caller.call()靜態的,那麼你可以這樣做就像你會,如果它是一個命名的類,我想。 (我敢肯定,你可以找到這樣的例子給自己。)


事實上,(模匿名類的可用性問題),它的工作原理。這裏,static main方法替代static Classer.call()方法。該程序編譯並運行,表明在靜態方法中聲明的匿名類可以被序列化和反序列化。

import java.io.*; 

public class Bar { 

    private interface Foo extends Runnable, Serializable {} 

    public static void main (String[] args) 
      throws InterruptedException, IOException, ClassNotFoundException { 

     Runnable foo = new Foo() { 
      @Override 
      public void run() { 
       System.out.println("Lala"); 
      } 
     }; 

     Thread t = new Thread(foo); 
     t.start(); 
     t.join(); 

     ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
     ObjectOutputStream oos = new ObjectOutputStream(baos); 
     oos.writeObject(foo); 
     oos.close(); 
     Foo foofoo = (Foo) new ObjectInputStream(
      new ByteArrayInputStream(baos.toByteArray())).readObject(); 

     t = new Thread(foofoo); 
     t.start(); 
     t.join(); 
    } 
} 

要記住的另一個重要的一點:Caller類不是存在於環境中,執行的方法,所以我想序列化過程中排除所有關於它的信息,以避免NoClassDefFoundError

沒有辦法避免這種情況。在遠程JVM中的反序列化的抱怨是因爲類描述符包含對外部類的引用。反序列化方需要解析該引用,即使您設法打開引用,並且即使您從未在反序列化對象中顯式或隱式使用綜合變量。

問題是遠程JVM的類加載器在加載內部類的類文件時需要知道外部類的類型。它需要驗證。它需要反思。垃圾收集器需要它。

沒有解決方法。

(我不知道這是否也適用於static內部類......但我懷疑它。)


試圖不具有外部類指的不是序列化匿名Runnable的實例只有序列化問題,而是在另一個環境中執行任意代碼的可能性。很高興看到JLS參考資料,描述這個問題。

這裏沒有JLS參考。序列化和類加載器不在JLS中指定。 (類初始化是...但這是一個不同的問題。)

可以通過RMI在遠程系統上運行任意代碼。但是,您需要實現RMI動態類加載來實現此目的。這裏是一個參考:

注意添加動態類加載遠程類RMI介紹顯著的安全問題。你必須考慮類加載器泄漏等問題。

+0

因此,在你的建議中,我們只是避免了使Bar類可序化的必要性,因爲調用是靜態的? – AdamSkywalker 2014-10-17 13:47:35

+0

@Boosha - 不完全。這是因爲在靜態方法中聲明Foo類有效地使其成爲一個靜態類。 – 2014-10-17 17:16:37

2

答案是否定的。你不能這樣做,因爲Inner類將需要外部類被序列化。當你試圖在內部類中調用外部類的實例方法時,你也會遇到麻煩。你爲什麼不再擁有另一個可以發送的頂級課程?

+0

我更新了這個問題,以解釋爲什麼我不能有一個頂級的可串行化類。 – AdamSkywalker 2014-10-17 10:31:02

+0

在這種情況下,我不知道,我會在這裏留下我的答案,以防其他人在沒有限制的情況下出現類似的問題。對不起,我忍不住了。 – Lucas 2014-10-17 10:40:11

3

如果你瘋了,可以使用反射來查找包含對外部類的引用的字段,並將其設置爲null

3

你如上所述,因爲匿名內部類是類來電中聲明在Java中不能工作了,你明確表示無法使用的RPC服務器上該類來電(如果我明白了正確的)例子。請注意,對於Java RPC,只有數據通過網絡發送,這些類必須已經在客戶端和服務器上可用。尊重你的例子沒有意義,因爲它看起來像你想發送代碼而不是數據。通常,您可以將可序列化的類放入可供服務器和客戶端使用的JAR中,並且每個可序列化的類應具有唯一的serialVersionUID。

2

你不能做的正是你想要的東西,這是序列化一個匿名內部類,不也使得其外圍實例序列化和序列化過它。這同樣適用於本地類。這些不可避免地有具有引用其封閉實例的隱藏字段,所以序列化實例也會嘗試序列化它們的封裝實例。

有幾種不同的方法可以嘗試。

如果您使用的是Java 8,則可以使用lambda表達式而不是匿名內部類。可序列化的lambda表達式不(必然)具有對其封閉實例的引用。您只需確保您的lambda表達式不明確或隱式地引用this,例如通過使用封閉類的字段或實例方法。該代碼,這應該是這樣的:

public class Caller { 
    void call() { 
     getRpcService().call(() -> { 
      System.out.println("It worked!"); 
      return null; 
     }); 
} 

(該return null是存在的,因爲RPCService.Runnable.run()聲明爲返回Object

另外請注意,任何值這個拉姆達捕獲(例如,局部變量,或封閉類的靜態字段)也必須是可序列化的。

如果您不使用Java 8,下一個最佳選擇是使用靜態嵌套類。

public class Caller { 
    static class StaticNested implements RPCService.Runnable { 
     @Override 
     public Object run() { 
      System.out.println("StaticNested worked!"); 
      return null; 
     } 
    } 

    void call() { 
     getRpcService().call(new StaticNested()); 
    } 
} 

這裏的主要區別是,這種缺乏捕捉Caller或局部變量的實例字段從call()方法的能力。如果有必要,這些可以作爲構造函數參數傳遞。當然,這樣通過的所有東西都必須是可序列化的。

對此的一種變化,如果你真的想要使用一個匿名類,就是在靜態上下文中實例化它。 (See JLS 15.9.2。)在這種情況下,匿名類不會有封閉的實例。代碼如下所示:

public class Caller { 
    static RPCService.Runnable staticAnonymous = new RPCService.Runnable() { 
     @Override 
     public Object run() { 
      System.out.println("staticAnonymous worked!"); 
      return null; 
     } 
    }; 

    void call() { 
     getRpcService().call(staticAnonymous); 
    } 
} 

雖然這對於靜態嵌套類來說並不容易,你仍然必須命名它存儲的字段,但仍然無法捕獲任何內容,甚至無法將值傳遞給構造函數。但它確實滿足了你最初問題的要求,即如何序列化一個匿名類的實例而不序列化一個封閉的實例。

相關問題