5

I am using Freemarker and DCEVM+HotSwapManager agent. This basically allows me to hotswap classes even when adding/removing methods.

Everything works like charm until Freemarker uses hotswapped class as model. It's throwing freemarker.ext.beans.InvalidPropertyException: No such bean property on me even though reflection shows that the method is there (checked during debug session).

I am using

final Method clearInfoMethod = beanWrapper.getClass().getDeclaredMethod("removeIntrospectionInfo", Class.class);
clearInfoMethod.setAccessible(true);
clearInfoMethod.invoke(clazz);

to clear the cache, but it does not work. I even tried to obtain classCache member field and clear it using reflection but it does not work too.

What am I doing wrong? I just need to force freemarker to throw away any introspection on model class/classes he has already obtained.

Is there any way?

UPDATE

Example code

Application.java

// Application.java
public class Application
{
    public static final String TEMPLATE_PATH = "TemplatePath";
    public static final String DEFAULT_TEMPLATE_PATH = "./";

    private static Application INSTANCE;
    private Configuration freemarkerConfiguration;
    private BeansWrapper beanWrapper;

    public static void main(String[] args)
    {
        final Application application = new Application();
        INSTANCE = application;
        try
        {
            application.run(args);
        }
        catch (InterruptedException e)
        {
            System.out.println("Exiting");
        }
        catch (IOException e)
        {
            System.out.println("IO Error");
            e.printStackTrace();
        }
    }

    public Configuration getFreemarkerConfiguration()
    {
        return freemarkerConfiguration;
    }

    public static Application getInstance()
    {
        return INSTANCE;
    }

    private void run(String[] args) throws InterruptedException, IOException
    {
        final String templatePath = System.getProperty(TEMPLATE_PATH) != null
                ? System.getProperty(TEMPLATE_PATH)
                : DEFAULT_TEMPLATE_PATH;

        final Configuration configuration = new Configuration();
        freemarkerConfiguration = configuration;

        beanWrapper = new BeansWrapper();
        beanWrapper.setUseCache(false);
        configuration.setObjectWrapper(beanWrapper);
        try
        {
            final File templateDir = new File(templatePath);
            configuration.setTemplateLoader(new FileTemplateLoader(templateDir));
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }

        final RunnerImpl runner = new RunnerImpl();
        try
        {
            runner.run(args);
        }
        catch (RuntimeException e)
        {
            e.printStackTrace();
        }
    }

    public BeansWrapper getBeanWrapper()
    {
        return beanWrapper;
    }
}

RunnerImpl.java

// RunnerImpl.java
public class RunnerImpl implements Runner
{
    @Override
    public void run(String[] args) throws InterruptedException
    {
        long counter = 0;
        while(true)
        {
            ++counter;
            System.out.printf("Run %d\n", counter);
//          Application.getInstance().getFreemarkerConfiguration().setObjectWrapper(new BeansWrapper());
            Application.getInstance().getBeanWrapper().clearClassIntrospecitonCache();
            final Worker worker = new Worker();
            worker.doWork();
            Thread.sleep(1000);
        }
    }

Worker.java

// Worker.java
public class Worker
{
    void doWork()
    {
        final Application application = Application.getInstance();
        final Configuration freemarkerConfiguration = application.getFreemarkerConfiguration();

        try
        {
            final Template template = freemarkerConfiguration.getTemplate("test.ftl");
            final Model model = new Model();
            final PrintWriter printWriter = new PrintWriter(System.out);

            printObjectInto(model);
            System.out.println("-----TEMPLATE MACRO PROCESSING-----");
            template.process(model, printWriter);
            System.out.println();
            System.out.println("-----END OF PROCESSING------");
            System.out.println();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        catch (TemplateException e)
        {
            e.printStackTrace();
        }
    }

    private void printObjectInto(Object o)
    {
        final Class<?> aClass = o.getClass();
        final Method[] methods = aClass.getDeclaredMethods();
        for (final Method method : methods)
        {
            System.out.println(String.format("Method name: %s, public: %s", method.getName(), Modifier.isPublic(method.getModifiers())));
        }
    }
}

Model.java

// Model.java    
public class Model
{
    public String getMessage()
    {
        return "Hello";
    }

