středa 8. ledna 2014

DCEVM a JSF





Programuji v Javě a dělám tak v Eclipse. Mojí denní, milionkrát opakovanou rutinou bylo upravit kód, následně pak  nastartovat aplikaci, potom se browserem zalogovat, dále se pak proklikat do oblíbeného místa v UI a konečně krokování v debugeru. Jak aplikace rostla, tak se čas startů čím dál tím víc prodlužoval a mě začala docházet trpělivost. Stárnu, a už si nemůžu dovolit  ztrácet čas zevlováním spojeným s civěním do výpisů logů při startu naší aplikace. Jal jsem se tedy s tím něco dělat.

Aplikace nám běží v Oracle JVM (HotSpot VM), ten umí hotswapovat třídu, pouze pokud došlo ke změně uvnitř metody. S každým jiným typem změny, například s přidáním metody, si JVM už neporadí a aplikaci je nutné restartovat. Toto je jistě neblahý nedostatek JVM, který se snaží napravit několik nekomerčních, tak i jeden známý komerční produkt. Vyzkoušel jsem mnohé a má prázdná kapsa mě nakonec dovedla k DCEVM.

DCEVM je upravený HotSpot VM v Openjdk, který si poradí i s jinými typy změn třídy. Původní DCEVM byl vyvinut pro Java6 a jeho další vývoj na delší dobu usnul. Naštěstí se letos objevila verze i pro Java7 od Ivana Dubrova . Ta je k nalezení zde. DCEVM je třeba zkompilovat, protože binární balíčky nejsou dostupné. Build pro Linux byl bez problémů, bohužel build pro win32 je v současné době rozbitý. Binární balíčky pro zbytek teamu pracující s win32/64 jsem nakonec získal vedlejším kanálem od autora. Poté, co jsem si v eclipse nastavil jako pracovní JVM OpenJDK s DCEVM, dal jsem se do testování.

První věcí bylo přidání/odebrání metody a zvolání  "Heuréka, ono to funguje!". Dalším krokem bylo otestování vazby s JSF EL. A ejhle, zádrhel, JSF nové metody nevidí. Moje podezření, že za všechno můžou reflection cache se potvrdilo. Zbývalo odhalit, kde všude jsou staré reflection informace uloženy, jak se na ně dostat, jak je rozlousknout, když jsou jako private a nakonec je smazat. Mazání cache jsem shrnul v utilitové třídě. Dále bylo nutné vytvořit watchdog hlídající změny v adřesáři projektu v tomcatu, přesněji v adresáři, kde jsou uloženy classes. Pokud watchdog zjistí modifikaci v deploy, potom reaguje tím, že smaže dotyčné cache. Watchdog se vytváři a registruje s inicializací aplikace ve WebAppContextListener(u).

Nakonec uvádím ještě zjednodušenou utilitovou třídu pro náš konkrétní případ :

public class HotSwapTools {
  public static void afterHotswapClasses() {
    clearBeanElResolver();
    java.beans.Introspector.flushCaches();
    PropertyUtils.clearDescriptors();
    HotSwapTools.clearJbossElMethodCache();
  }

  private static void clearJbossElMethodCache()  {
    Field cacheField = getField(org.jboss.el.util.ReflectionUtil.class"methodCache");
    cacheField.setAccessible(true);
    Object cache = cacheField.get(null);
    Method m = cache.getClass().getMethod("clear");
    m.invoke(cache);
  }

  private static boolean clearBeanElResolver() {
    boolean result;
    BeanELResolver beanELResolver = getSeamBeanELResover();
    result = doCleanBeanElResolver(beanELResolver);
    result = doCleanBeanElResolver(ELUtils.BEAN_RESOLVER) && result;
    return result;
  }

  private static BeanELResolver getSeamBeanELResover() {
    CompositeELResolver compositeElResolver = (CompositeELResolver) EL.EL_RESOLVER;

    Field elResolvesField = getField(CompositeELResolver.class"resolvers");
    elResolvesField.setAccessible(true);
    ELResolver elResolvers[] = (ELResolver[]) elResolvesField.get(compositeElResolver);

    for (ELResolver elResolver: elResolvers) {
      if (elResolver instanceof BeanELResolver)
        return (BeanELResolver) elResolver;
    }
  }

