CLOVER🍀

That was when it all began.

Javaからの外部プロセス起動に、ZT Process Executorを使う

Javaから外部プロセス起動するする時は、自分でProcessまわりのクラス使ったり、Commons Exec使ったりするものですが、他に何かないのかな?と思い(Commons Execより、もうちょっと簡単に書けるものが欲しかった)調べたら、こういうのが見つかりました。

GitHub - zeroturnaround/zt-exec: ZeroTurnaround Process Executor

最終リリースは2015年5月なのかぁって思ってたら、Commons Execは2014年だった…。
※Commons Execはコードは最近更新されている模様

せっかくなので、試してみます。

準備

Maven依存関係としては、最低限以下が必要です。

        <dependency>
            <groupId>org.zeroturnaround</groupId>
            <artifactId>zt-exec</artifactId>
            <version>1.8</version>
        </dependency>

ZT Process Executor自体の依存関係は、Commons IOとSLF4Jです。ロガーの実装は適当に。今回は、slf4j-simpleを使っています。

あと、サンプルではテストコードで書くので、JUnitとAssertJを依存関係に加えています。

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.3.0</version>
            <scope>test</scope>
        </dependency>

使ってみる

以降のコードでは、以下のimport文があることを前提にしています。

import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;

import org.junit.Test;
import org.slf4j.LoggerFactory;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.ProcessResult;
import org.zeroturnaround.exec.StartedProcess;
import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;

import static org.assertj.core.api.Assertions.assertThat;

なお、以降の内容はGitHubのREADMEにある、Examplesをマネたものです。

Examples

とりえず外部プロセスを起動してみます。

    @Test
    public void test1() throws InterruptedException, TimeoutException, IOException {
        new ProcessExecutor().command("java", "-version").execute();
    }

これだとプロセスは起動しますが、プロセスの標準出力などで書き出す内容は捨てられてしまいます。

結果を取得するには、execute後に取ればよいみたいです。

    @Test
    public void test2() throws InterruptedException, TimeoutException, IOException {
        String output =
                new ProcessExecutor()
                        .command("java", "-version")
                        .readOutput(true)
                        .execute()
                        .outputUTF8();
        assertThat(output)
                .contains("java version")
                .contains("Java(TM) SE Runtime Environment")
                .contains("Java HotSpot(TM)");
    }

readOutputをtrueにすることがポイントです。

任意のエンコーディングでもOKです。

    @Test
    public void test3() throws InterruptedException, TimeoutException, IOException {
        String output =
                new ProcessExecutor()
                        .command("java", "-version")
                        .readOutput(true)
                        .execute()
                        .outputString("UTF-8");
        assertThat(output)
                .contains("java version")
                .contains("Java(TM) SE Runtime Environment")
                .contains("Java HotSpot(TM)");
    }

標準出力の内容を、StreamとしてSLF4Jに書き出すこともできます。

    @Test
    public void test4() throws InterruptedException, TimeoutException, IOException {
        new ProcessExecutor()
                .command("java", "-version")
                .redirectOutput(Slf4jStream.of(LoggerFactory.getLogger(getClass())).asInfo())
                .execute();
    }

まあ、このAPI、SLF4J向けにしかなさそうですけど…ないものについては、自分でLogOutputStreamというクラスの実装を作成する必要があります。

呼び出し元のClassクラスを渡す、でも可。

    @Test
    public void test5() throws InterruptedException, TimeoutException, IOException {
        new ProcessExecutor()
                .command("java", "-version")
                .redirectOutput(Slf4jStream.of(getClass()).asInfo())
                .execute();
    }

Slf4jStream#ofCallerというのもあるようで。

ログ出力しつつも、結果を取得することも可能です。

    @Test
    public void test6() throws InterruptedException, TimeoutException, IOException {
        String output =
                new ProcessExecutor()
                        .command("java", "-version")
                        .readOutput(true)
                        .redirectOutput(Slf4jStream.of(LoggerFactory.getLogger(getClass())).asInfo())
                        .execute()
                        .outputUTF8();
        assertThat(output)
                .contains("java version")
                .contains("Java(TM) SE Runtime Environment")
                .contains("Java HotSpot(TM)");
    }

あらかじめdestroyOnExitを呼び出しておくことで、Javaプロセス終了時にプロセスを破棄してくれるようにすることができます(Runtime#addShutdownHookを利用)。

    @Test
    public void test7() throws InterruptedException, TimeoutException, IOException {
        new ProcessExecutor()
                .command("java", "-version")
                .readOutput(true)
                .destroyOnExit()
                .execute();
    }

起動しっぱなしの外部プロセスを扱う場合(ちょっと微妙ですが)などに、役に立つかも?

外部プロセスの終了コードを得る場合。

    @Test
    public void test8() throws InterruptedException, TimeoutException, IOException {
        int returnCode =
                new ProcessExecutor()
                        .command("java", "-version")
                        .destroyOnExit()
                        .execute()
                        .getExitValue();
        assertThat(returnCode).isEqualTo(0);
    }

出力内容も終了コードも欲しい、という場合は、1度ProcessExecutor#executeの結果を捉えましょう。

    @Test
    public void test9() throws InterruptedException, TimeoutException, IOException {
        ProcessResult result =
                new ProcessExecutor()
                        .command("java", "-version")
                        .readOutput(true)
                        .destroyOnExit()
                        .execute();

        assertThat(result.getExitValue()).isEqualTo(0);
        assertThat(result.getOutput().getUTF8())
                .contains("java version")
                .contains("Java(TM) SE Runtime Environment")
                .contains("Java HotSpot(TM)");
    }

ProcessResultから得ることができます。

外部プロセスを非同期に起動する(Future)場合は、ProcessExecutor#executeではなくstartを使用します。

    @Test
    public void test10() throws InterruptedException, TimeoutException, IOException, ExecutionException {
        StartedProcess startedProcess =
                new ProcessExecutor()
                        .command("java", "-version")
                        .readOutput(true)
                        .destroyOnExit()
                        .start();
        Future<ProcessResult> future = startedProcess.getFuture();
        ProcessResult result = future.get();

        assertThat(result.getExitValue()).isEqualTo(0);
        assertThat(result.getOutput().getUTF8())
                .contains("java version")
                .contains("Java(TM) SE Runtime Environment")
                .contains("Java HotSpot(TM)");
        assertThat(startedProcess.getProcess())
                .isInstanceOf(Process.class);
    }

executeの場合は、現在のスレッドで動いていました。

startの場合は戻り値がStartedProcessとなり、Future経由でProcessResultを取得したり、生のProcessのインスタンスそのものを取得できます。

その他、環境変数を渡したり、プロセスの破棄方法のカスタマイズ、プロセスの開始/終了時などに仕込むリスナーなどもあるようです。

APIとしては使いやすい感じがするので、たまに使っていこうかなと。