使用Processbuilder运行jar文件无法正常工作

7
我有以下代码:
ProcessBuilder pb = new ProcessBuilder( "java", "-jar", "test.jar", Integer.toString( jobId ), Integer.toString( software ), Integer.toString( entryPoint ), application );
pb.directory( new File("/home/userName/TestBSC") );
Process proc = pb.start();

当我使用以下命令从终端运行jar文件时:

java -jar test.jar 135 3 3 appName

它可以很好地工作。该jar文件将一些内容推送到我的数据库中,因此我可以看到它正在运行。但是,当我使用上面提到的processBuilder代码从我的JavaServlet中执行时,我在我的数据库中没有得到任何数据,也没有收到任何错误信息。

然而,该进程本身正在运行,我用终端中的"ps ax"进行了检查。所以我想知道这里的区别在哪里?我做错了什么?

有人有想法吗?

编辑:更多代码:

ProcessBuilder pb = new ProcessBuilder( "java", "-jar", "test.jar", Integer.toString( jobId ), Integer.toString( software ), Integer.toString( entryPoint ), application );
pb.directory( new File("/home/userName/TestBSC") );
Process proc = pb.start();

System.out.println( "Job running" );
proc.waitFor(); // wait until jar is finished
System.out.println( "Job finished" );

InputStream in = proc.getInputStream();
InputStream err = proc.getErrorStream();

byte result[] = new byte[ in.available() ];
in.read( result, 0, result.length );
System.out.println( new String( result ) );

byte error[] = new byte[ err.available() ];
err.read( error, 0, error.length );
System.out.println( new String( error ) );

更新:

我尝试调用一个shell脚本而不是我的jar文件。因此,我从我的java文件中使用processbuilder调用了一个shell脚本。

我的shell脚本执行以下操作:

java -jar test.jar "$1" "$2" "$3" "$4"

好的,它仍然没有起作用。所以我尝试了这个:

gnome-terminal -x java -jar test.jar "$1" "$2" "$3" "$4"

突然它起作用了!但是它会打开gnome终端,然后执行jar文件。
所以我想知道,这是否与在eclipse中未显示的输出有关?我真的不明白。现在这是一个很好的解决方法。但是我真的希望在每次执行jar时不必打开终端来使其正常工作。

您的进程中是否有任何错误/异常? - Mickael
@PeterMmm:我也尝试过这个,但结果一样。等等...你说的Java是什么意思?你是指JAR文件的完整路径还是Java JRE? - progNewbie
首先你说 java -jar test.jar 135 3 3 appName 可以工作,现在又说 java -jar test.jar "$1" "$2" "$3" "$4" 不能工作,这很奇怪。而且为什么你有两个不同的JAR文件? - Mickael
@MickaëlB:是的,这很奇怪!但是在语句前面写上“gnome-terminal -x”,它就可以工作了。我不知道为什么。对于第二个jar文件的混淆名称,我很抱歉,这是一个复制/粘贴错误。 - progNewbie
@MickaëlB:我刚意识到我误解了你的问题。我说这个命令'java -jar test.jar 135 3 3'是有效的,因为如果我在终端中输入它,它就可以工作。但是当我将其放入像test.sh这样的文件中,并使用processbuilder从我的javacode启动test.sh时,它就无法工作。希望现在清楚了。 - progNewbie
显示剩余7条评论
2个回答

8
首先,我无法重现您的问题,因此这个答案将仅基于文档资料。
默认情况下,创建的子进程没有自己的终端或控制台。其所有标准I/O操作(即stdin、stdout、stderr)都将被重定向到父进程,可以通过使用getOutputStream()、getInputStream()和getErrorStream()方法获取的流在父进程中访问。父进程使用这些流来提供输入并从子进程获取输出。由于某些本地平台只为标准输入和输出流提供有限的缓冲区大小,因此不能及时写入子进程的输入流或读取子进程的输出流可能会导致子进程阻塞甚至死锁。
基本上,这告诉您需要正确处理外部进程的流,否则在某些平台上可能会导致死锁。这意味着如果我们运行的命令产生一些输出,您必须读取该输出。

让我们来看一下你的代码;你正在调用 process.waitFor() 来等待进程完成,但问题是你的进程无法完成,除非你读取/消耗它的输出,因此你会创建一个死锁。

如何克服这个问题:

  1. Method is using an InputStreamConsumerThread to handle input/error stream properly;

    public class InputStreamConsumerThread extends Thread
    {
      private InputStream is;
      private boolean sysout;
      private StringBuilder output = new StringBuilder();
    
      public InputStreamConsumerThread (InputStream is, boolean sysout)
      {
        this.is=is;
        this.sysout=sysout;
      }
    
      public void run()
      {
        try(BufferedReader br = new BufferedReader(new InputStreamReader(is)))
        {
          for (String line = br.readLine(); line != null; line = br.readLine())
          {
            if (sysout)
              System.out.println(line);    
            output.append(line).append("\n");
          }
        }
      }
      public String getOutput(){
        return output.toString();
      }
    }
    

