Über ein Erlebnis der besonderen Art: JDK6, JDK5 und Spring 2.0.0

3 Kommentare

In einem unserer Projekte führen wir derzeit eine Migration von JDK5 Update 7 zu JDK6 Update 12 durch. In einer unserer Anwendungen verwenden wir JCaptcha zur Absicherung von Formularen. Den größtmöglichen Konfigurations-Komfort für das Captcha erreichen wir durch die Verwendung von Spring-Beans, die beim Initialisieren des Captcha-Servlets zur Anwendung kommen.

Bei der Umstellung auf Java 6 (Update 12) kam es auf einer lokalen Entwicklungsmaschine plötzlich zu einer IllegalArgumentException bei der Initialisierung des Captcha-Servlets. JBoss lokal weiterhin mit Java 5 laufen zu lassen behob zwar das Problem, war jedoch nicht zielführend, da die Zielsysteme schrittweise alle auf Java 6 umgestellt werden sollen.

Anbei ein Auszug aus dem Stacktrace der Exception:

java.lang.IllegalArgumentException: Color parameter outside of expected range: Red Green Blue
	at java.awt.Color.testColorValueRange(Color.java:298)
	at java.awt.Color.(Color.java:382)
	at java.awt.Color.(Color.java:357)
	at java.awt.Color.(Color.java:448)

Eine genauere Untersuchung des Phänomens im Debugger ergab letztlich folgendes Bild:

Es wurde also offensichtlich der Color-Konstruktor verwendet, der 3 float-Parameter akzeptiert. Ein Auszug aus der Spring-Bean-Konfiguration:

<bean id="captchaBackgroundColor" class="java.awt.Color">
	<constructor-arg index="0"><value>255</value></constructor-arg>
	<constructor-arg index="1"><value>255</value></constructor-arg>
	<constructor-arg index="2"><value>255</value></constructor-arg>
</bean>

Der float-Konstruktor enthält als erstes die Zeile:

this( (int) (r*255+0.5), (int) (g*255+0.5), (int) (b*255+0.5));

Damit wird der Konstruktor für 3 int-Parameter aufgerufen und bekommt dreimal 65025 als Argument übergeben. Das Resultat: die oben beschriebene IllegalArgumentException.

Die exakte Ursache für das Problem liegt in einer Kombination aus mehreren Umständen. Weniger technisch interessierte Leser können die folgende Aufzählung guten Gewissens überspringen:

  • Es wird Spring 2.0.0 verwendet. Der zu verwendende Konstruktor wird mittels ConstructorResolver.autowireConstructor(…) ermittelt. In Zeile 100, in ebendieser Methode, wird per Reflection ein Array der potenziellen Konstruktoren aufgebaut. Anschließend wird dieses Array sortiert. Das darunter wirkende Arrays.sort(…) liefert mit JDK6 auf Windows-Systemen ein anderes Ergebnis als mit JDK5.
  • In dem sortierten Array steht also der Konstruktor Color(float, float, float) weiter vorn als der Konstruktor Color(int, int, int). Mit JDK5 steht der int-Konstruktor weiter vorn.
  • Es folgt eine Schleife, die aus dem sortierten Konstruktor-Array den Konstruktor auswählt, der zur Instanzierung des gewünschten Objekts verwendet werden soll. Diese Schleife ermittelt anhand der Anzahl Argumente (trivial) und einer sogenannten TypeDifferenceWeight (etwas komplizierter) den zu verwendenden Konstruktor.
  • TypeDifferenceWeight bedeutet, dass eine Differenz in der Klassenhierarchie zwischen Typen und Argumenten errechnet wird. Wir wollen int-Argumente verwenden, um unser Color-Objekt zu instanzieren. Die Methode zur Berechnung der TypeDifferenceWeight geht für jeden Parameter-Typ die Klassenhierarchie des dazugehörigen Arguments so lange nach oben, bis keine höhere Superklasse auffindbar ist. Solange die gefundene Superklasse ein Typ ist, dem das dazugehörige Argument zugewiesen werden kann, wird der TypeDifferenceWeight-Wert erhöht und die Suche fortgesetzt. (AutowireUtils.getTypeDifferenceWeight(…))
  • Dies bedeutet logischerweise, dass die TDW von float und int 0 ist, da beide primitive Datentypen sind.
  • Ist die ermittelte TDW kleiner als die bisher kleinste gefundene TDW, wird der Konstruktor aus dem aktuellen Schleifendurchlauf als zu verwendender Konstruktor gesetzt.
  • Da im Array der float-Konstruktor mit JDK6 weiter vorn steht, und da bei den nachfolgenden Konstruktoren die kleinste gefundene TDW nicht mehr kleiner werden kann (0 kann nicht kleiner als 0 sein), wird im Endeffekt der float-Konstruktor verwendet.
  • Der float-Konstruktor übergibt die Argumente multipliziert mit 255 und um 0,5 erhöht als int-Werte an den int-Konstruktor.
  • Der int-Konstruktor wird also als Color(65025, 65025, 65025) aufgerufen. Folge: die IllegalArgumentException, weil RGB-Werte nur zwischen 0 und 255 liegen können.

Zurück zum greifbaren Phänomen: auf dem Testserver, der bereits auf Java 6 umgestellt ist, läuft das Captcha nach wie vor fehlerfrei. Offensichtlich ist also das Problem darin begründet, dass die Konstruktoren von einer JVM auf einem Linux-System anders sortiert werden als auf einem Windows-System. Zusätzlich sollte der Type Weight Difference Mechanismus in Spring hoffentlich in neueren Versionen intelligenter sein, falls Autowire noch darauf basiert.

Abhilfe schafft eine explizite Typangabe in der Bean-Konfiguration:

<bean id="captchaBackgroundColor" class="java.awt.Color">
	<constructor-arg index="0" type="int"><value>255</value></constructor-arg>
	<constructor-arg index="1" type="int"><value>255</value></constructor-arg>
	<constructor-arg index="2" type="int"><value>255</value></constructor-arg>
</bean>

Damit ist sichergestellt, dass der gewünschte Konstruktor, nämlich java.awt.Color#Color(int r, int g, int b), verwendet wird.

Meine weitestmöglich in die Tiefe gehende Sourcecode-Analyse von Spring 2.0.0 und JDK6 Update 12 ergab keine exakte Erkenntnis, warum Arrays.sort(…) mit dem vom Spring Framework übergebenen Comparator auf Windows-Systemen ein anderes Ergebnis als auf Linux-Systemen liefert. Wer dazu etwas sagen kann, ist herzlich eingeladen dies zu tun.

Fazit: der Teufel steckt im Detail. Auch eine vermeintlich „kleine“ Änderung wie ein Update der Java-Version kann dazu führen, dass schwer zu findende Fehler entstehen. Intensives und präzises Testen ist bei einer solchen Änderung unerlässlich!

Vielen lieben Dank an Mike Wiesner, Senior Consultant bei SpringSource, der mir bei einer Frage zu Spring 2.0.0 „in Echtzeit“ weitergeholfen hat!

Kommentare

  • Fabian Lange

    Nice finding, Rob. Open Source is great when you really want to dig deep. And digging deep is really fun, because you can learn a lot from how others have created code.

  • Mark Helmstetter

    23. Februar 2010 von Mark Helmstetter

    Thank you!!! You just saved me a ton of time. I had no idea that a simple switch from JDK5 to JDK6 could cause so many quirks.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.