10. 7. 2014

Číslování anonymních tříd

Že Eclipse JDT compiler dělá některé věci jinak než javac, je obecně známo – typickou odlišností je např. lepší inference generických typů při volání generických metod. Dnes jsem narazil na další vypečený případ, který stojí za blogpost. Naše aplikace je tvořena
  • serverovou částí, která běží v Tomcatu
  • klientskou částí, která se u uživatelů spouští přes Java Web Start, ale při vývoji na lokálním počítači ji spouštíme klasicky z IDE (při testování ji samozřejmě z testovacích prostředí spouštíme taky přes Java Web Start)
  • společnou částí – api.jar, který je sdílen serverem i klientem, obsahuje interfacy servisních metod a DTO objekty
Tentokrát jsem však potřeboval otestovat plnou funkčnost včetně spouštění přes Java Web Start, ale z lokálního počítače. Tento usecase byl trochu výjimečný, šlo o otestování spouštění pod jiným updatem JRE, než je na testovacích prostředích. Zbuildoval jsem tedy api.jar, klient i server, spustil jsem v Eclipsu Tomcat, vypublikoval do něj server (používáme WTP) a připravil klienta tak, aby se našel dle URL v JNLP souboru. Vše připraveno, musí to klapnout! Oops - nastává chyba:
java.io.InvalidClassException: Foo$2; class invalid for deserialization

Příčina

V api.jar existovala třída Foo, která v sobě měla dvě různé instance anonymních tříd (osekávám na minimalistický případ, ve skutečnosti jich bylo víc). Buildování aplikace provádíme Mavenem, který (přesněji maven-compiler-plugin) pro kompilaci tříd používá javac.Vypublikování do Tomcatu však WTP dělalo z adresáře, kam byl api.jar zbuildován pomocí Eclipse JDT.

Pozorování ukázalo, že v souboru Foo$1 je – podle kompilátoru, který jej vytvořil – pokaždé jiná třída. Totéž v souboru Foo$2. Server a klient tedy měly každý svůj api.jar, v něm stejně pojmenované třídy, ale s jiným obsahem. Server klientovi posílal serializovanou instanci třídy Foo$2, ale ten pod stejným názvem znal jinou anonymní třídu.

Ve třídě Foo navíc byly anonymní třídy použity při inicializaci fieldu (to je pro rekonstrukci chyby důležité – viz příklad níže), což není časté a zdůvodňuje to, proč se na problém nepřišlo dřív.

Nikde jsem se nedogooglil závazné specifikace, že anonymní třídy se mají číslovat $1, $2..., natož způsob určení pořadí dle jazykových konstruktů ve zdrojáku. (Nejpodobnější byla pouze tato stará chyba v JDT.) Pozorováním se zdá, že javac čísluje anonymní třídy podle jejich výskytu ve zdrojáku, zatímco Eclipse JDT projde nejdřív fieldy (možná tvoří implicitní konstruktor) a pak teprve metody:

Příklad

package numbering_anonymous_class;
public class Test {
    public void method() {new Error() {};}
    public final Object field = new Exception() {};
}

Pozn.: třídy Error a Exception jsou vybrány jen pro stručnost, aby nebylo nutné importovat ani překrývat nic v těle a byl poznat původ v Java decompileru (proto ne new Object() {}).

Po uložení v Eclipse a zkompilování dostává třída ve fieldu číslo $1, třída v metodě číslo $2:

c:\java\workspace\work\bin\numbering_anonymous_class>javap -c Test
Compiled from "Test.java"
public class numbering_anonymous_class.Test extends java.lang.Object{
public final java.lang.Object field;

public numbering_anonymous_class.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #10; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   new     #12; //class numbering_anonymous_class/Test$1
   8:   dup
   9:   aload_0
   10:  invokespecial   #14; //Method numbering_anonymous_class/Test$1."<init>":(Lnumbering_anonymous_class/Test;)V
   13:  putfield        #17; //Field field:Ljava/lang/Object;
   16:  return

public void method();
  Code:
   0:   new     #24; //class numbering_anonymous_class/Test$2
   3:   aload_0
   4:   invokespecial   #26; //Method numbering_anonymous_class/Test$2."<init>":(Lnumbering_anonymous_class/Test;)V
   7:   return

}

Po zkompilování v javac

c:\java\workspace\work\bin\numbering_anonymous_class>javac -d .. ..\..\src\numbering_anonymous_class\Test.java

dostává třída ve fieldu číslo $2, třída v metodě číslo $1:

c:\java\workspace\work\bin\numbering_anonymous_class>javap -c Test
Compiled from "Test.java"
public class numbering_anonymous_class.Test extends java.lang.Object{
public final java.lang.Object field;

public numbering_anonymous_class.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   new     #2; //class numbering_anonymous_class/Test$2
   8:   dup
   9:   aload_0
   10:  invokespecial   #3; //Method numbering_anonymous_class/Test$2."<init>":(Lnumbering_anonymous_class/Test;)V
   13:  putfield        #4; //Field field:Ljava/lang/Object;
   16:  return

public void method();
  Code:
   0:   new     #5; //class numbering_anonymous_class/Test$1
   3:   dup
   4:   aload_0
   5:   invokespecial   #6; //Method numbering_anonymous_class/Test$1."<init>":(Lnumbering_anonymous_class/Test;)V
   8:   pop
   9:   return

}

Poučení

  • Na pořadí anonymních tříd nelze spoléhat.
  • Všechno dělat jedním compilerem. (Nebo aspoň mít přehled, který compiler co dělá.)

Pokud s tím máte jiné nebo další zkušenosti, rád do článku doplním update.

2 komentáře:

  1. Anonymni tridy lze normalizovat (precislovat) pri nacitani class loaderem. Inspiraci muzes najit v projektu HotspawAgent - plugin AnonymousClassPatchPlugin.

    OdpovědětVymazat
    Odpovědi
    1. Díky za připomínku. Vždy sice raději preferuji řešení, kdy se počet vrstev nutných pro vyřešení problému minimalizuje, takže myslím, že opravit buildování bylo v dané situaci přímočařejší než do toho zatahovat proprietární classloader. Ale připomínky si vážím, protože se na věc dívá z jiného pohledu, což se může hodit. A zvýšila můj zájem o projekt HotswapAgent.

      Vymazat