Sunday, November 2. 2014
Compiling Servlet (part I)
Today's entry is the first post for a new series about compiling java sources in-line and using the resulting classes directly inside the application. I have had a new idea and I need this feature to fully implement that idea (basically I am testing different approaches with different JavaEE technologies). In this first entry a simple compiling servlet is going to read another servlet source class file, compile it, load the resulting class using a ClassLoader and finally invoke that servlet to actually process the current request. Obviously this idea let us store the java sources in another repository instead of packaging them inside the WAR on the file system. I know that this process is quite strange and extravagant but I think it is an interesting feature for projects which need modification of classes without restarting or re-deploying (hot modifications).
In order to compile java sources in-line a compiler is the first thing we need. I decided to use the Eclipse Compiler for Java (ECJ) instead of the bundle tools.jar that comes with my OpenJDK installation. The reason was that I found more examples for ECJ and it has the interesting feature of putting the resulting classes in a memory array (not in files like the normal javac compilation), this way everything remains in memory. Besides the ECJ compiler is used in several well known projects (tomcat for compiling JSPs, eclipse IDE,...) so the project is trustworthy.
In order to compile a java source inside your program using ECJ the following things are needed:
A CompilationUnit, which represents a single source file to compile. In my example the unit is just an object which has the source directly in a char array (as the ECJ compiler requires it).
A CompilerEnvironment, which is a class that locates classes and packages. In my example a simple environment is created, it just returns the in-memory source if the class requested is the class which is being compiled and delegates other elements to a class loader (argument passed in the constructor of the environment object).
A CompilerRequestor, another object that receives the results of the compilation (errors or compiled classes) to do whatever the application wants with them. In my simple example the requestor loads the class in a custom class loader and return a Class instance of the compiled source.
A CustomClassLoader was also added cos I wanted to perform the whole process just in memory (the byte-code is not going to be saved in any file). This way the resulting compiled classes will be directly inserted in this class loader in order to create a Class with them. This class loader is used by the requestor if the compilation is performed successfully.
With the previous classes a compilation of an example java source is done with the following code:
static public void main(String[] args) throws Exception { // define class name, source and the class loader String className = "example.HelloWorld"; String source = "package example;" + "import java.util.*;\n" + "public class HelloWorld {\n" + " static public class Internal {\n" + " public Object sayHello() {\n" + " return \"Hello!\";" + " }\n" + " }\n" + " public static void sayHello() {\n" + " System.out.println(new Internal().sayHello());\n" + " }\n" + "}\n"; ClassLoader cl = Thread.currentThread().getContextClassLoader(); // create the environment INameEnvironment env = new CompilerEnvironment(cl, className, source.toCharArray()); // policy IErrorHandlingPolicy policy = DefaultErrorHandlingPolicies.proceedWithAllProblems(); // options Mapsettings = new HashMap<>(); settings.put(CompilerOptions.OPTION_LineNumberAttribute, CompilerOptions.GENERATE); settings.put(CompilerOptions.OPTION_SourceFileAttribute, CompilerOptions.GENERATE); settings.put(CompilerOptions.OPTION_ReportDeprecation, CompilerOptions.IGNORE); settings.put(CompilerOptions.OPTION_Encoding, "UTF-8"); settings.put(CompilerOptions.OPTION_Source, "1.7"); settings.put(CompilerOptions.OPTION_TargetPlatform, "1.7"); settings.put(CompilerOptions.OPTION_Compliance, "1.7"); CompilerOptions options = new CompilerOptions(settings); // requestor CompilerRequestor requestor = new CompilerRequestor(className, cl); // problem factory IProblemFactory problemFactory = new DefaultProblemFactory(Locale.getDefault()); ICompilationUnit[] compilationUnits = new ICompilationUnit[1]; compilationUnits[0] = new CompilationUnit(className, source.toCharArray()); // compile Compiler compiler = new Compiler(env, policy, options, requestor, problemFactory); compiler.compile(compilationUnits); if (!requestor.isSuccess()) { throw new Exception(requestor.getErrors()); } // execute the static method of the sample class Class clazz = requestor.getResultClass(); Method method = clazz.getMethod("sayHello"); method.invoke(null); }
So with that information a SimpleCompiler class was created with more or less the previous code inside. A compiler of that type compiles the source and returns the Class object for the main class defined in the file (there are different methods to overload how the source code is passed -a String, a Reader,...-).
And finally I started a simple Servlet (called CompilerServlet) which instead of executing the request by itself, it loads the source for another servlet (in a real application it could be read from a database or any other repository), compiles it, instantiates an object and forwards the processing to this new Servlet object. The code is quite simple and it is condensed in the following method:
protected void processCompileServlet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try (InputStream in = this.getServletContext().getResourceAsStream( "/WEB-INF/classes/es/rickyepoderi/samples/servlet/" + request.getParameter("name") + ".txt")) { SimpleCompiler compiler = new SimpleCompiler( Thread.currentThread().getContextClassLoader(), new DefaultCompilerOptions()); Class clazz = compiler.compile("es.rickyepoderi.samples.servlet." + request.getParameter("name"), new InputStreamReader(in)); HttpServlet servlet = (HttpServlet) clazz.newInstance(); servlet.init(this.getServletConfig()); servlet.service(request, response); } catch (InstantiationException | IllegalAccessException e) { throw new ServletException(e); } }
As you see the CompilerServlet recovers the source from a file inside the WAR file using a name parameter (this is a emulation of a real repository, ddbb, cache system or whatever), it creates a SimpleCompiler and then uses it to compile the new Servlet source. The result is a new Servlet Class, using reflection a new object is instantiated. Finally the Servlet instance is initialized and executed. This way the output of this request is processed by the compiled Servlet and not by the compiler one. In this little example the Servlet is always re-compiling the source of the real servlet (no cache of any type is used).
And that's all. In this first chapter the ECJ compiler was presented and used to compiling in-line java sources. A CompilerServlet was done in order to locate, compile and load another servlet, this new Servlet is used to actually process the current request. I will try in a new entry to continue this idea using other JavaEE technologies.
Don't try this at home!
Comments