你的代码将会是:

    String systemProperties = "-Dkey=value";    
    ProcessBuilder pb = new ProcessBuilder( "java", systemProperties, "-jar", "test.jar", Integer.toString( jobId ), Integer.toString( software ), Integer.toString( entryPoint ), application );
    pb.directory( new File("/home/userName/TestBSC") );
    Process proc = pb.start();

    InputStreamConsumerThread inputConsumer = 
         new InputStreamConsumerThread(proc.getInputStream(), true);
    InputStreamConsumerThread errorConsumer = 
         new InputStreamConsumerThread(proc.getErrorStream(), true);

    inputConsumer.start();
    errorConsumer.start();

    System.out.println( "Job running" );
    proc.waitFor(); // wait until jar is finished
    System.out.println( "Job finished" );

    inputConsumer.join(); // wait for consumer threads to read the whole output
    errorConsumer.join();

    String processOutput = inputConsumer.getOutput();
    String processError = errorConsumer.getOutput();
    
    if(!processOutput.isEmpty()){
        //there were some output
    }

    if(!processError.isEmpty()){
        //there were some error
    }

    
  1. Method is using ProcessBuilder to redirect output. If you just want your sub-process use the same input/output stream with your parent process, you can use ProcessBuilder.Redirect.INHERIT like this;

    ProcessBuilder pb = new ProcessBuilder( "java", "-jar", "test.jar", Integer.toString( jobId ), Integer.toString( software ), Integer.toString( entryPoint ), application );
    pb.directory( new File("/home/userName/TestBSC") );
    pb.redirectErrorStream(true); // redirect error stream to output stream
    pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
    Process proc = pb.start();
    
    System.out.println( "Job running" );
    //since process builder will handle the streams for us
    //we can call waitFor() safely
    proc.waitFor(); 
    System.out.println( "Job finished" );
    
  2. Method is using a 3rd party library. If you don't wanna hassle with ProcessBuilder and the consumer threads yourself, there are 2 libraries that I know of that handles creating sub-process very nicely.

一个低开销、非阻塞 I/O、外部进程执行的 Java 实现。它是 java.lang.ProcessBuilder 和 java.lang.Process 的替代品。
你是否曾经感到烦恼,每当你在 Java 中生成一个进程时,你都必须创建两个或三个“泵浦”线程(对于每个进程)来从 stdout 和 stderr 管道中取出数据并将数据泵入 stdin?如果你的代码启动了很多进程,你可能会有几十个或几百个线程只是在泵送数据。
NuProcess 使用 JNA 库使用特定于平台的本机 API 在你的 Java 进程和生成的进程之间的管道上实现非阻塞 I/O。
有许多方法可用于从Java中运行外部进程。有JRE选项,如Runtime.exec()和ProcessBuilder。还有Apache Commons Exec。尽管如此,我们创建了另一个进程库(YAPL)。
这种疯狂的努力的一些原因是: - 改进流处理:读/写流、将stderr重定向到stdout - 改进超时处理 - 改进退出代码检查 - 改进API - 一行代码实现相当复杂的用例 - 一行代码将进程输出转换为字符串 - 可以访问进程对象 - 支持异步进程(Future) - 使用SLF4J API改进日志记录 - 支持多个进程
另外,在Java的Process API中还存在其他陷阱,请参阅JavaWorld文章了解更多信息。When Runtime.exec() won't. ZT Process Executor

哇,感谢您提供这个很棒的答案。我目前正在测试方法1。在InputStreamConsumerThread的run方法中,是否需要使用new BufferedReader()而不是new BufferedInputStream()?因为它说我无法从BufferedInputStream转换为BufferedReader。另外,我该如何从errorConsumer中读取错误信息等内容呢? - progNewbie
@progNewbie 是的,我的错误。已经更正了答案。 - Onur
谢谢。我怎样可以从errorConsumer那里读取信息?因为在我的原始代码中,我会在.jar文件执行完毕后检查是否有错误信息。 - progNewbie
哇,谢谢!虽然它需要是 processOutput.isEmpty()。 - progNewbie
只有一个问题,但首先:它工作了!哇。但是我也在我的错误流中得到了所有警告。但我想要的只是真正的错误,这样我就可以检查程序是否成功结束了。这可能吗? - progNewbie
显示剩余3条评论

0

你可以试试这个?

更新

代码:

// Java runtime
Runtime runtime = Runtime.getRuntime();
// Command
String[] command = {"java", "-jar", "test.jar", Integer.toString( jobId ), Integer.toString( software ), Integer.toString( entryPoint ), application};
// Process
Process process = runtime.exec(command, null, new File("/home/userName/TestBSC"));

不幸的是,情况还是一样的。进程已经执行了。但是没有输出,也没有任何东西存储在我的数据库中。就好像进程根本没有在处理一样。这太奇怪了。 - progNewbie
进程的 exitValue 是什么? (http://docs.oracle.com/javase/7/docs/api/java/lang/Process.html#exitValue()) - Mickael
这个进程没有停止。 :( - progNewbie
这可能是一个优先级的问题或类似的东西吗? - progNewbie
我不这么认为。你还有这个 waitFor() 吗?如果有,你能把它移到 System.out.println( new String( error ) ); 之后吗? - Mickael
它没有改变任何东西。但是哇,这太奇怪了。我刚刚结束了一个仍在运行的进程,之前的测试还在运行。突然间,所有来自jar的System.out.println输出都出现在我的Eclipse控制台上,但数据库中仍然没有任何行。真的很奇怪。 - progNewbie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接