6. 3. 2015

Kam až může vést konflikt maven dependencí

Poznámky z čerstvé zkušenosti.

Problém

Na projektu máme závislost na artefaktu javassist-3.6.0. Kolega nedávno přidal do projektu dvě knihovny pro podporu REST API: Jersey a výbornou knihovnu Swagger pro dokumentaci. Obě ale zatáhly jiné verze artefaktů: 3.16.1 a 3.18.1. Než jsme si toho všimli, asi týden vše fungovalo, pak jsem však narazil na příklad, kde mi nad chováním Javy zůstal rozum stát. Po anonymizaci, minimalizaci a převodu na test vypadal takto:

Animal animal = ...
Dog dog = (Dog)animal;
assertSame(dog,animal);
assertEquals(BigDecimal.ONE,animal.getId());
assertEquals(BigDecimal.ONE,dog.getId()); // zde test spadne, getId() vrací null

Jak mohou dvě volání obyčejného getteru nad stejnou referencí dávat pokaždé jiný výsledek? Referencovaný objekt je javassist proxy představující Hibernate lazy inicializovanou entitu. První volání volá invocation handler, který provolá reálnou metodu, druhé volání se ale na invocation handler nedostane a vrací hodnotu fieldu (která se u umělé třídy nevyužívá, proto je null). Zkoumání ukázalo, že kořenovou příčinou je konflikt artefaktů, nicméně za chováním je shoda více okolností:
  • Animal (ve skutečnosti předek všech Hibernate entit) je generická třída parametrizovaná typem svého id, Dog je potomek konkretizující typový parametr pro typ BigDecimal
  • Artefakt javassist má groupId ve verzích 3.16.1 a 3.18.1 org.javassist, ve verzi 3.6.0 pouze javassist. Při kopírování do adresáře target se konflikt názvů sice vyřešil přidáním groupId – ve výsledku byly v adresáři nakonec 3 soubory javassist.jar, javassist-javassist.jar a org.javassist-javassist.jar, všechny soubory však měly stejný obsah odpovídající verzi 3.16.1.
  • Ve verzi 3.16.1 je bug projevující se u generik.
  • Projekt se na lokále spouštěl pod JRE 6.0.37 32bit (to jsem musel nedávno nainstalovat kvůli jiné aplikaci). Pod JRE 7u65 64bit, které běží na produkci, se chyba opět nezreprodukuje.

Kompletní ukázka


import java.lang.reflect.Method;
import java.math.BigDecimal;

import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

public class JavassistTest {

 static abstract class Animal<I> {
  abstract I getId();
 }

 static class Dog extends Animal<BigDecimal> {
  private BigDecimal id;
  BigDecimal getId() {
   return id;
  }
  void setId(BigDecimal poziceId) {
   this.id = poziceId;
  }
 }

 @Test
 public void test() throws Exception {
  ProxyFactory factory = new ProxyFactory();
  factory.setSuperclass(Dog.class);
  ProxyObject proxy = (ProxyObject)factory.createClass().newInstance();
  proxy.setHandler(new MethodHandler() {
   public Object invoke(Object arg0, Method arg1, Method arg2, Object[] arg3) throws Throwable {
    return BigDecimal.ONE; // return something nonnull - just proof of handler call
   }
  });
  Animal animal = (Animal<?>)proxy;
  Dog dog = (Dog)animal;
  assertTrue(proxy instanceof Animal && animal instanceof Animal && dog instanceof Animal);
  assertSame(dog,animal); // references are same
  assertEquals(BigDecimal.ONE,animal.getId());
  assertEquals(BigDecimal.ONE,dog.getId()); // here, proxy is not called in JDK 6.0.37 and javassist 3.16.1
 }

}

Zde je vidět chování pro jednotlivé verze JRE a javassistu:


Závěr


Poučením z uvedené situace je hlídat si pořádek v mvn dependencích a rozumně aktualizovat na vyšší verze. Problém se sice stal ve fázi vývoje a neohrozil provoz, nicméně jeho diagnostika nějaký čas zabrala. Trávit další čas hrabáním v bytekódu, proč se druhé volání nezaproxuje, už samozřejmě nehodlám, stačí mi potvrzení, že příčina je v nesprávném artefaktu, obzvlášť když bug je v javassistu již fixnut. (Po prolítnutí changelogu jen tipuji, že se jednalo o tuto chybu, ale blíže už to nezkoumám.) Vedlejší zkušeností pak bylo poznat, jak se Maven s konfliktem vypořádává.