使用apache-commons exec处理输出结果

29

我到了崩溃的边缘。我确定这是一些简单的东西,我很可能对Java和Streams有很大的理解漏洞。我认为有太多的类让我有点不知所措,试图浏览API以找出何时以及如何使用各种输入/输出流。

我刚刚了解到apache commons库的存在(自学Java失败),目前正在尝试将我的Runtime.getRuntime().exec的一些内容转换为使用commons-exec。这已经修复了执行中每6个月出现一次的问题,然后就会消失的风格问题。

代码执行一个perl脚本,并在GUI中显示脚本的stdout。

调用代码位于swingworker内部。

我迷失了如何使用pumpStreamHandler... 不管怎样,这是旧代码:

String pl_cmd = "perl script.pl"
Process p_pl = Runtime.getRuntime().exec( pl_cmd );

BufferedReader br_pl = new BufferedReader( new InputStreamReader( p_pl.getInputStream() ) );

stdout = br_pl.readLine();
while ( stdout != null )
{
    output.displayln( stdout );
    stdout = br_pl.readLine();
}

我想这就是当我复制粘贴自己不完全理解的代码一段时间后得到的结果。上面的代码应该是执行进程,然后通过“getInputStream”获取输出流,将其放入缓冲区读取器中,然后只会循环直到缓冲区为空。

我不明白的是为什么这里不需要'waitfor'样式的命令?难道在缓冲区为空、退出循环并继续执行时,进程仍在运行是不可能的吗?当我运行它时,并没有出现这种情况。

无论如何,我正在尝试使用commons exec获得相同的行为,基本上再次从谷歌发现的代码开始:

DefaultExecuteResultHandler rh = new DefaultExecuteResultHandler();
ExecuteWatchdog wd  = new ExecuteWatchdog( ExecuteWatchdog.INFINITE_TIMEOUT );
Executor exec = new DefaultExecutor();

ByteArrayOutputStream out = new ByteArrayOutputStream();
PumpStreamHandler psh = new PumpStreamHandler( out );

exec.setStreamHandler( psh );
exec.setWatchdog( wd );

exec.execute(cmd, rh );
rh.waitFor();
我正在尝试弄清楚pumpstreamhandler在做什么。我认为它会获取exec对象的输出,并使用来自Perl脚本的stdout/err的字节填充我提供的OutputStream?
如果是这样,您要如何获得上述行为以使其逐行流式传输输出?在示例中,人们向您展示如何在最后调用out.toString(),我想这只会在脚本运行完成后给出所有输出的转储。如何使它逐行显示输出,而不是一次性显示所有输出?
------------ 未来编辑 ---------------------
通过Google找到了这个,效果也很好:
public static void main(String a[]) throws Exception
{
    ByteArrayOutputStream stdout = new ByteArrayOutputStream();
    PumpStreamHandler psh = new PumpStreamHandler(stdout);
    CommandLine cl = CommandLine.parse("ls -al");
    DefaultExecutor exec = new DefaultExecutor();
    exec.setStreamHandler(psh);
    exec.execute(cl);
    System.out.println(stdout.toString());
}

我也在尝试从ExecuteResultHandler获取错误/输出/输入流,并将其抛到JTextPane上。上面的方法是否有效,或者您使用ExecuteResultHandler setOut/In/Error来完成它? - Giannis
不是你的问题,是Java的问题 :-) 在(POSIX)脚本中使用“反引号”或者在C语言(或其他POSIX友好型语言)中使用popen会很简单,但在Java中却变成了一个复杂的傻瓜式步行。 - Roboprog
4个回答

31
不要将ByteArrayOutputStream传递给PumpStreamHandler,而是使用抽象类org.apache.commons.exec.LogOutputStream的实现。根据javadoc

  

该实现解析传入的数据以构造一行,并将整个行传递给用户定义的实现。

因此,LogOutputStram对输出进行预处理,使您能够控制处理单个行而不是原始字节。像这样:

import java.util.LinkedList;
import java.util.List;
import org.apache.commons.exec.LogOutputStream;

public class CollectingLogOutputStream extends LogOutputStream {
    private final List<String> lines = new LinkedList<String>();
    @Override protected void processLine(String line, int level) {
        lines.add(line);
    }   
    public List<String> getLines() {
        return lines;
    }
}

然后,在调用 exec.execute 进行阻塞后,你的 getLines() 将包含你想要的标准输出和标准错误。在执行进程并将所有 stdOut/stdErr 收集到一行列表中的角度来看,ExecutionResultHandler 是可选的。


1
为什么不传递一个“ByteArrayOutputStream”? 盲目使用“LogOutputStream”之前,更详细的解释会更有帮助。 - Lucas

3
基于James A Wilson的答案,我创建了一个帮助类“Execute”。它将他的答案包装成一个解决方案,并提供了一个方便的exitValue。
只需一行代码即可执行命令: ExecResult result = Execute.execCmd(cmd, expectedExitCode); 以下Junit测试用例测试并展示了如何使用它: Junit4测试用例:
package com.bitplan.newsletter;

import static org.junit.Assert.*;

import java.util.List;

import org.junit.Test;

import com.bitplan.cmd.Execute;
import com.bitplan.cmd.Execute.ExecResult;

