package org.hotswap.agent.plugin.zk; import org.hotswap.agent.annotation.*; import org.hotswap.agent.command.Command; import org.hotswap.agent.command.ReflectionCommand; import org.hotswap.agent.command.Scheduler; import org.hotswap.agent.javassist.*; import org.hotswap.agent.logging.AgentLogger; import org.hotswap.agent.util.PluginManagerInvoker; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; /** * ZK framework - http://www.zkoss.org/. * <p/> * <p>Plugin:<ul> * <li>Plugin initialization is triggered after DHtmlLayoutServlet.init() method in servlet classloader</li> * <li>Change default value for library properties of ZK caches * (org.zkoss.web.classWebResource.cache=false, org.zkoss.zk.WPD.cache=false, org.zkoss.zk.WCS.cache=false, * zk-dl.annotation.cache=false). App can override this setting by explicitly set value to true in zk.xml</li> * <li>Clear Labels cache after change of any .properties file</li> * <li>Clear org.zkoss.zel.BeanELResolver caches after any class change</li> * </ul> * <p/>All is invoked via reflection, no ZK lib direct dependency. * * @author Jiri Bubnik */ @Plugin(name = "ZK", description = "ZK Framework (http://www.zkoss.org/). Change library properties default values to disable" + "caches, maintains Label cache and bean resolver cache.", testedVersions = {"6.5.2"}, expectedVersions = {"5x", "6x", "7x?"}) public class ZkPlugin { private static AgentLogger LOGGER = AgentLogger.getLogger(ZkPlugin.class); // clear labels cache ReflectionCommand refreshLabels = new ReflectionCommand(this, "org.zkoss.util.resource.Labels", "reset"); @Init Scheduler scheduler; @Init ClassLoader appClassLoader; /** * Initialize the plugin after DHtmlLayoutServlet.init() method. */ @OnClassLoadEvent(classNameRegexp = "org.zkoss.zk.ui.http.DHtmlLayoutServlet") public static void layoutServletCallInitialized(CtClass ctClass) throws NotFoundException, CannotCompileException { CtMethod init = ctClass.getDeclaredMethod("init"); init.insertAfter(PluginManagerInvoker.buildInitializePlugin(ZkPlugin.class)); LOGGER.debug("org.zkoss.zk.ui.http.DHtmlLayoutServlet enahnced with plugin initialization."); } /** * Default values of caches in development mode. * <p/> * Note, that this is a little bit aggressive, but the user may override this by providing explicit value in zk.xml */ @OnClassLoadEvent(classNameRegexp = "org.zkoss.lang.Library") public static void defaultDisableCaches(ClassPool classPool, CtClass ctClass) throws NotFoundException, CannotCompileException { LOGGER.debug("org.zkoss.lang.Library enhanced to replace property '*.cache' default value to 'false'."); CtMethod m = ctClass.getDeclaredMethod("getProperty", new CtClass[]{classPool.get("java.lang.String")}); // see http://books.zkoss.org/wiki/ZK%20Configuration%20Reference/zk.xml/The%20Library%20Properties defaultLibraryPropertyFalse(m, "org.zkoss.web.classWebResource.cache"); defaultLibraryPropertyFalse(m, "org.zkoss.zk.WPD.cache"); defaultLibraryPropertyFalse(m, "org.zkoss.zk.WCS.cache"); // see. http://zk.datalite.cz/wiki/-/wiki/Main/DLComposer++-+MVC+Controller#section-DLComposer++-+MVC+Controller-ImplementationDetails defaultLibraryPropertyFalse(m, "zk-dl.annotation.cache"); } private static void defaultLibraryPropertyFalse(CtMethod m, String setPropertyFalse) throws CannotCompileException { m.insertAfter("if (_props.get(key) == null && \"" + setPropertyFalse + "\".equals(key)) return \"false\";"); } @OnResourceFileEvent(path = "/", filter = ".*.properties") public void refreshProperties() { // unable to tell if properties are ZK labels or not for custom label locator. // however Label refresh is very cheep, do it for any properties. scheduler.scheduleCommand(refreshLabels); } /** * BeanELResolver contains reflection cache (bean properites). */ @OnClassLoadEvent(classNameRegexp = "org.zkoss.zel.BeanELResolver") public static void beanELResolverRegisterVariable(CtClass ctClass) throws CannotCompileException { String registerThis = PluginManagerInvoker.buildCallPluginMethod(ZkPlugin.class, "registerBeanELResolver", "this", "java.lang.Object"); for (CtConstructor constructor : ctClass.getDeclaredConstructors()) { constructor.insertAfter(registerThis); } ctClass.addMethod(CtNewMethod.make("public void __resetCache() {" + " this.cache = new org.zkoss.zel.BeanELResolver.ConcurrentCache(CACHE_SIZE); " + "}", ctClass)); LOGGER.debug("org.zkoss.zel.BeanELResolver - added method __resetCache()."); } Set<Object> registeredBeanELResolvers = Collections.newSetFromMap(new WeakHashMap<Object, Boolean>()); public void registerBeanELResolver(Object beanELResolver) { registeredBeanELResolvers.add(beanELResolver); } /** * BeanELResolver contains reflection cache (bean properites). */ @OnClassLoadEvent(classNameRegexp = "org.zkoss.bind.impl.BinderImpl") public static void binderImplRegisterVariable(CtClass ctClass) throws CannotCompileException { String registerThis = PluginManagerInvoker.buildCallPluginMethod(ZkPlugin.class, "registerBinderImpl", "this", "java.lang.Object"); for (CtConstructor constructor : ctClass.getDeclaredConstructors()) { constructor.insertAfter(registerThis); } ctClass.addMethod(CtNewMethod.make("public void __resetCache() {" + " this._initMethodCache = new org.zkoss.util.CacheMap(600,org.zkoss.util.CacheMap.DEFAULT_LIFETIME); " + " this._commandMethodCache = new org.zkoss.util.CacheMap(600,org.zkoss.util.CacheMap.DEFAULT_LIFETIME); " + " this._globalCommandMethodCache = new org.zkoss.util.CacheMap(600,org.zkoss.util.CacheMap.DEFAULT_LIFETIME); " + "}", ctClass)); LOGGER.debug("org.zkoss.bind.impl.BinderImpl - added method __resetCache()."); } Set<Object> registerBinderImpls = Collections.newSetFromMap(new WeakHashMap<Object, Boolean>()); public void registerBinderImpl(Object binderImpl) { registerBinderImpls.add(binderImpl); } // invalidate BeanELResolver caches after any class reload (it is cheap to rebuild from reflection) @OnClassLoadEvent(classNameRegexp = ".*", events = LoadEvent.REDEFINE) public void invalidateClassCache() throws Exception { scheduler.scheduleCommand(invalidateClassCache); } // schedule refresh in case of multiple class redefinition to merge command executions private Command invalidateClassCache = new Command() { @Override public void executeCommand() { LOGGER.debug("Refreshing ZK BeanELResolver and BinderImpl caches."); try { Method beanElResolverMethod = resolveClass("org.zkoss.zel.BeanELResolver").getDeclaredMethod("__resetCache"); for (Object registeredBeanELResolver : registeredBeanELResolvers) { LOGGER.trace("Invoking org.zkoss.zel.BeanELResolver.__resetCache on {}", registeredBeanELResolver); beanElResolverMethod.invoke(registeredBeanELResolver); } Method binderImplMethod = resolveClass("org.zkoss.bind.impl.BinderImpl").getDeclaredMethod("__resetCache"); for (Object registerBinderImpl : registerBinderImpls) binderImplMethod.invoke(registerBinderImpl); Field afterComposeMethodCache = resolveClass("org.zkoss.bind.BindComposer").getDeclaredField("_afterComposeMethodCache"); afterComposeMethodCache.setAccessible(true); ((Map)afterComposeMethodCache.get(null)).clear(); } catch (Exception e) { LOGGER.error("Error refreshing ZK BeanELResolver and BinderImpl caches.", e); } } }; private Class<?> resolveClass(String name) throws ClassNotFoundException { return Class.forName(name, true, appClassLoader); } }