Saturday, April 7, 2012

Class reloading-3 using Apache commons-jci-fam

Apache commons-jci-fam

This is a popular apache project based on JSR http://commons.apache.org/jci/usage.html
This project JCI is a (java compiler interface) and FAM is (FilesystemAlterationMonitor)

 The most important parts of the code that we are going to use are as below. This does the trick of class reloading.
ReloadingClassLoader classloader = new ReloadingClassLoader(this.getClass().getClassLoader());
ReloadingListener listener = new ReloadingListener();

listener.addReloadNotificationListener(classloader);

FilesystemAlterationMonitor fam = new FilesystemAlterationMonitor();
fam.addListener(directory, listener);
fam.start();



It took me a while to set up their example which I downloaded. I don't remember where I downloaded the example sources from. But running it was interesting. All the examples ran without a worry. Except for this ServerPageServlet.java which required one tricky configuration which I found out the hard way.
Well the curious and excited ones just checkout project from SVN repo http://code.google.com/p/class-reloading-test/source/browse/#svn/trunk/samjci.
Configure web.xml
 
  <servlet>
        <servlet-name>samjciservlet</servlet-name>
        <servlet-class>org.apache.commons.jci.examples.serverpages.ServerPageServlet</servlet-class>
        <init-param>serverpagesDir</param-name>
        <param-value>jsptest</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
Excerpts from ServerPageServlet.java showing some important lines that helps in compilation and class reloading.
This defines the directory for monitoring of jsp files that will need to be loaded.
final File serverpagesDir = new File(getServletContext().getRealPath("/") + getInitParameter("serverpagesDir"));
This store class actually stores the class in byte array form.

final TransactionalResourceStore store = new TransactionalResourceStore(new MemoryResourceStore()) {
    public void onStart() {..}

    /**
    * Gets called soon after onStart() by compiler. Conpiler writes the compiled class bytes in the store.
    * Soon after this onStop() gets called in which the class bytes are picked up and
    * a new class instance is created
    */
    public void write(String pResourceName, byte[] pResourceData) {
     ...
    }

    public void onStop() {
          ....
          final Class clazz = classloader.loadClass(clazzName);

          if (!HttpServlet.class.isAssignableFrom(clazz)) {
                log(clazzName + " is not a servlet");
                continue;
          }
          // create new instance of jsp page
          final HttpServlet servlet = (HttpServlet) clazz.newInstance();
          ....
    }
    
}


Although compilation is out of scope for this blog, but just for the sake of completeness, I am writing about it here.
jspListener = new CompilingListener(new JavaCompilerFactory().createCompiler("eclipse"), store) {

            private final JspGenerator transformer = new JspGenerator();
            private final Map sources = new HashMap();
            private final Set resourceToCompile = new HashSet();

            public void onStart(FilesystemAlterationObserver pObserver) {
                super.onStart(pObserver);

                resourceToCompile.clear();
            }


            public void onFileChange(File pFile) {
                if (pFile.getName().endsWith(".ss")) {
                    final String resourceName = ConversionUtils.stripExtension(getSourceNameFromFile(observer, pFile)) + ".java";

                    log("Updating " + resourceName);
                    byte ar[] = transformer.generateJavaSource(resourceName, pFile);
                    sources.put(resourceName, ar);
                    System.out.println(new String(ar));
                    resourceToCompile.add(resourceName);
                }
                super.onFileChange(pFile);
            }


            public void onFileCreate(File pFile) {
                if (pFile.getName().endsWith(".ss")) {
                    final String resourceName = ConversionUtils.stripExtension(getSourceNameFromFile(observer, pFile)) + ".java";

                    log("Creating " + resourceName);

                    sources.put(resourceName, transformer.generateJavaSource(resourceName, pFile));

                    resourceToCompile.add(resourceName);
                }
                super.onFileCreate(pFile);
            }


            public String[] getResourcesToCompile(FilesystemAlterationObserver pObserver) {
                // we only want to compile the jsp pages
                final String[] resourceNames = new String[resourceToCompile.size()];
                resourceToCompile.toArray(resourceNames);
                return resourceNames;
            }


            public ResourceReader getReader( final FilesystemAlterationObserver pObserver ) {
                return new JspReader(sources, super.getReader(pObserver));
            }
        };
        jspListener.addReloadNotificationListener(classloader);
        
        fam = new FilesystemAlterationMonitor();
        fam.addListener(serverpagesDir, jspListener);
        fam.setInterval(3000);
        fam.start();
  

Whenever a new *.ss file gets created OnFileChange() or onFileCreate() gets called, which writes the resource names in resourceToCompile list. Super class of CompilationListener is ReloadingListener. This is part of ReloadingListener.java

public void onStop( final FilesystemAlterationObserver pObserver ) {
        
        
        if (store instanceof Transactional) {
            ((Transactional)store).onStart();
        }

        final boolean reload = isReloadRequired(pObserver);

        if (store instanceof Transactional) {
            ((Transactional)store).onStop();
        }
        
        if (reload) {
            notifyReloadNotificationListeners();
        }
        
        super.onStop(pObserver);
    }

