Guia da API java.lang.ProcessBuilder

Guia da API java.lang.ProcessBuilder

1. Visão geral

OProcess API fornece uma maneira poderosa de executar comandos do sistema operacional em Java. No entanto, ele tem várias opções que podem dificultar o trabalho.

Neste tutorial,we’ll take a look at how Java alleviates that with the ProcessBuilder API.

2. APIProcessBuilder

A classeProcessBuilder fornece métodos para criar e configurar processos do sistema operacional. Each ProcessBuilder instance allows us to manage a collection of process attributes. Podemos então iniciar um novoProcess com os atributos fornecidos.

Aqui estão alguns cenários comuns em que poderíamos usar esta API:

  • Encontre a versão atual do Java

  • Configure um mapa de valor-chave personalizado para o nosso ambiente

  • Altere o diretório de trabalho de onde nosso comando shell está sendo executado

  • Redirecione fluxos de entrada e saída para substituições personalizadas

  • Herdar os dois fluxos do processo atual da JVM

  • Execute um comando shell a partir do código Java

Vamos dar uma olhada em exemplos práticos para cada um deles nas seções posteriores.

Mas antes de mergulharmos no código de trabalho, vamos dar uma olhada no tipo de funcionalidade que esta API oferece.

2.1. Resumo do método

Nesta seção,we’re going to take a step back and briefly look at the most important methods in the ProcessBuilder class. Isso nos ajudará quando mergulharmos em exemplos reais mais tarde:

  • ProcessBuilder(String... command)

    Para criar um novo construtor de processos com o programa e os argumentos do sistema operacional especificados, podemos usar este construtor conveniente.

  • directory(File directory)

    Podemos substituir o diretório de trabalho padrão do processo atual chamando o métododirectorye passando um objetoFile. By default, the current working directory is set to the value returned by the user.dir system property.

  • environment()

    Se quisermos obter as variáveis ​​de ambiente atuais, podemos simplesmente chamar o métodoenvironment. Ele nos retorna uma cópia do ambiente de processo atual usandoSystem.getenv(), mas comoMap.

  • inheritIO()

    Se quisermos especificar que a origem e o destino de nosso padrão de E / S de subprocesso devem ser iguais aos do processo Java atual, podemos usar o métodoinheritIO.

  • redirectInput(File file), redirectOutput(File file), redirectError(File file)

    Quando queremos redirecionar a entrada, saída e destino de erro padrão do construtor de processo para um arquivo, temos esses três métodos de redirecionamento semelhantes à nossa disposição.

  • start()

    Por último, mas não menos importante, para iniciar um novo processo com o que configuramos, simplesmente chamamosstart().

We should note that this class is NOT synchronized. Por exemplo, se temos vários threads acessando uma instânciaProcessBuilder simultaneamente, a sincronização deve ser gerenciada externamente.

3. Exemplos

Agora que temos um entendimento básico da APIProcessBuilder, vamos ver alguns exemplos.

3.1. UsandoProcessBuilder para imprimir a versão do Java

In this first example, we’ll run the java command with one argument in order to get the version.

Process process = new ProcessBuilder("java", "-version").start();

Primeiro, criamos nosso objetoProcessBuilder passando os valores do comando e do argumento para o construtor. Em seguida, iniciamos o processo usando o métodostart() para obter um objetoProcess.

Agora vamos ver como lidar com a saída:

List results = readOutput(process.getInputStream());

assertThat("Results should not be empty", results, is(not(empty())));
assertThat("Results should contain java version: ", results, hasItem(containsString("java version")));

int exitCode = process.waitFor();
assertEquals("No errors should be detected", 0, exitCode);

Aqui estamos lendo a saída do processo e verificando se o conteúdo é o que esperamos. Na etapa final, esperamos que o processo termine usandoprocess.waitFor().

Once the process has finished, the return value tells us whether the process was successful or not.

Alguns pontos importantes a serem lembrados:

  • Os argumentos devem estar na ordem correta

  • Além disso, neste exemplo, o diretório de trabalho e o ambiente padrão são usados

  • Deliberadamente não chamamosprocess.waitFor() até depois de lermos a saída porque o buffer de saída pode paralisar o processo

  • Assumimos que o comandojava está disponível por meio da variávelPATH

3.2. Iniciando um Processo com um Ambiente Modificado

Neste próximo exemplo, veremos como modificar o ambiente de trabalho.

But before we do that let’s begin by taking a look at the kind of information we can find in the default environment:

ProcessBuilder processBuilder = new ProcessBuilder();
Map environment = processBuilder.environment();
environment.forEach((key, value) -> System.out.println(key + value));

Isso simplesmente imprime cada uma das entradas variáveis ​​fornecidas por padrão:

PATH/usr/bin:/bin:/usr/sbin:/sbin
SHELL/bin/bash
...

Agora vamos adicionar uma nova variável de ambiente ao nosso objetoProcessBuilder e executar um comando para gerar seu valor:

environment.put("GREETING", "Hola Mundo");

processBuilder.command("/bin/bash", "-c", "echo $GREETING");
Process process = processBuilder.start();

Vamos decompor as etapas para entender o que fizemos:

  • Adicione uma variável chamada ‘SAUDAÇÃO’ com um valor ‘Hola Mundo’ ao nosso ambiente, que é um padrãoMap<String, String>

  • Desta vez, em vez de usar o construtor, definimos o comando e os argumentos por meio do métodocommand(String… command) diretamente.

  • Em seguida, iniciamos nosso processo conforme o exemplo anterior.

Para concluir o exemplo, verificamos que a saída contém nossa saudação:

List results = readOutput(process.getInputStream());
assertThat("Results should not be empty", results, is(not(empty())));
assertThat("Results should contain java version: ", results, hasItem(containsString("Hola Mundo")));

