Processamento em lote do Java EE 7
1. Introdução
Imagine que tivemos que concluir manualmente tarefas como processamento de holerites, cálculo de juros e geração de faturas. Seria muito chato, propenso a erros e uma lista interminável de tarefas manuais!
Neste tutorial, daremos uma olhada no Java Batch Processing (JSR 352), uma parte da plataforma Jakarta EE e uma ótima especificação para automatizar tarefas como essas. It offers application developers a model for developing robust batch processing systems so that they can focus on the business logic.
2. Dependências do Maven
Como JSR 352 é apenas uma especificação, precisaremos incluirits API eimplementation, comojberet:
javax.batch
javax.batch-api
1.0.1
org.jberet
jberet-core
1.0.2.Final
org.jberet
jberet-support
1.0.2.Final
org.jberet
jberet-se
1.0.2.Final
Também adicionaremos um banco de dados na memória para que possamos olhar alguns cenários mais realistas.
3. Conceitos chave
O JSR 352 apresenta alguns conceitos, que podemos ver da seguinte maneira:
Vamos primeiro definir cada peça:
-
Começando pela esquerda, temos oJobOperator. Émanages all aspects of job processing such as starting, stopping, and restarting
-
Em seguida, temos oJob. Um trabalho é uma coleção lógica de etapas; ele encapsula um processo em lote inteiro
-
Um trabalho conterá entre 1 e nSteps. Cada etapa é uma unidade de trabalho seqüencial e independente. Uma etapa é composta dereading input,processing dessa entrada ewriting output
-
E por último, mas não menos importante, temos oJobRepository que armazena as informações em execução dos jobs. Ajuda a acompanhar os trabalhos, seu estado e seus resultados de conclusão
As etapas têm um pouco mais de detalhes do que isso, então vamos dar uma olhada nisso a seguir. Primeiro, veremos as etapas deChunk e, em seguida,Batchlets.
4. Criando um Chunk
Como afirmado anteriormente, um pedaço é uma espécie de etapa. Frequentemente usaremos um pedaço para expressar uma operação que é executada repetidamente, digamos sobre um conjunto de itens. É como operações intermediárias do Java Streams.
Ao descrever um pedaço, precisamos expressar de onde pegar itens, como processá-los e para onde enviá-los posteriormente.
4.1. Lendo itens
Para ler os itens, precisamos implementarItemReader.
Nesse caso, criaremos um leitor que simplesmente emitirá os números de 1 a 10:
@Named
public class SimpleChunkItemReader extends AbstractItemReader {
private Integer[] tokens;
private Integer count;
@Inject
JobContext jobContext;
@Override
public Integer readItem() throws Exception {
if (count >= tokens.length) {
return null;
}
jobContext.setTransientUserData(count);
return tokens[count++];
}
@Override
public void open(Serializable checkpoint) throws Exception {
tokens = new Integer[] { 1,2,3,4,5,6,7,8,9,10 };
count = 0;
}
}
Agora, estamos apenas lendo o estado interno da classe aqui. Mas, é claro,readItem could pull from a database, do sistema de arquivos ou alguma outra fonte externa.
Observe que estamos salvando parte desse estado interno usandoJobContext#setTransientUserData(), que será útil mais tarde.
Also, note the checkpoint parameter. Vamos retomar isso também.
4.2. Processando itens
Obviamente, a razão pela qual estamos dividindo é que queremos realizar algum tipo de operação em nossos itens!
Sempre que retornarmosnull de um processador de item, retiramos esse item do lote.
Então, digamos aqui que queremos manter apenas os números pares. Podemos usar umItemProcessor que rejeita os ímpares retornandonull:
@Named
public class SimpleChunkItemProcessor implements ItemProcessor {
@Override
public Integer processItem(Object t) {
Integer item = (Integer) t;
return item % 2 == 0 ? item : null;
}
}
processItem será chamado uma vez para cada item que nossoItemReader emite.
4.3. Escrevendo itens
Finalmente, o trabalho invocaráItemWriter para que possamos escrever nossos itens transformados:
@Named
public class SimpleChunkWriter extends AbstractItemWriter {
List processed = new ArrayList<>();
@Override
public void writeItems(List
How long is items? Em um momento, definiremos o tamanho de um bloco, que determinará o tamanho da lista que é enviada parawriteItems.
4.4. Definindo um pedaço em um trabalho
Agora, reunimos tudo isso em um arquivo XML usando JSL ou Job Specification Language. Observe que listaremos nosso leitor, processador, bloco e também um tamanho de bloco:
The chunk size is how often progress in the chunk is committed to the job repository, que é importante para garantir a conclusão, caso parte do sistema falhe.
Precisamos colocar este arquivo emMETA-INF/batch-jobs para.jar files e emWEB-INF/classes/META-INF/batch-jobs para.war arquivos.
Demos ao nosso trabalho o id“simpleChunk”, , então vamos tentar isso em um teste de unidade.
Now, jobs are executed asynchronously, which makes them tricky to test. Na amostra, certifique-se de verificar nossoBatchTestHelper which pesquisa e espera até que o trabalho seja concluído:
@Test
public void givenChunk_thenBatch_completesWithSuccess() throws Exception {
JobOperator jobOperator = BatchRuntime.getJobOperator();
Long executionId = jobOperator.start("simpleChunk", new Properties());
JobExecution jobExecution = jobOperator.getJobExecution(executionId);
jobExecution = BatchTestHelper.keepTestAlive(jobExecution);
assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}
Então é isso que os pedaços são. Agora, vamos dar uma olhada nos batchlets.
5. Criando um lote
Nem tudo se encaixa perfeitamente em um modelo iterativo. Por exemplo, podemos ter uma tarefa que simplesmente precisamos parainvoke once, run to completion, and return an exit status.
O contrato para um lote é bastante simples:
@Named
public class SimpleBatchLet extends AbstractBatchlet {
@Override
public String process() throws Exception {
return BatchStatus.COMPLETED.toString();
}
}
Como é o JSL:
E podemos testá-lo usando a mesma abordagem de antes:
@Test
public void givenBatchlet_thenBatch_completeWithSuccess() throws Exception {
JobOperator jobOperator = BatchRuntime.getJobOperator();
Long executionId = jobOperator.start("simpleBatchLet", new Properties());
JobExecution jobExecution = jobOperator.getJobExecution(executionId);
jobExecution = BatchTestHelper.keepTestAlive(jobExecution);
assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}
Então, vimos algumas maneiras diferentes de implementar etapas.
Agora vamos dar uma olhada nos mecanismos paramarking and guaranteeing progress.
6. Ponto de verificação personalizado
Falhas provavelmente acontecerão no meio de um trabalho. Should we just start over the whole thing, or can we somehow start where we left off?
Como o nome sugere,checkpoints nos ajuda a definir periodicamente um marcador em caso de falha.
By default, the end of chunk processing is a natural checkpoint.
No entanto, podemos personalizá-lo com nosso próprioCheckpointAlgorithm:
@Named
public class CustomCheckPoint extends AbstractCheckpointAlgorithm {
@Inject
JobContext jobContext;
@Override
public boolean isReadyToCheckpoint() throws Exception {
int counterRead = (Integer) jobContext.getTransientUserData();
return counterRead % 5 == 0;
}
}
Lembra da contagem que colocamos nos dados transitórios anteriormente? Aqui,we can pull it out with JobContext#getTransientUserData to afirma que queremos comprometer a cada 5º número processado.
Sem isso, um commit aconteceria no final de cada bloco ou, no nosso caso, a cada 3º número.
And then, we match that up with the checkout-algorithm directive in our XML underneath our chunk:
Vamos testar o código, novamente observando que algumas das etapas padronizadas estão escondidas emBatchTestHelper:
@Test
public void givenChunk_whenCustomCheckPoint_thenCommitCountIsThree() throws Exception {
// ... start job and wait for completion
jobOperator.getStepExecutions(executionId)
.stream()
.map(BatchTestHelper::getCommitCount)
.forEach(count -> assertEquals(3L, count.longValue()));
assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}
Portanto, podemos esperar uma contagem de confirmação de 2, pois temos dez itens e configuramos os commits para cada quinto item. Mas,the framework does one more final read commit at the end para garantir que tudo foi processado, que é o que nos traz até 3.
A seguir, vamos ver como lidar com erros.
7. Manipulação de exceção
Por padrão,the job operator will mark our job as FAILED in case of an exception.
Vamos mudar nosso leitor de item para ter certeza de que ele falha:
@Override
public Integer readItem() throws Exception {
if (tokens.hasMoreTokens()) {
String tempTokenize = tokens.nextToken();
throw new RuntimeException();
}
return null;
}
E depois teste:
@Test
public void whenChunkError_thenBatch_CompletesWithFailed() throws Exception {
// ... start job and wait for completion
assertEquals(jobExecution.getBatchStatus(), BatchStatus.FAILED);
}
Mas, podemos substituir esse comportamento padrão de várias maneiras:
-
[.s1]#skip-limit # especifica o número de exceções que esta etapa irá ignorar antes de falhar
-
retry-limit especifica o número de vezes que o operador de tarefa deve repetir a etapa antes de falhar
-
[.s1]#skippable-exception-class # especifica um conjunto de exceções que o processamento do bloco irá ignorar
Portanto, podemos editar nosso trabalho para que ele ignoreRuntimeException, bem como alguns outros, apenas para ilustração:
E agora nosso código passará:
@Test
public void givenChunkError_thenErrorSkipped_CompletesWithSuccess() throws Exception {
// ... start job and wait for completion
jobOperator.getStepExecutions(executionId).stream()
.map(BatchTestHelper::getProcessSkipCount)
.forEach(skipCount -> assertEquals(1L, skipCount.longValue()));
assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}
8. Execução de várias etapas
Mencionamos anteriormente que um trabalho pode ter qualquer número de etapas, então vamos ver isso agora.
8.1. Disparando a próxima etapa
Por padrão,each step is the last step in the job.
Para executar a próxima etapa em um trabalho em lote, teremos que especificar explicitamente usando o atributonext na definição da etapa:
Se esquecermos esse atributo, a próxima etapa da sequência não será executada.
E podemos ver como isso se parece na API:
@Test
public void givenTwoSteps_thenBatch_CompleteWithSuccess() throws Exception {
// ... start job and wait for completion
assertEquals(2 , jobOperator.getStepExecutions(executionId).size());
assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}
8.2. Fluxos
Uma sequência de etapas também pode ser encapsulada emflow. When the flow is finished, it is the entire flow that transitions to the execution element. Além disso, os elementos dentro do fluxo não podem fazer a transição para elementos fora do fluxo.
Podemos, por exemplo, executar duas etapas dentro de um fluxo e fazer a transição desse fluxo para uma etapa isolada:
E ainda podemos ver cada execução de etapa independentemente:
@Test
public void givenFlow_thenBatch_CompleteWithSuccess() throws Exception {
// ... start job and wait for completion
assertEquals(3, jobOperator.getStepExecutions(executionId).size());
assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}
8.3. Decisões
Também temos suporte if / else na forma dedecisions. Decisions providea customized way of determining a sequence among steps, flows, and splits.
Como as etapas, ele funciona em elementos de transição comonext, que podem direcionar ou encerrar a execução do trabalho.
Vamos ver como o trabalho pode ser configurado:
Qualquer elementodecision precisa ser configurado com uma classe que implementeDecider. Sua função é retornar uma decisão comoString.
Cadanext dentro dedecision é como uma instruçãocase en umswitch .
8.4. Divisões
Splits são úteis, pois nos permitem executar fluxos simultaneamente:
Claro,this means that the order isn’t guaranteed.
Vamos confirmar se todos eles ainda são executados. The flow steps will be performed in an arbitrary order, but the isolated step will always be last:
@Test
public void givenSplit_thenBatch_CompletesWithSuccess() throws Exception {
// ... start job and wait for completion
List stepExecutions = jobOperator.getStepExecutions(executionId);
assertEquals(3, stepExecutions.size());
assertEquals("splitJobSequenceStep3", stepExecutions.get(2).getStepName());
assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}
9. Particionando um Trabalho
Também podemos consumir as propriedades do lote em nosso código Java que foram definidas em nosso trabalho.
They can be scoped at three levels – the job, the step, and the batch-artifact.
Vamos ver alguns exemplos de como eles consumiram.
Quando queremos consumir as propriedades no nível do trabalho:
@Inject
JobContext jobContext;
...
jobProperties = jobContext.getProperties();
...
Isso também pode ser consumido em um nível de etapa:
@Inject
StepContext stepContext;
...
stepProperties = stepContext.getProperties();
...
Quando queremos consumir as propriedades no nível de artefato em lote:
@Inject
@BatchProperty(name = "name")
private String nameString;
Isso é útil em partições.
Veja, com divisões, podemos executar fluxos simultaneamente. But we can also partition a step into n sets of items or set separate inputs, allowing us another way to split up the work across multiple threads.
Para compreender o segmento de trabalho que cada partição deve realizar, podemos combinar propriedades com partições:
10. Parar e reiniciar
Agora, isso é para definir empregos. Agora vamos falar um pouco sobre como gerenciá-los.
Já vimos em nossos testes de unidade que podemos obter uma instância deJobOperator deBatchRuntime:
JobOperator jobOperator = BatchRuntime.getJobOperator();
E então, podemos começar o trabalho:
Long executionId = jobOperator.start("simpleBatchlet", new Properties());
No entanto, também podemos interromper o trabalho:
jobOperator.stop(executionId);
E, finalmente, podemos reiniciar o trabalho:
executionId = jobOperator.restart(executionId, new Properties());
Vamos ver como podemos interromper um job em execução:
@Test
public void givenBatchLetStarted_whenStopped_thenBatchStopped() throws Exception {
JobOperator jobOperator = BatchRuntime.getJobOperator();
Long executionId = jobOperator.start("simpleBatchLet", new Properties());
JobExecution jobExecution = jobOperator.getJobExecution(executionId);
jobOperator.stop(executionId);
jobExecution = BatchTestHelper.keepTestStopped(jobExecution);
assertEquals(jobExecution.getBatchStatus(), BatchStatus.STOPPED);
}
E se um lote forSTOPPED, podemos reiniciá-lo:
@Test
public void givenBatchLetStopped_whenRestarted_thenBatchCompletesSuccess() {
// ... start and stop the job
assertEquals(jobExecution.getBatchStatus(), BatchStatus.STOPPED);
executionId = jobOperator.restart(jobExecution.getExecutionId(), new Properties());
jobExecution = BatchTestHelper.keepTestAlive(jobOperator.getJobExecution(executionId));
assertEquals(jobExecution.getBatchStatus(), BatchStatus.COMPLETED);
}
11. Buscando empregos
Quando um trabalho em lote é enviado,the batch runtime creates an instance of JobExecution to track it.
Para obter oJobExecution para um id de execução, podemos usar o métodoJobOperator#getJobExecution(executionId).
E,StepExecution provides helpful information for tracking a step’s execution.
Para obter oStepExecution para um id de execução, podemos usar o métodoJobOperator#getStepExecutions(executionId).
E a partir disso, podemos obterseveral metrics sobre a etapa por meio deStepExecution#getMetrics:
@Test
public void givenChunk_whenJobStarts_thenStepsHaveMetrics() throws Exception {
// ... start job and wait for completion
assertTrue(jobOperator.getJobNames().contains("simpleChunk"));
assertTrue(jobOperator.getParameters(executionId).isEmpty());
StepExecution stepExecution = jobOperator.getStepExecutions(executionId).get(0);
Map metricTest = BatchTestHelper.getMetricsMap(stepExecution.getMetrics());
assertEquals(10L, metricTest.get(Metric.MetricType.READ_COUNT).longValue());
assertEquals(5L, metricTest.get(Metric.MetricType.FILTER_COUNT).longValue());
assertEquals(4L, metricTest.get(Metric.MetricType.COMMIT_COUNT).longValue());
assertEquals(5L, metricTest.get(Metric.MetricType.WRITE_COUNT).longValue());
// ... and many more!
}
12. Desvantagens
O JSR 352 é poderoso, embora falte em várias áreas:
-
Parece haver falta de leitores e escritores que podem processar outros formatos, como JSON
-
Não há suporte para genéricos
-
O particionamento suporta apenas uma única etapa
-
A API não oferece nada para oferecer suporte ao agendamento (embora o J2EE tenha um módulo de agendamento separado)
-
Devido à sua natureza assíncrona, o teste pode ser um desafio
-
A API é bastante detalhada
13. Conclusão
Neste artigo, analisamos o JSR 352 e aprendemos sobre pedaços, lotes, divisões, fluxos e muito mais. No entanto, mal arranhamos a superfície.
Como sempre, o código de demonstração pode ser encontradoover on GitHub.