0

I am building a dynamic Web Project (in Eclipse with Tomcat as server) using servlets and JSPs. The general purpose of the project is to let the user solve small code problems. In order to do that I take a method written by the user and use it to build a java file that I can run via Reflection. The problem I can't seem to figure out is that Tomcat (or Eclipse?) does not update the file at runtime. So when I create the file using the code of the current user and try to compile it, my program always executes the file as it was when I started the server using the code of the previous user. How can I tell it to update the file before running it?

Edit: That's how I create the file:

public boolean writeFile() {
    try {
        PrintWriter writer = new PrintWriter(relativePath + "src\\testfiles\\TestFile.java");
        writer.print(content);
        writer.close();
        return true; }...

Here I call the writer and try running the file:

FileWriter writer = new FileWriter(content);        
    if(writer.writeFile()){
        Class<?> TestFile;
        Method m;
        try {
            TestFile = cl.loadClass("testfiles.TestFile");
            m = TestFile.getDeclaredMethod("testSolution");
            m.invoke(null);

Thanks in advance!

Jo_Nathan
  • 35
  • 8
  • 1
    So you create a java class from user input at runtime and store it sowhere? – Zpetkov May 27 '18 at 10:45
  • You're expecting us to debug the code you didn't even post? – kaqqao May 27 '18 at 10:58
  • @kaqqao I can provide some code but I didn't really want you to debug it, but rather wanted to know what possibilities there are to tell the server to update the file in his memory at runtime, as I couldn't find any while researching online. – Jo_Nathan May 27 '18 at 11:10
  • But we need to see how are you storing the file before we can know why it is not being updated. In the current state, the only possible answer is "you reload it", but without the context "reloading" doesn't mean much. What do you do with the code you receive? Save on disk? Compile from the string using Javassist? Something else? The answer would be different... – kaqqao May 27 '18 at 11:14
  • @Zpetkov Yes I store the Java class in the source Folder (see the edit) – Jo_Nathan May 27 '18 at 11:16
  • @kaqqao Okay sure, I provided some infos, hope that is enough – Jo_Nathan May 27 '18 at 11:18

1 Answers1

1

Ok, it's now clear what the issue is. Your issue is not with Tomcat not reloading the file, but with the classloader not reloading the class.

Normal classloaders will only load a class once, and keep it cached forever. The only way for a class to get unloaded is by its classloader being garbage collected. To reload a class you either have to use a different classloader each time (with the previous one getting garbage collected or you'll run out of memory), or to have a custom loader thar doesn't cache.

See this article for an implementation of a custom classloader that does what you want.

You could theoretically just have a new class each time (by changing its name on each save) but you'd eventually run out of memory.

Maybe the easiest way is to instantiate a new classloader in the method itself, load a class, run the method on it, and don't keep any other references to the loader or the class. That way, you'll get a freshly loaded class each time, and the old instances of both classes and loaders will get garbage collected.

UPDATE: I was operating under the assumption that you already know how to compile a class at runtime but, based on the comments, that's not the case. A classloader can, of course, only load a compiled class, so a source directly is not very useful.

Java internally provides a a compiler interface under javax.tools.JavaCompiler, but it's not easy to use and requires a different handling of Java versions before and after Java 9. So it is much easier to use a library like jOOR that hides the gory parts:

Class clazz = Reflect.compile("com.example.Test",
                "package com.example;" +
                "public class Test {\n" +
                "        public String hello() {\n" +
                "            return \"hello\";\n" +
                "        }\n" +
                "    }")
              .type();

Instead of type() to simply get the class, you can actually keep using jOOR's fluent reflection API to trigger the methods on the generated class or whatever it is you'd normally do via regular reflection.

For direct JavaCompiler usage, see e.g. this or, even better, jOOR's source code.

kaqqao
  • 12,984
  • 10
  • 64
  • 118
  • Okay thank you that makes sense! I will have a look at it. – Jo_Nathan May 27 '18 at 11:37
  • @PeterPan Just added a paragraph at the end. I think that's the simplest way to go about this, if it suffices. If not, check the `DynamicClassLoader` from the article. – kaqqao May 27 '18 at 11:54
  • I tried using a new one each time: `ClassLoader cl = this.getClass().getClassLoader(); FileWriter writer = new FileWriter(content); if(writer.writeFile()){ Class> TestFile; Method m; try { TestFile = cl.loadClass("testfiles.TestFile"); m = TestFile.getDeclaredMethod("testSolution"); m.invoke(null);` But the problem remains and the file is not updated until I refresh the project. I suspect it may be because Eclipse itself doesn't update the project and thus the classloader still accesses the old file – Jo_Nathan May 27 '18 at 12:00
  • @PeterPan You're not creating a new classloader. You keep getting the one that loaded the surrounding class, which is the same one Tomcat gave you. Try `new URLClassLoader`. – kaqqao May 27 '18 at 12:04
  • So I am using a URLClassloader: `URL[] url = {this.getClass().getClassLoader().getResource("./../classes")}; URLClassLoader ucl=new URLClassLoader(url); TestFile = ucl.loadClass("testfiles.TestFile"); ucl.close();` But it still doesn't work. Do I need to compile the file first somehow? – Jo_Nathan May 27 '18 at 12:48
  • @PeterPan Oh, of course. You can't load a class directly for the sources. I thought you already had that part resolved. See the update. – kaqqao May 27 '18 at 15:44