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);
  }
}