The onStart() method actually just clears various flags of compiler and flags of stores. The real execution starts when onStop() gets called. onStop() method of ReloadingListener has the high level steps of compilation and followed by reload of classes.
It has one method final boolean reload = isReloadRequired(pObserver); which logs the changes resources
  log.debug("created:" + created.size() + " changed:" + changed.size() + " deleted:" + deleted.size() + " resources");, then it calls
  final String[] resourcesToCompile = getResourcesToCompile(pObserver);.
 After this actual compilation happens for each source that got changed
  final CompilationResult result = compiler.compile(resourcesToCompile, reader, transactionalStore); 
At the end it logs the errors and warnings of compilation
  log.debug(errors.length + " errors, " + warnings.length + " warnings"); Once back from isReloadingRequired(), it is sure that class compilation is complete and class bytes are available in the store. The only thing remains is the read those bytes and load them into a class. ....
The following log gets written automatically when the ss.ss file is edited using a notepad. Just hit Ctrl+S after modification and see the result.


created:0 changed:0 deleted:0 resources
check signal
check signal
onStart F:\eclipse\workspace\HTMLProcessor\deploypath\wtpwebapps\samjci\jsptest
onStart F:\eclipse\workspace\HTMLProcessor\deploypath\wtpwebapps\samjci\jsptest
onFileChange F:\eclipse\workspace\HTMLProcessor\deploypath\wtpwebapps\samjci\jsptest\ss.ss
onFileChange F:\eclipse\workspace\HTMLProcessor\deploypath\wtpwebapps\samjci\jsptest\ss.ss
Apr 08, 2012 1:33:14 AM org.apache.catalina.core.ApplicationContext log
INFO: samjciservlet: Updating ss.java
import java.io.PrintWriter;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
public class ss extends HttpServlet {
  protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    final PrintWriter out = response.getWriter();
System.out.println("Custom jsp servlet:"+this.getClass().getName());    out.write("this is ss changed ");
    out.close();
    out.flush();
  }
}

onStop F:\eclipse\workspace\HTMLProcessor\deploypath\wtpwebapps\samjci\jsptest
onStop F:\eclipse\workspace\HTMLProcessor\deploypath\wtpwebapps\samjci\jsptest
created:0 changed:1 deleted:0 resources
created:0 changed:1 deleted:0 resources
1 classes to compile
1 classes to compile
className=ss
className=ss
fileName=ss.java
fileName=ss.java
typeName=ss
typeName=ss
compiling ss.java
...
writing resource ss.class(1314)
0 errors, 1 warnings
0 errors, 1 warnings
reading resource ss.class
reading resource ss.class
org.apache.commons.jci.stores.ResourceStoreClassLoader@18e3b6c[WebappClassLoader
  delegate: false
  repositories:
    /WEB-INF/classes/
----------> Parent Classloader:

reading resource ss.class
org.apache.commons.jci.stores.ResourceStoreClassLoader@18e3b6c[WebappClassLoader
  delegate: false
  repositories:
    /WEB-INF/classes/
----------> Parent Classloader:
org.apache.catalina.loader.StandardClassLoader@12c8b2a
] found class: ss (1314 bytes)
org.apache.commons.jci.stores.ResourceStoreClassLoader@18e3b6c[WebappClassLoader
  delegate: false
  repositories:
    /WEB-INF/classes/
----------> Parent Classloader:
org.apache.catalina.loader.StandardClassLoader@12c8b2a
] found class: ss (1314 bytes)
reading resource javax/servlet/http/HttpServlet.class
reading resource javax/servlet/http/HttpServlet.class
org.apache.commons.jci.stores.ResourceStoreClassLoader@18e3b6c[WebappClassLoader
  delegate: false
  repositories:
    /WEB-INF/classes/
----------> Parent Classloader:
org.apache.catalina.loader.StandardClassLoader@12c8b2a
] loaded from store: ss
org.apache.commons.jci.stores.ResourceStoreClassLoader@18e3b6c[WebappClassLoader
  delegate: false
  repositories:
    /WEB-INF/classes/
----------> Parent Classloader:
org.apache.catalina.loader.StandardClassLoader@12c8b2a
] loaded from store: ss
reading resource java/lang/Object.class
reading resource java/lang/Object.class
Apr 08, 2012 1:37:50 AM org.apache.catalina.core.ApplicationContext log
INFO: samjciservlet: Activating new map of servlets {ss=ss@cb70f0}
...
...
If the newly created and loaded servlet is loaded then the following log comes. The url invoked is http://localhost:6060/samjci/samjci/ss the new servlet getscalled. This line comes from the System.out.println() written dynamically in the servlet "Custom jsp servlet:ss".
request.getpathInfo:/ss
Custom jsp servlet:ss
Apr 08, 2012 1:56:22 AM org.apache.catalina.core.ApplicationContext log
INFO: samjciservlet: Request /samjci/samjci/ss
Apr 08, 2012 1:56:22 AM org.apache.catalina.core.ApplicationContext log
INFO: samjciservlet: Checking for serverpage ss
Apr 08, 2012 1:56:22 AM org.apache.catalina.core.ApplicationContext log
INFO: samjciservlet: Delegating request to ss

Now everything got cleared from the logs. Hope this inspires new ideas. You can find the project in google code SVN repository here http://code.google.com/p/class-reloading-test/source/browse/#svn/trunk/samjci

No comments:

Post a Comment