    public String getAnotherMessage()
    {
        return "Hello World!";
    }
}

This example does not work at all. Even changing BeansWrapper during runtime won't have any effect.

Martin Macak
  • 3,507
  • 2
  • 30
  • 54
  • Assuming you call the method on the `BeansWrapper` that's actually in use, it should work. But, `removeFromClassIntrospectionCache(Class clazz)` is a public method, why are you calling it like that? If it's an old FreeMarker, try an update first. – ddekany Feb 25 '15 at 19:04
  • @ddekany: I'm facing the same issue as Martin. I'm using the 2.3.19 version so upgrade should do the trick. However the Javadoc comment for the `removeFromClassIntrospectionCache` puzzles me: "`...If the class will be still used, the cache entry will be silently re-created`" - does that mean that when a template cache contains mapping for the class in question the introspection would not get the reloaded version of it? Thanks! – plesatejvlk Feb 26 '15 at 07:53
  • I am cleaning the only BeansWrapper which I set to freemarker Configuration object. Please see an update with the example. – Martin Macak Feb 26 '15 at 11:45
  • @plesatejvlk: The point of that sentence is the second half of it: "so this isn't a dangerous operation". So that thing doesn't affect you in this issue. – ddekany Feb 26 '15 at 18:18
  • 1
    Where you dump the methods, what's if you use `beanInfo = java.beans.Introspector.getBeanInfo(aClass)` instead of direct reflection, then list the methods with `beanInfo.getMethodDescriptors()`. Also the properties with `beanInfo.getPropertyDescriptors()`, because I guess you are actually accessing properties in the template (haven't seen it yet). Maybe the problem is that DCEVM doesn't to clean Java's `BeanIntrospector` cache. Just a wild guess. – ddekany Feb 26 '15 at 18:23
  • Checked right now and BeanInfo for the particular Model class is updated just after the DCEVM hotswap. – Martin Macak Feb 26 '15 at 19:40
  • UPDATE!!!!! I also checked the propertyDescriptors to be sure and they are NOT updated. Might that be a problem? – Martin Macak Feb 26 '15 at 19:45
  • 1
    UPDATE 2: Calling Introspector.flushCaches solved the problem. @ddekany can you please post a regular answer so you get the credit for that? – Martin Macak Feb 26 '15 at 19:50
  • I'm happy that it has worked (but update FreeMarker anyways...) Answer posted. – ddekany Feb 26 '15 at 23:16

1 Answers1

3

BeansWrapper (and DefaultObjectWrapper's, etc.) introspection cache relies on java.beans.Introspector.getBeanInfo(aClass), not on reflection. (That's because it treats objects as JavaBeans.) java.beans.Introspector has its own internal cache, so it can return stale information, and in that case BeansWrapper will just recreate its own class introspection data based on that stale information. As of java.beans.Introspector's caching, it's in fact correct, as it builds on the assumption that classes in Java are immutable. If something breaks that basic rule, it should ensure that java.beans.Introspector's cache is cleared (and many other caches...), or else it's not just FreeMarker that will break. At JRebel for example they made a lot of effort to clear all kind of caches. I guess DCEVM doesn't have the resources for that. So then, it seems you have to call Introspector.flushCaches() yourself.

Update: For a while (Java 7, maybe 6) java.beans.Introspector has one cache per thread group, so you have call flushCaches() from all thread groups. And this all is actually implementation detail that, in principle, can change any time. And sadly, the JavaDoc of Introspector.flushCaches() doesn't warn you...

ddekany
  • 29,656
  • 4
  • 57
  • 64
  • I just discovered really weird thing. I deployed this solution to our application that runs on WLS and it stopped working. I discovered that Introspector returns another values on theread which is called by HotSwapAgent and the thread from WLS WorkerThreadPool. Any idea why is that happening and how to fix this? – Martin Macak Feb 27 '15 at 13:53
  • That's interresting. I tried a solution based on your observations and you were right once again. Was your conclusion based on observations or are there any materials to that? I recently opened another SO question for that at http://stackoverflow.com/questions/28768348/strange-java-beans-introspector-behavior-on-weblogic-with-dcevm-and-hotswapagent/28796646#28796646 – Martin Macak Mar 01 '15 at 17:33
  • I have just looked at the `Introspector` source code. – ddekany Mar 01 '15 at 17:45