  private static boolean doCleanBeanElResolver(BeanELResolver beanELResolver) {
    boolean result = true;

    try {
      Field cacheField = getField(beanELResolver.getClass(), "cache");
      cacheField.setAccessible(true);
      Object cache = cacheField.get(beanELResolver);
      try {
        Method m = cache.getClass().getMethod("clear");
        m.invoke(cache);
      }
      catch (NoSuchMethodException e){
        Class cacheClass = HotSwapTools.class.getClassLoader().loadClass("javax.el.BeanELResolver$ConcurrentCache");
        Constructor con = cacheClass.getConstructor(int.class);
        con.setAccessible(true);
        Object cacheInstance = con.newInstance(100);
        cacheField.set(beanELResolver, cacheInstance);
      }

    }
    catch (NoSuchFieldException ee) {
      Field props = getField(beanELResolver.getClass(), "properties");
      props.setAccessible(true);
      Object cache = props.get(beanELResolver);
      Method m = cache.getClass().getMethod("clear");
      m.invoke(cache);
    }
  }
  private static Field getField(Class clazz, String name) throws NoSuchFieldException {
    if (clazz == Object.class) {
      throw new NoSuchFieldException();
    }

    try {
      return clazz.getDeclaredField(name);
    }
    catch (Exception e){
    }
    return getField(clazz.getSuperclass(), name);
  }
}

10 komentářů:

  1. Kdysi jsem mel podobny problem. Myslim ze neni nutne pouzivat DCEVM, staci zajistit aby se pro kompilaci v Eclipse a pres Maven/Ant pouzil stejny kompilator. Kompilator v Eclipse je velmi dobry - staci ho nakonfigurovat pro pouziti v Mavenu.

    OdpovědětVymazat
  2. Tento komentář byl odstraněn autorem.

    OdpovědětVymazat
  3. HotSwap je zalezitost JVM, s tim nema kompilator nic spolecneho. Standardni hotswap v eclipse samozrejme funguje, ale nezabere pri slozitejsich upravach trid.

    OdpovědětVymazat
  4. Je mozne ze si to spatne pamatuju (viz "kdysi") ale rozdil mezi tim urcite je.

    Mozna ze nestaci pouzit Eclipsi kompilator ale musi se pouzit Eclipsi JVM i v runtime.
    Jde o to ze je tezke hotswapovat bytecode udelany v Oracle javac za bytecode udelany v Eclipse kompilatoru. Jednodusi je hotwsapovat bytecode z Eclipse za bytecode z Eclipse.

    Kazdopadne tohle reseni v te dobe nebylo tak dobre jako JRebel. Ale bylo jednoduche a zadarmo.

    OdpovědětVymazat
  5. Eclipsi JVM , to existuje? Nemate nekde odkaz na to reseni?

    OdpovědětVymazat
  6. Problem je zminen napr. tady:

    http://stackoverflow.com/questions/538261/how-do-i-get-java-hot-code-replacement-working-in-jboss

    nebo tady:
    http://blog.boruvka.net/2007/07/eclipse-nefunkn-hot-code-replace.html

    Omlouvam se za pripadne dezinformace (Eclipsi JVM asi neexistuje ;) ). dekuji za clanek.

    OdpovědětVymazat
  7. Dekuji, hlavne za ten druhy odkaz. Jak jsem uz psal, hotswap jednoduchych zmen neni v eclipse problem. Ale pokud chce clovek psat kod a nerestartovat aplikaci kazdych 10 minut pri pridani nove metody, tak musi sahnout po DCEVM nebo JRebel.

    OdpovědětVymazat
  8. Pekny clanek. Zajimava je tam i podpora pro Mixins (http://ssw.jku.at/dcevm/mixins/) Sice to nejsou uplne plnotucne Mixins, jako treba ve Scale, protoze z toho demicka, co tam maji vypada, ze nejde primixovat vice stejnych metod a pak je chainovat, ale i tak to vypada pekne.

    OdpovědětVymazat
  9. Dovolim si jen poznamenat, ze celkove naklady na toto reseni byly zcela jiste vetsi nez kdybyste si koupil JRebel a ziskal tim podporu obcerstvovani i pro Hibernate, Spring a spoustu dalsich frameworku. Vzdycky me prekvapuje snaha lidi usetrit par penez za licence. Je to podobne jako u IntelliJ IDEA.

    OdpovědětVymazat
  10. Dovolim si oponovat. Nejprve musim na rovinu rict, ze jsem nebyl nijak motivovany tim, abych usetril. Tu vetu o prazdne kapse berte s rezervou... Nicmene pocitejme. Stravil jsem nad tim cca 10 hodin prace, v teamu mame 4 lidi, licence jrebel stoji $365 per user per 1 year. Pokud nas projekt pojede dalsich 5 let ve 4 lidech, potom je to $(20*365) = $7300. Nejakych 150.000. Pokud mate na hodinu 15.000, tak Vam rozumim. Dalsi motivaci bylo moje presvedceni o tom, ze kvalitni Hotswap patri do zakladniho JVM a tyhle produkty se tak trochu prizivuji na nedostatcich zakladniho JVM. Jak je videt, funkcni reseni zdarma neni ani nijak slozite...

    OdpovědětVymazat