Skip to content

JavassistWithJCP

Igor Maznitsa edited this page Dec 31, 2023 · 6 revisions

Introduction

Javassist stands out as a robust Java framework renowned for its capability to dynamically alter bytecode and perform on-the-fly compilation of Java-like language sources. Its effectiveness is evident through its widespread adoption in prominent tools such as JRebel and XRebel. Now, let's delve into an illustrative example showcasing the practical usage of Javassist.

public void insertTimingIntoMethod(String targetClass, String targetMethod) throws NotFoundException, CannotCompileException, IOException {
      Logger logger = Logger.getLogger("Javassist");
      final String targetFolder = "./target/javassist";
 
      try {
         final ClassPool pool = ClassPool.getDefault();
         pool.appendClassPath(new LoaderClassPath(getClass().getClassLoader()));
         final CtClass compiledClass = pool.get(targetClass);
         final CtMethod method = compiledClass.getDeclaredMethod(targetMethod);
         method.addLocalVariable("startMs", CtClass.longType);
         method.insertBefore("startMs = System.currentTimeMillis();");
         method.insertAfter("{final long endMs = System.currentTimeMillis();" +
            "iterate.jz2011.codeinjection.javassist.PerformanceMonitor.logPerformance(\"" +
            targetMethod + "\",(endMs-startMs));}");
 
         compiledClass.writeFile(targetFolder);
         logger.info(targetClass + "." + targetMethod +
               " has been modified and saved under " + targetFolder);
      } catch (NotFoundException e) {
         logger.warning("Failed to find the target class to modify, " +
               targetClass + ", verify that it ClassPool has been configured to look " +
               "into the right location");
      }
   }

This represents a straightforward example of utilizing Javassist. However, it illustrates the potential for errors or typos within this form of "string-programming." Imagine having to compose 500 to 1000 lines of such "string" code – the likelihood of mistakes increases significantly. This situation can be exasperating, especially considering that errors are primarily detected during runtime.

How JCP can help

In the JCP version 6.0, two new functions have been introduced to simplify "string" programming within Javassist: evalfile() and str2java().

  • str evalfile(str): This function facilitates the preprocessing of an external file specified by a string name and retrieves the preprocessing result as a string.
  • str str2java(str,bool): Allows for the transformation of a string into a Java-compatible format, while also segmenting the string into well-formed lines.

The Problem

Let's consider a scenario where there's a necessity to insert a prefix into a class method for the purpose of intercepting its execution and returning different data. The method structure is as follows:

public static Class<?> prepare() throws Exception {
      final ClassPool cp = ClassPool.getDefault();
      cp.appendClassPath(new LoaderClassPath(Main.class.getClassLoader()));
      final CtClass ctClazz = cp.get(Main.class.getPackage().getName()+".ExtProcessor");
      final CtMethod method1 = ctClazz.getDeclaredMethod("extractExtension");
      method1.insertBefore(
      +"if (!$2) {return $1;} else { final int index = $1.lastIndexOf('.');"
      +"String result;"
      +"if (index < 0) {result = \"\";} else { result = $1.substring(index + 1);}"
      +"return result;}"
      );
      ctClazz.writeFile();
      return ctClazz.toClass();
  }

The approach appears functional, but comprehending its logic can be challenging, and editing might introduce errors and typos. Therefore, let's explore how JCP can assist us even in simpler scenarios.

To enhance clarity and ease of editing, we can extract the method body into an external class. We'll create an additional class within the same package named extractExtensionMethod.java and house within it all the necessary code.

//#excludeif true
//#-
public class _extractExtensionMethod {
  public String extractExtension(final String ____arg1, final boolean ____arg2) {
    final String fileName = ____arg1;
    final boolean allowDynamicCode = ____arg2;
//#+
    //$String fileName=$1;
    //$boolean allowDynamicCode=$2;
    if (!allowDynamicCode) {
      return /*$"$1;"$*//*-*/ "";
    }
    else {
      final int index = fileName.lastIndexOf('.');
      final String result;
      if (index < 0) {
        result = "";
      }
      else {
        result = fileName.substring(index + 1);
      }
      return result;
    }
//#-
  }
}
//+

It might seem unusual if you're new to working with JCP. However, there's a crucial advantage here: approximately 80% of your code within the class can undergo refactoring and be controlled by the IDE, reducing the likelihood of typos.

The class contains an active //#excludeif directive, ensuring it's excluded from the preprocessing result. Unnecessary sections of the class are encompassed by //#-..//#+, which are effectively trimmed from the preprocessing output. Leveraging the //-// trick enables obtaining $1 as the resulting output after preprocessing.

Adapt our main class to use the external one

We simply eliminate all our "string-programming" code and replace it with the result of preprocessing for the extractExtensionMethod.java class. By setting the second parameter of str2java() to true, the resultant output won't just be escaped; it will also be structured as a string concatenation, ready to be utilized as an argument in Java sources.

public static Class<?> prepare() throws Exception {
      final ClassPool cp = ClassPool.getDefault();
      cp.appendClassPath(new LoaderClassPath(Main.class.getClassLoader()));
      final CtClass ctClazz = cp.get(Main.class.getPackage().getName()+".ExtProcessor");
      final CtMethod method1 = ctClazz.getDeclaredMethod("extractExtension");
      method1.insertBefore(
/*$str2java(evalfile("_extractExtensionMethod.java"),true)$*//*-*/""
      );
      ctClazz.writeFile();
      return ctClazz.toClass();
  }

now looks much better

Result

Let's initiate the preprocessing of the example and examine the output. The generated file will contain a prepare() method structured as follows:

  public static Class<?> prepare() throws Exception {
      final ClassPool cp = ClassPool.getDefault();
      cp.appendClassPath(new LoaderClassPath(Main.class.getClassLoader()));
      final CtClass ctClazz = cp.get(Main.class.getPackage().getName()+".ExtProcessor");
      final CtMethod method1 = ctClazz.getDeclaredMethod("extractExtension");
      method1.insertBefore(
"    String fileName=$1;\n"
+"    boolean allowDynamicCode=$2;\n"
+"    if (!allowDynamicCode) {\n"
+"      return $1;\n"
+"    }\n"
+"    else {\n"
+"      final int index = fileName.lastIndexOf('.');\n"
+"      final String result;\n"
+"      if (index < 0) {\n"
+"        result = \"\";\n"
+"      }\n"
+"      else {\n"
+"        result = fileName.substring(index + 1);\n"
+"      }\n"
+"      return result;\n"
+"    }\n"
      );
      ctClazz.writeFile();
      return ctClazz.toClass();
  }

This method appears significantly improved, reducing manual effort. Reading and editing become much more manageable, substantially decreasing the probability of typos. Additionally, even the spaces from the original file are injected. Furthermore, there's a function STR trimlines(STR) available since version 6.1.2, allowing exclusion of spaces and empty lines.

Sources of the example

The Zipped Demo project sources can be downloaded from here. It is a maven project and after unzipping it can be started with maven command

 mvn clean install exec:java