3.3. Iniciando um processo com um diretório de trabalho modificado

Sometimes it can be useful to change the working directory. Em nosso próximo exemplo, veremos como fazer exatamente isso:

@Test
public void givenProcessBuilder_whenModifyWorkingDir_thenSuccess()
  throws IOException, InterruptedException {
    ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", "ls");

    processBuilder.directory(new File("src"));
    Process process = processBuilder.start();

    List results = readOutput(process.getInputStream());
    assertThat("Results should not be empty", results, is(not(empty())));
    assertThat("Results should contain directory listing: ", results, contains("main", "test"));

    int exitCode = process.waitFor();
    assertEquals("No errors should be detected", 0, exitCode);
}

In the above example, we set the working directory to the project’s src dir using the convenience method directory(File directory). Em seguida, executamos um comando simples de listagem de diretório e verificamos se a saída contém os subdiretóriosmainetest.

3.4. Redirecionando entrada e saída padrão

In the real world, we will probably want to capture the results of our running processes inside a log file for further analysis. Felizmente, a APIProcessBuilder tem suporte integrado exatamente para isso, como veremos neste exemplo.

By default, our process reads input from a pipe. We can access this pipe via the output stream returned by Process.getOutputStream().

No entanto, como veremos em breve, a saída padrão pode ser redirecionada para outra fonte, como um arquivo usando o métodoredirectOutput. In this case, getOutputStream() retornará um*ProcessBuilder.NullOutputStream*.

Vamos voltar ao nosso exemplo original para imprimir a versão do Java. Mas desta vez, vamos redirecionar a saída para um arquivo de log, em vez do canal de saída padrão:

ProcessBuilder processBuilder = new ProcessBuilder("java", "-version");

processBuilder.redirectErrorStream(true);
File log = folder.newFile("java-version.log");
processBuilder.redirectOutput(log);

Process process = processBuilder.start();

No exemplo acima,we create a new temporary file called log and tell our ProcessBuilder to redirect output to this file destination.

Neste último snippet, simplesmente verificamos segetInputStream() é realmentenull e se o conteúdo do nosso arquivo é o esperado:

assertEquals("If redirected, should be -1 ", -1, process.getInputStream().read());
List lines = Files.lines(log.toPath()).collect(Collectors.toList());
assertThat("Results should contain java version: ", lines, hasItem(containsString("java version")));

Now let’s take a look at a slight variation on this example. For example when we wish to append to a log file rather than create a new one each time:

File log = tempFolder.newFile("java-version-append.log");
processBuilder.redirectErrorStream(true);
processBuilder.redirectOutput(Redirect.appendTo(log));

Também é importante mencionar a chamada pararedirectErrorStream(true). Em caso de erros, a saída de erro será mesclada no arquivo de saída do processo normal.

Naturalmente, podemos especificar arquivos individuais para a saída padrão e a saída de erro padrão:

File outputLog = tempFolder.newFile("standard-output.log");
File errorLog = tempFolder.newFile("error.log");

processBuilder.redirectOutput(Redirect.appendTo(outputLog));
processBuilder.redirectError(Redirect.appendTo(errorLog));

3.5. Herdando a E / S do Processo Atual

Neste penúltimo exemplo, veremos o métodoinheritIO() em ação. We can use this method when we want to redirect the sub-process I/O to the standard I/O of the current process:

@Test
public void givenProcessBuilder_whenInheritIO_thenSuccess() throws IOException, InterruptedException {
    ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", "echo hello");

    processBuilder.inheritIO();
    Process process = processBuilder.start();

    int exitCode = process.waitFor();
    assertEquals("No errors should be detected", 0, exitCode);
}

No exemplo acima, usando o métodoinheritIO(), vemos a saída de um comando simples no console em nosso IDE.

Na próxima seção, daremos uma olhada nas adições feitas à APIProcessBuilder no Java 9.

4. Adições Java 9

Java 9 introduziu o conceito de pipelines na APIProcessBuilder:

public static List startPipeline​(List builders)

Usando o métodostartPipeline, podemos passar uma lista de objetosProcessBuilder. Este método estático iniciará umProcess para cadaProcessBuilder. Assim, criando um pipeline de processos que são vinculados por sua saída padrão e fluxos de entrada padrão.

Por exemplo, se queremos executar algo parecido com isto:

find . -name *.java -type f | wc -l

O que faríamos é criar um construtor de processo para cada comando isolado e compô-los em um pipeline:

@Test
public void givenProcessBuilder_whenStartingPipeline_thenSuccess()
  throws IOException, InterruptedException {
    List builders = Arrays.asList(
      new ProcessBuilder("find", "src", "-name", "*.java", "-type", "f"),
      new ProcessBuilder("wc", "-l"));

    List processes = ProcessBuilder.startPipeline(builders);
    Process last = processes.get(processes.size() - 1);

    List output = readOutput(last.getInputStream());
    assertThat("Results should not be empty", output, is(not(empty())));
}

Neste exemplo, estamos pesquisando todos os arquivos java dentro do diretóriosrc e canalizando os resultados para outro processo para contá-los.

Para saber mais sobre outras melhorias feitas na API de processo em Java 9, consulte nosso excelente artigo emJava 9 Process API Improvements.

5. Conclusão

Para resumir, neste tutorial, exploramos a APIjava.lang.ProcessBuilder em detalhes.

Primeiro, começamos explicando o que pode ser feito com a API e resumimos os métodos mais importantes.

Em seguida, examinamos vários exemplos práticos. Por fim, analisamos quais novas adições foram introduzidas na API no Java 9.

Como sempre, o código-fonte completo do artigo está disponívelover on GitHub.