O kompatybilności wstecznej javac słów kilka…

Słowa te to:

To jebane kurestwo nie działa!

Przynajmniej nie działa tak jak normalny człowiek, gdzie normalny równoważne jest programista java, się spodziewa. W czym rzecz. Na początek wygeneruj projekt w mavenie i skonfiguruj maven-build-plugin by korzystał z flag source i target do wersji 1.5.

UWAGA! Potrzebujesz zainstalowanej Javy 1.5 oraz 1.6+. Java 1.6+ musi być domyślną, a 1.5 nie powinno być możliwości wywołania inaczej niż przez podanie pełnej ścieżki do pliku java.exe.

Następnie piszemy prosty program:

Listing 1. Program testowy

public class App {
	public static void main(String[] args) {
		System.out.println("Hello World!".isEmpty());
	}
}

Metoda isEmpty() doszła w API 1.6. Zatem jeżeli damy odpowiednie flagi w trakcie kompilacji to…

Skompiluj program (mvn compile), następnie wejdź do target/classes i wywołaj:

Listing 2. Co mamy w środku klasy

$ javap -verbose pl.koziolekweb.blog.javac.App
Compiled from "App.java"
public class pl.koziolekweb.blog.javac.App extends java.lang.Object
  SourceFile: "App.java"
  minor version: 0
  major version: 49
  Constant pool:
const #1 = Method       #7.#16; //  java/lang/Object."":()V
const #2 = Field        #17.#18;        //  java/lang/System.out:Ljava/io/PrintStream;
const #3 = String       #19;    //  Hello World!
const #4 = Method       #20.#21;        //  java/lang/String.isEmpty:()Z
const #5 = Method       #22.#23;        //  java/io/PrintStream.println:(Z)V
const #6 = class        #24;    //  pl/koziolekweb/blog/javac/App
const #7 = class        #25;    //  java/lang/Object
const #8 = Asciz        ;
const #9 = Asciz        ()V;
const #10 = Asciz       Code;
const #11 = Asciz       LineNumberTable;
const #12 = Asciz       main;
const #13 = Asciz       ([Ljava/lang/String;)V;
const #14 = Asciz       SourceFile;
const #15 = Asciz       App.java;
const #16 = NameAndType #8:#9;//  "":()V
const #17 = class       #26;    //  java/lang/System
const #18 = NameAndType #27:#28;//  out:Ljava/io/PrintStream;
const #19 = Asciz       Hello World!;
const #20 = class       #29;    //  java/lang/String
const #21 = NameAndType #30:#31;//  isEmpty:()Z
const #22 = class       #32;    //  java/io/PrintStream
const #23 = NameAndType #33:#34;//  println:(Z)V
const #24 = Asciz       pl/koziolekweb/blog/javac/App;
const #25 = Asciz       java/lang/Object;
const #26 = Asciz       java/lang/System;
const #27 = Asciz       out;
const #28 = Asciz       Ljava/io/PrintStream;;
const #29 = Asciz       java/lang/String;
const #30 = Asciz       isEmpty;
const #31 = Asciz       ()Z;
const #32 = Asciz       java/io/PrintStream;
const #33 = Asciz       println;
const #34 = Asciz       (Z)V;

{
public pl.koziolekweb.blog.javac.App();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."":()V
   4:   return
  LineNumberTable:
   line 7: 0


public static void main(java.lang.String[]);
  Code:
   Stack=2, Locals=1, Args_size=1
   0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   ldc     #3; //String Hello World!
   5:   invokevirtual   #4; //Method java/lang/String.isEmpty:()Z
   8:   invokevirtual   #5; //Method java/io/PrintStream.println:(Z)V
   11:  return
  LineNumberTable:
   line 9: 0
   line 10: 11


}

Linia major version: 49 oznacza ni mniej ni więcej, że klasa jest zgodna z API 1.5. Uruchommy program na Javie 1.6. Ok wynik spodziewany, czyli false, choć nie do końca, bo jednak w 1.5 nie ma isEmpty(). Teraz uruchommy ją na Javie 1.5:

Listing 3. Uruchomienie z wykorzystaniem 1.5

$ /c/Program\ Files/Java/jre1.5.0_22/bin/java pl.koziolekweb.blog.javac.App
Exception in thread "main" java.lang.NoSuchMethodError: java.lang.String.isEmpty()Z
        at pl.koziolekweb.blog.javac.App.main(App.java:9)

o… popsuło się… Czyli jednak metody isEmpty() nie ma w 1.5.

Dlaczego tak się dzieje?

Otóż flagi source i target są sugestią dla kompilatora jak ma załatwić pewne sprawy. W przypadku tej pierwszej flagi kompilator będzie wiedział, że powinien zgłaszać błędy w przypadku użycia np. typów generycznych i enumów jeżeli jest ustawiona na wersję 1.4 i wcześniejsze. Ta druga flaga wymusza na kompilatorze tylko ustawienie odpowiedniej sygnatury w pliku class. Przy czym wartość przy target nie może być niższa niż ta przy source.
Kompilator nie sprawdza jednak czy dana metoda jest dostępna w API w danej wersji. W javadocu jest znacznik @since, ale jako, że javadoc jest usuwany z plików class to kompilator nie może sprawdzić czy metoda występuje w danej wersji API. Sprawdzane jest tylko czy metoda o danej sygnaturze jest dostępna w API aktualnie używanym do kompilacji. Sygnatury metod są sprawdzane w trakcie wywołania. Zresztą widzieliśmy to w przypadku uruchomienia programu w API 1.6. Pomimo, że wersja klasy wskazywała, że dana metoda nie powinna istnieć dla kodu w danej wersji to program uruchomił się prawidłowo.

Po co to wszystko

Ponieważ jest to pułapka, w którą bardzo łatwo wpaść pisząc integrację ze starszym systemem. Pomimo, że wersja klasy będzie prawidłowa to nadal istnieje szansa, że program wywali się z błędem NoSuchMethodError, a ty będziesz się zastanawiał dlaczego, bo przecież skompilowało się prawidłowo. Jeszcze bardziej będzie to bolało jeżeli używasz CI, bo wtedy problem wyjdzie dopiero u klienta w trakcie testów.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax