Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java - start another class' main in a different process

I need a clean way to start many instances of a Java program with a GUI, and I want to do it programmatically. The "program" i want to run is just a .class file (a compiled .java file with a main method), it should show a GUI and run independently from the others (as its own process). I also need to pass that program some arguments.

Check EDIT5 for the complete working solution code.

Here's the class that's supposed to start the many processes

package startothermain;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Starter {

    public static void main(String[] args) {
        int starts = 4;

        for (int i = 0; i < starts; ++i) {
            System.out.println("Starting an app");
            ProcessBuilder pb = new ProcessBuilder("java.exe", "-cp", "bin", "Started", "arg0");
            try {
                pb.start();
            } catch (IOException ex) {
                Logger.getLogger(Starter.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}

This is the class that's supposed to be started and display a GUI

package startothermain;

import javax.swing.JOptionPane;

public class Started {

    public static void main(String[] args) {
        JOptionPane.showMessageDialog(null, args[0]);
    }
}

I tried both ProcessBuilder and Runtime.getRuntime() suggestions I found in other answers, but they don't seem to work. I always get some kind of "not found" error, like this one

SEVERE: null
java.io.IOException: Cannot run program "java.exe": error=2, No such file or directory
    at java.lang.ProcessBuilder.start(ProcessBuilder.java:1041)
    at startothermain.Starter.main(Starter.java:35)
Caused by: java.io.IOException: error=2, No such file or directory
    at java.lang.UNIXProcess.forkAndExec(Native Method)
    at java.lang.UNIXProcess.<init>(UNIXProcess.java:135)

I'm working from NetBeans and clicking the Run button, so the .java files should be compiled properly and in the same folder. I hope this is no side problem with the src/build folders created by the IDE.

EDIT: I was trying to find the java.exe, but I'm on Linux. Sigh. I changed it for "java", but I still have the same problem. The path variable should be set. Moreover, I'm worried that, if I give it a full path, it will become unportable.

Trying to get the JAVA_HOME on Linux

System.getenv("JAVA_HOME"); // returns null
System.getProperty("java.home"); // returns /usr/lib/jvm/java-7-openjdk-amd64/jre

And on Windows

System.getenv("JAVA_HOME"); // returns C:\Program Files\Java\jdk1.7.0_51
System.getProperty("java.home"); // returns C:\Program Files\Java\jdk1.7.0_51\jre

EDIT2: This new code does NOT spawn errors, but does not open any GUI either. I tried this on both on Windows and Linux, with the same results.

String javaHome = System.getProperty("java.home");
ProcessBuilder pb = new ProcessBuilder(javaHome + "/bin/java", "-cp", "bin", "Started", "arg0");

EDIT3: I redirected the error and output streams of the processbuilder (not of the crerated processes) and it turns out that the Java ClassLoader can't find the class. I guess the problem is not JAVA_HOME, but rather a path problem.

Here's the new code

package startothermain;

import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Starter {

    public static void main(String[] args) {
        int starts = 4;
        System.out.println(System.getProperty("java.home")); // prints C:\Program Files\Java\jdk1.7.0_51\jre
        System.out.println(System.getenv("JAVA_HOME")); // prints C:\Program Files\Java\jdk1.7.0_51

        System.out.println("Working Directory = "
                + System.getProperty("user.dir")); // prints C:\Users\Agostino\Documents\NetBeansProjects\StartOtherMain

        for (int i = 0; i < starts; ++i) {
            System.out.println("Starting an app");
            ProcessBuilder pb = new ProcessBuilder("java", "build.classes.startothermain.Started");
            File log = new File("log");
            pb.redirectOutput(log);
            pb.redirectError(log);
            try {
                pb.start();
            } catch (IOException ex) {
                Logger.getLogger(Starter.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}

In the log file I find this error

java.lang.NoClassDefFoundError: build/classes/startothermain/Started (wrong name: startothermain/Started)
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:800)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:449)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:71)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
    at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:482)
Exception in thread "main" 

EDIT4: now the code works, but if I try to use a .jar library in the Started class, it can't be found.

See this Started class that uses a .jar lib (JavaTuples) that is added to the project libraries through NetBeans

package startothermain;

import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Starter {

    public static void main(String[] args) {
        int starts = 1;

        for (int i = 0; i < starts; ++i) {
            System.out.println("Starting an app");
            ProcessBuilder pb = new ProcessBuilder();

            String fullClassName = Started.class.getName();
            pb.command("java", "-cp", "./build/classes", fullClassName, "myArg");

            File log = new File("log");
            pb.redirectOutput(log);
            pb.redirectError(log);
            try {
                pb.start();
            } catch (IOException ex) {
                Logger.getLogger(Starter.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}

Fixed Starter class

package startothermain;

import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Starter {

    public static void main(String[] args) {
        int starts = 1;

        for (int i = 0; i < starts; ++i) {
            System.out.println("Starting an app");
            ProcessBuilder pb = new ProcessBuilder();

            String fullClassName = Started.class.getName();
            pb.command("java", "-cp", "./build/classes", fullClassName, "myArg");

            File log = new File("log");
            pb.redirectOutput(log);
            pb.redirectError(log);
            try {
                pb.start();
            } catch (IOException ex) {
                Logger.getLogger(Starter.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}

The error in the ProcessBuilder log

Exception in thread "main" java.lang.NoClassDefFoundError: org/javatuples/Pair
    at startothermain.Started.main(Started.java:28)
Caused by: java.lang.ClassNotFoundException: org.javatuples.Pair
    at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
    ... 1 more

EDIT5: Working code

Starter class

package startothermain;

import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Starter {

    public static void main(String[] args) {
        int starts = 1;

        for (int i = 0; i < starts; ++i) {
            System.out.println("Starting an app");
            ProcessBuilder pb = new ProcessBuilder();

            String fullClassName = Started.class.getName();
            String pathToClassFiles = new File("./build/classes").getPath();
            String pathSeparator = File.pathSeparator; // ":" on Linux, ";" on Windows
            String pathToLib = new File("./lib/javatuples-1.2.jar").getPath();
            pb.command("java", "-cp", pathToLib + pathSeparator + pathToClassFiles, fullClassName, "myArg");

            File log = new File("log" + i + ".txt"); //debug log for started process
            try {
                pb.redirectOutput(log);
                pb.redirectError(log);
                pb.start();
            } catch (IOException ex) {
                Logger.getLogger(Starter.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}

Started class

package startothermain;

import javax.swing.JOptionPane;
import org.javatuples.Pair;

public class Started {

    public static void main(String[] args) {
        Pair<String, Integer> pair = Pair.with("One", 1);
        JOptionPane.showMessageDialog(null, args[0] + " " + pair);
    }
}
like image 277
Agostino Avatar asked Jan 17 '14 19:01

Agostino


People also ask

Can we call a class's main () method from another class?

Solution: Though Java doesn't prefer main() method called from somewhere else in the program, it does not prohibit one from doing it as well. So, in fact, we can call the main() method whenever and wherever we need to.

How do you call a main method from another method in Java?

Call a MethodInside main , call the myMethod() method: public class Main { static void myMethod() { System.out.println("I just got executed!"); } public static void main(String[] args) { myMethod(); } } // Outputs "I just got executed!"

Can you have 2 main methods in Java?

From the above program, we can say that Java can have multiple main methods but with the concept of overloading. There should be only one main method with parameter as string[ ] arg.


1 Answers

As on your request I summarize my comments in an answer.

Your first problem was that you tried to invoke java.exe on a Linux or Mac box, which does not stick to the Microsoft convention of including the file type into the name. Therefore Linux and Mac usually use java instead.

The path to the executable can vary also from computer to computer. Therefore the probably most generic way is to locate the java executable using the JAVA_HOME environment variable which can be fetched in Java via System.getenv("JAVA_HOME"); or via System.getProperty("java.home"); Note however that they might not obviously point to the desired directory. Especially some Linux distributions place all binary files inside /bin or /usr/bin and therefore ${JAVA_HOME}/bin/java might not be available. In that case you should create a (hard)link to the executable.

If the path is not set you can set it manually in the session of your console you are executing your application (on Windows set JAVA_HOME=C:\Program Files\Java (or setx on newer Windows versions) on Linux export JAVA_HOME=/opt/java). This can also be done with user-priviledges

It is further strongly recommended to take care of in- and output-streams when dealing with processes. The linked article explains in depth why you should take care of it - not only to catch exceptions thrown by the invoked process.

On invoking a process (especially with the Java executable) you have a couple of options: You can either invoke the Java process directly (assuming Java is on your PATH) with new ProcessBuilder("java", "-cp", "path/to/your/binaries", "package.ClassName"); or you can invoke the Java executable via a shell - on Linux f.e. you could also use new ProcessBuilder("/bin/bash", "-c", "java -cp path/to/your/binaries package.ClassName"); (though the primer one is obviously preferable).

On loading classes/libraries dynamically you have to use the fully qualified class name. So, if you invoke the Java process in the root-directory of your project and your building-tool generates the class-files inside of ./build/classes and your class Test is located in package testpackage you will end up with the following set: ./build/classes/testpackage/Test.class. To start a new Java process which invokes the main method included in your Test.class you have to use the following command: java -cp ./build/classes testpackage.Test else Java will not be able to find the class definition of Test and execute the main-method.

Missing dependencies have to be added to the classpath (-cp ...) segment of the invoking Java command. F.e: java -cp lib/jar1.jar;lib/jar2.jar;build/classes/* package.ClassName. Multiple archives or directories are separated by a ; and an asterisk * is also possible to include everything within a directory.

One note left: if you ever try to ship your "application" you will need to adapt this code to a more generic version (property file f.e) as this will probably fail with high probability as the path might be totally different.

If I have something forgotten, please let me know.

like image 70
Roman Vottner Avatar answered Oct 02 '22 18:10

Roman Vottner