/**
 * test case for the execute class
 * @author wf
 *
 */
public class TestExecute {
     @Test
   public void testExecute() throws Exception {
     String cmd="/bin/ls";
     ExecResult result = Execute.execCmd(cmd,0);
     assertEquals(0,result.getExitCode());
     List<String> lines = result.getLines();
     assertTrue(lines.size()>0);
     for (String line:lines) {
         System.out.println(line);
     }
   }
}

执行Java辅助类:

package com.bitplan.cmd;

import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.LogOutputStream;
import org.apache.commons.exec.PumpStreamHandler;

/**
 * Execute helper using apache commons exed
 *
 *  add this dependency to your pom.xml:
   <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-exec</artifactId>
            <version>1.2</version>
        </dependency>

 * @author wf
 *
 */
public class Execute {

    protected static java.util.logging.Logger LOGGER = java.util.logging.Logger
            .getLogger("com.bitplan.cmd");

    protected final static boolean debug=true;

    /**
     * LogOutputStream
     * https://dev59.com/IGs05IYBdhLWcg3wR_22
     * -apache-commons-exec
     * 
     * @author wf
     * 
     */
    public static class ExecResult extends LogOutputStream {
        private int exitCode;
        /**
         * @return the exitCode
         */
        public int getExitCode() {
            return exitCode;
        }

        /**
         * @param exitCode the exitCode to set
         */
        public void setExitCode(int exitCode) {
            this.exitCode = exitCode;
        }

        private final List<String> lines = new LinkedList<String>();

        @Override
        protected void processLine(String line, int level) {
            lines.add(line);
        }

        public List<String> getLines() {
            return lines;
        }
    }

    /**
     * execute the given command
     * @param cmd - the command 
     * @param exitValue - the expected exit Value
     * @return the output as lines and exit Code
     * @throws Exception
     */
    public static ExecResult execCmd(String cmd, int exitValue) throws Exception {
        if (debug)
            LOGGER.log(Level.INFO,"running "+cmd);
        CommandLine commandLine = CommandLine.parse(cmd);
        DefaultExecutor executor = new DefaultExecutor();
        executor.setExitValue(exitValue);
        ExecResult result =new ExecResult();
        executor.setStreamHandler(new PumpStreamHandler(result));
        result.setExitCode(executor.execute(commandLine));
        return result;
    }

}

你会如何使用异步执行来实现这个功能? - sebster
使用Java FutureTask怎么样?例如,参见https://blog.codecentric.de/2011/10/tasks-parallel-ausfuhren-mit-java-future/。 - Wolfgang Fahl

3
我不明白的是为什么这里没有需要“waitfor”样式命令?难道在缓冲区为空,退出循环并继续执行进程的某个时间段内是有可能的吗?当我运行它时,似乎并非如此。
readLine会阻塞。也就是说,您的代码将等待直到读取了一行。
PumpStreamHandler 来自文档
将子进程的标准输出和错误复制到父进程的标准输出和错误。如果输出或错误流设置为null,则该流的任何反馈都将丢失。

我看了文档并阅读了它,但我不明白如何连接这些点来捕获输出到除父进程标准输出之外的其他位置。 - James DeRagon
我相信你可以在PumpStreamHandler中传递一个不同的OutputStream,而不是使用ByteArrayOutputStream out = new ByteArrayOutputStream();(例如,你可以创建一个文件输出流,以便将日志转储到文件中)。 - Hyangelo
我猜这就是情况,之前我本来想找到需要传递的流并将字节流转换为字符流,然后再通过缓冲读取器进行读取,然后也在思考exec是否有不同的行为。但我猜代码基本上就像之前的例子一样,只是需要将其包装在pumpstreamhandler中并将其传递给exec,但保持一切不变。 - James DeRagon

0

这是一个非常古老的帖子,但我不得不使用Apache Commons Exec并解决了相同的问题。我相信在2014年发布的Apache Commons Exec的最新版本中,以下解决方案可以很好地处理有和没有监视程序的情况;

class CollectingLogOutputStream implements ExecuteStreamHandler {
private final List<String> lines = new LinkedList<String>();
public void setProcessInputStream(OutputStream outputStream) throws IOException 
{
}
//important - read all output line by line to track errors
public void setProcessErrorStream(InputStream inputStream) throws IOException {
    InputStreamReader isr = new InputStreamReader(inputStream);
    BufferedReader br = new BufferedReader(isr);
    String line="";
    while( (line = br.readLine()) != null){
        //use lines whereever you want - for now just print on console
        System.out.println("error:"+line);
    }
}
//important - read all output line by line to track process output
public void setProcessOutputStream(InputStream inputStream) throws IOException 
{
    InputStreamReader isr = new InputStreamReader(inputStream);
    BufferedReader br = new BufferedReader(isr);
    String line="";
    while( (line = br.readLine()) != null){
        //use lines whereever you want - for now just print on console
        System.out.println("output:"+line);
    }
  }

public void start() throws IOException {
}

public void stop() throws IOException {
}
}

以上类可以设置为执行器的 StreamHandler,代码如下;
//set newly created class stream handler for the executor
executor.setStreamHandler(new CollectingLogOutputStream());

完整的代码可以在这里找到:https://github.com/raohammad/externalprocessfromjava

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