Search code examples
javaeclipsedynamicclassnotfoundexception

ClassNotFoundException when running outside IDE


I have a java application which was developed in Eclipse.

There is a folder on the system which contains a lot of ".java" files. These ".java" files are classes which some user has written. I wish to load all these java classes and compile them inside my java application.

Another property of all the ".java" files are that all the classes written inside extend a class which is inside my original application.

I used the following to read and compile all the classes.

File parentFile = new File(rulesDir + "\\");
String fileName = rulesDir + "\\" + ruleName + ".java";
File ruleFile = new File(fileName);
// Compile source file.
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, null, ruleFile.getPath());             
// Load and instantiate compiled class.
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[] { parentFile.toURI().toURL() });
Class<? extends AbstractRule> cls = (Class<? extends AbstractRule>)Class.forName(ruleName, true, classLoader); 

If I run the above code inside Eclipse, it works fine. When I run the application as a jar from elsewhere, it throws an ClassNotFoundException for the line Class<? extends AbstractRule> cls = (Class<? extends AbstractRule>)Class.forName(ruleName, true, classLoader);

Why is this happening? What is different that it executes in Eclipse and doesn't via command line?


Solution

  • From the documentation for Class.forName

    name - fully qualified name of the desired class

    So, in order to get that fully qualified class name, you need to manipulate your rulesDir variable to replace the backslashes with periods, then prepend that to your ruleName variable, combined with another period, to get the fully qualified class name. Then you'll be able to use the ClassLoader to load the class. The fully qualified name is required so that the ClassLoader can find your resource from the root of your classpath.

    NB I make the assumption that your rulesDir is a relative path from the base of your classpath. If it is not, then you'll have extra manipulation to do here

    See code manipulation below:

    import java.io.File;
    import java.net.URL;
    import java.net.URLClassLoader;
    import javax.tools.JavaCompiler;
    import javax.tools.ToolProvider;
    import rules.AbstractRule;
    
    public class MainApp {
        public static void main(String[] args) {
            try {
                System.out.println("Test");
                // NB Amended here to now take project root, relative path to rules directory and class name. So that compilation can take place based on the absolute path and class loading from the relative one.
                compile("C:\\Media\\Code\\manodestra_java\\src\\tmp", "rules", "TestRule");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private static void compile(String projectRoot, String rulesDir, String ruleName) throws Exception {
            File parentFile = new File(projectRoot + "\\" + rulesDir + "\\");
            System.out.println(parentFile);
            String fileName = parentFile.getCanonicalPath() + "\\" + ruleName + ".java";
            File ruleFile = new File(fileName);
            System.out.println(ruleFile);
    
            // Compile source file.
            System.out.println("Compiling...");
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            compiler.run(null, null, null, ruleFile.getPath());
    
            // Load and instantiate compiled class.
            System.out.println("Loading class...");
            URLClassLoader classLoader = URLClassLoader.newInstance(new URL[] { parentFile.toURI().toURL() });
            System.out.println("Class Loader: " + classLoader);
            ruleName = rulesDir.replace("\\", ".") + "." + ruleName;
            Class<? extends AbstractRule> clazz = (Class<? extends AbstractRule>)Class.forName(ruleName, true, classLoader);
            System.out.println(clazz);
        }
    }
    

    For the sake of my testing, this class was defined in the default package and I created a rules directory below that level to contain my subclasses of AbstractRule. So, rules.TestRule was my fully qualified path to my class name. But, yours could be...

    com.example.testapplication.rules.TestRule, etc.

    And that's what would be required in Class.forName. There's a path to your classpath root, then the relative path from there to your java files (which is equivalent to the package of your classes), then the actual class names under that package path.