编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

Java中Runtime.exec()外部命令执行优化

wxchong 2024-06-17 22:32:00 开源技术 20 ℃ 0 评论
坚持原创,共同进步!请关注我,后续分享更精彩!

背景

项目中使用到Runtime.getRuntime().exec(command)和外部程序的跨进程交互。

遇到以下问题:

  • 调用线程间隙性被阻塞,不返回结果。
  • 线程并发量增加,部分线程假死,导致资源一直不释放。

经过优化,整理记录下来,给遇到同样问题的小伙伴。

代码实现

执行结果返回类:

@Data
public static class ExecResult<S>{
    private boolean status;
    private S successResult;
    private String errorResult;
}

执行任务接口:

/**
 * 运行时执行任务接口
 */
public interface RuntimeExecTask<S>{
    /**
     * 处理命令执行返回的输入流
     * @param inputStream
     */
    S handleInputStream(InputStream inputStream);

    /**
     * 处理命令执行错误结果
     * @param error
     */
    String handleErrorStream(String error);
}

RuntimeExecCommander类exec执行方法:

/**
 * 执行运行时命令返回
 * @param command   执行命令
 * @param task      命令执行结果处理任务
 * @param timeout   超时时间
 * @param unit      超时时间单位
 */
public <S> ExecResult<S> exec(String command,long timeout, TimeUnit unit,RuntimeExecTask<S> task){
    if (StringUtils.isBlank(command)) {
        throw new RuntimeException("command is not allowed null.");
    }
    ExecutorService pool = Executors.newFixedThreadPool(2);
    Process process = null;
    ExecResult<S> execResult = new ExecResult<>();
    try {
        process = Runtime.getRuntime().exec(command);
        //避免process被hang住,需要新开线程处理buffer stream数据

        Process finalProcess = process;
        Future<String> errorFutureResult = pool.submit(() -> task.handleErrorStream(write2String(finalProcess.getErrorStream())));
        Future<S> successFutureResult = pool.submit(() -> task.handleInputStream(finalProcess.getInputStream()));

        if (!process.waitFor(timeout, unit)) {
            //超时处理
            execResult.setStatus(false);
            execResult.setErrorResult("timeout is occurred. timeout="+unit.toSeconds(timeout)+"ms");
            return execResult;
        }

        String errorResult = errorFutureResult.get(1,TimeUnit.SECONDS);
        S successResult = successFutureResult.get(1,TimeUnit.SECONDS);

        if (StringUtils.isNotBlank(errorResult)) {
            execResult.setStatus(false);
            execResult.setErrorResult(errorResult);
        }else{
            execResult.setStatus(true);
            execResult.setSuccessResult(successResult);
        }
        return execResult;
    } catch (Exception e) {
        log.error("runtime command exec error:",e);
        execResult.setStatus(false);
        execResult.setErrorResult("运行时命令执行错误:"+e.getMessage());
        return execResult;
    }finally {
        try {
            if (process!=null) {
                process.destroy();
            }
            pool.shutdown();
            pool.awaitTermination(1,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

说明:

1.Runtime.getRuntime().exec(command) 命令执行后会启动新的目标调用进程。java中的当前线程和新的目标调用进程间,通过error和input流进行结果反馈。error和input流读取时有一个buffer缓存区,当缓存区填满,java线程迟迟未读取时,java 线程和目标进程将被阻塞。所以上面代码20、21行处,需新开线程实时监控处理,避免程序被阻塞。

2.经过反复测试,即便添加第1条的机制。并发线程上来(50线程压测),10-20%调用会假死,一直不释放资源。为解决这个问题,通过timeout超时机制来避免资源一直被占用(代码23行)。最后49行代码回收销毁进程资源。

RuntimeExecCommander类流处理辅助方法:

private String write2String(InputStream inputStream){
    StringBuilder sb = new StringBuilder();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
    String line = null;
    try {
        while ((line = bufferedReader.readLine())!=null){
            sb.append(line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return sb.toString();
}

测试方法:

public static void main(String[] args) {
    String command = "node d:\\working\\workspace\\puppeteer\\page2pdf.js \"https://www.baidu.com\"";
    for(int i=0;i<50;i++){
        int finalI = i;
        new Thread(() -> {
            RuntimeExecCommander execCommander = new RuntimeExecCommander();
            execCommander.exec(command,10,TimeUnit.MINUTES, new RuntimeExecTask<String>() {
                @Override
                public String handleInputStream(InputStream inputStream) {
                    return execCommander.write2String(inputStream);
                }

                @Override
                public String handleErrorStream(String error) {
                    if (error!=null && error.length()>0) {
                        error = "i="+finalI+",错误提示:"+error;
                    }
                    return error;
                }
            });
            System.out.println("第"+ finalI +"个任务执行完成.");
        }).start();
    }
}

小结

本文介绍了java中通过Runtime.getRuntime().exec(command)调用外部进程的方法。以及在调用过程中线程被阻塞和假死情况下解决方式。

最后,希望本文对大家有所帮助和参考。若有疑问,欢迎留言讨论。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表