Saturday, April 7, 2012

Class Reloading-2 using AgentSmith

Class Reloading using AgentSmith

AgentSmith uses jvm HotSwap method for class reloading. The project is simple and consists of a few classes. The application works by java instrumentation API. We will break down the working of each of the components in it and see how it all fits together to enable automatic class reloading.


Setup:

This is what worked for me. SVN checkout from the below repository. https://svn.java.net/svn/agentsmith~svn/tags/smith-1.0
Compile using
ant dist

This will create smith-1.0.jar file. This jar file needs to be used to instrument the jvm of tomcat which will be running the web application. The following code does exactly that:
Edit catalina.sh or catalina.bat to add the following to JAVA_OPTS variables.

-javaagent:E:/samarjit/tomcat-6.0.35/lib/smith-1.0.jar=classes=E:/samarjit/tomcat-6.0.35/webapps/sam-2.3.1/WEB-INF/classes,jars=E:/samarjit/tomcat-6.0.35/webapps/sam-2.3.1/WEB-INF/lib,period=1000,loglevel=INFO



If you start the tomcat now it will show the debug messages, which will show the path AgentSmith is looking for class changes E:/samarjit/tomcat-6.0.35/webapps/sam-2.3.1/WEB-INF/classes and library changes.in E:/samarjit/tomcat-6.0.35/webapps/sam-2.3.1/WEB-INF/lib.

To test if it really works deploy a webapplication in tomcat outside eclipse. Now recompile some class file after modifying some contents of a method in it. Then invoke that servlet. You can instantly see the effect.

Some excerpts form Smith.java

public static void premain(String agentArgs, Instrumentation inst) {
  System.out.println("Premain()");
  initialize(agentArgs, inst);
 }


The portion that starts off the monitor is here:

/**
  * Creates and starts a new Smith agent. Please note that periods smaller than
  * 500 (milliseconds) won't be considered.
  * 
  * @param inst
  *          the instrumentation implementation
  * @param args
  *          the {@link SmithArgs} instance
  */
 public Smith(Instrumentation inst, SmithArgs args) {
  this.inst = inst;
  this.classFolder = args.getClassFolder();
  this.jarFolder = args.getJarFolder();
  int monitorPeriod = MONITOR_PERIOD_MIN_VALUE;
  if (args.getPeriod() > monitorPeriod) {
   monitorPeriod = args.getPeriod();
  }
  log.setUseParentHandlers(false);
  log.setLevel(args.getLogLevel());
  ConsoleHandler consoleHandler = new ConsoleHandler();
  System.out.println("Log level:"+args.getLogLevel());
  consoleHandler.setLevel(args.getLogLevel());
  log.addHandler(consoleHandler);

  service = Executors.newScheduledThreadPool(2);

  FileMonitor fileMonitor = new FileMonitor(classFolder, "class");
  fileMonitor.addModifiedListener(this);
  service.scheduleWithFixedDelay(fileMonitor, 0, monitorPeriod,
    TimeUnit.MILLISECONDS);

  if (jarFolder != null) {
   JarMonitor jarMonitor = new JarMonitor(jarFolder);
   jarMonitor.addJarModifiedListener(this);
   service.scheduleWithFixedDelay(jarMonitor, 0,
                                        monitorPeriod,
     TimeUnit.MILLISECONDS);
  }

  log.info("Smith: watching class folder: " + classFolder);
  log.info("Smith: watching jars folder: " + jarFolder);
  log.info("Smith: period between checks (ms): " + monitorPeriod);
  log.info("Smith: log level: " + log.getLevel());
 }


The most important that does the HotSwap is here: There is a call to inst.redefineClasses(...) which does the trick.

/**
  * Redefines the specified class
  * 
  * @param className
  *          the class name to redefine
  * @param event
  *         the event which contains the info to access the modified class
  *          files
  * @throws IOException
  *           if the inputstream is someway unreadable
  * @throws ClassNotFoundException
  *           if the class name cannot be found
  * @throws UnmodifiableClassException
  *           if the class is unmodifiable
  */
 protected void redefineClass(String className, EventObject event) {
  Class[] loadedClasses = inst.getAllLoadedClasses();
  for (Class clazz : loadedClasses) {
   if (clazz.getName().equals(className)) {
    try {
       ClassDefinition definition = 
                            new ClassDefinition(clazz, getByteArrayOutOf(event));
       inst.redefineClasses( new ClassDefinition[] { definition });
       if (log.isLoggable(Level.FINE)) {
         log.log(Level.FINE, "Redefined: "                                                + clazz.getName());
       }
      } catch (Exception e) {
       log.log(Level.SEVERE, "error", e);
      }
   }
  }
 }


Once this class redefinition is done, the updated class automatically gets reflected when invoked. Till here all the changes done are framework agnostic. But if you are using a framework like struts, you will probably need to update the framework related configurations.

No comments:

Post a Comment