Guia para Java EE JTA

Guia para Java EE JTA

1. Visão geral

Java Transaction API, mais comumente conhecido como JTA,is an API for managing transactions in Java. Ele nos permite iniciar, confirmar e reverter transações de uma forma agnóstica de recursos.

O verdadeiro poder do JTA reside em sua capacidade de gerenciar vários recursos (ou seja, bancos de dados, serviços de mensagens) em uma única transação.

Neste tutorial, vamos conhecer JTA no nível conceitual e ver como o código de negócios normalmente interage com JTA.

2. API universal e transação distribuída

O JTA fornece uma abstração sobre o controle de transação (início, confirmação e reversão) para o código comercial.

Na ausência dessa abstração, teríamos que lidar com as APIs individuais de cada tipo de recurso.

Por exemplo, precisamos lidar com o recurso JDBClike this. Da mesma forma, um recurso JMS pode ter umsimilar but incompatible model.

Com JTA, podemosmanage multiple resources of different types in a consistent and coordinated manner.

Como uma API, JTA define interfaces e semânticas a serem implementadas portransaction managers. As implementações são fornecidas por bibliotecas comoNarayanaeBitronix.

3. Exemplo de configuração de projeto

O aplicativo de amostra é um serviço de backend muito simples de um aplicativo bancário. Temos dois serviços, oBankAccountService andAuditService usando dois bancos de dados diferentesThese independent databases need to be coordinated upon transaction begin, commit or rollback.

Para começar, nosso projeto de amostra usa o Spring Boot para simplificar a configuração:


    org.springframework.boot
    spring-boot-starter-parent
    2.0.4.RELEASE



    org.springframework.boot
    spring-boot-starter-jta-bitronix

Finalmente, antes de cada método de teste, inicializamosAUDIT_LOG com dados vazios e o banco de dadosACCOUNT com 2 linhas:

+-----------+----------------+
| ID        |  BALANCE       |
+-----------+----------------+
| a0000001  |  1000          |
| a0000002  |  2000          |
+-----------+----------------+

4. Demarcação Declarativa de Transação

A primeira forma de trabalhar com transações em JTA é com o uso da anotação@Transactional. Para uma explicação e configuração mais elaboradas, consultethis article.

Vamos anotar o método de serviço de fachadaexecuteTranser() com@Transactional.  Isso instruitransaction manager a iniciar uma transação:

@Transactional
public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) {
    bankAccountService.transfer(fromAccontId, toAccountId, amount);
    auditService.log(fromAccontId, toAccountId, amount);
    ...
}

Aqui, o métodoexecuteTranser() chama 2 serviços diferentes,AccountServiceeAuditService. Esses serviços usam 2 bancos de dados diferentes.

QuandoexecuteTransfer() retorna,the transaction manager recognizes that it is the end of the transaction and will commit to both databases:

tellerService.executeTransfer("a0000001", "a0000002", BigDecimal.valueOf(500));
assertThat(accountService.balanceOf("a0000001"))
  .isEqualByComparingTo(BigDecimal.valueOf(500));
assertThat(accountService.balanceOf("a0000002"))
  .isEqualByComparingTo(BigDecimal.valueOf(2500));

TransferLog lastTransferLog = auditService
  .lastTransferLog();
assertThat(lastTransferLog)
  .isNotNull();
assertThat(lastTransferLog.getFromAccountId())
  .isEqualTo("a0000001");
assertThat(lastTransferLog.getToAccountId())
  .isEqualTo("a0000002");
assertThat(lastTransferLog.getAmount())
  .isEqualByComparingTo(BigDecimal.valueOf(500));

4.1. Revertendo na Demarcação Declarativa

No final do método,executeTransfer() verifica o saldo da conta e lançaRuntimeException se o fundo de origem for insuficiente:

@Transactional
public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) {
    bankAccountService.transfer(fromAccontId, toAccountId, amount);
    auditService.log(fromAccontId, toAccountId, amount);
    BigDecimal balance = bankAccountService.balanceOf(fromAccontId);
    if(balance.compareTo(BigDecimal.ZERO) < 0) {
        throw new RuntimeException("Insufficient fund.");
    }
}

Umunhandled RuntimeException past the first @Transactional will rollback the transactionto both databases. Na verdade, a execução de uma transferência com um valor maior que o saldo causará um rollback:

assertThatThrownBy(() -> {
    tellerService.executeTransfer("a0000002", "a0000001", BigDecimal.valueOf(10000));
}).hasMessage("Insufficient fund.");

assertThat(accountService.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(1000));
assertThat(accountService.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2000));
assertThat(auditServie.lastTransferLog()).isNull();

5. Demarcação de transação programática

Outra maneira de controlar a transação JTA é programaticamente por meio deUserTransaction.

Agora vamos modificarexecuteTransfer() para lidar com a transação manualmente:

userTransaction.begin();

bankAccountService.transfer(fromAccontId, toAccountId, amount);
auditService.log(fromAccontId, toAccountId, amount);
BigDecimal balance = bankAccountService.balanceOf(fromAccontId);
if(balance.compareTo(BigDecimal.ZERO) < 0) {
    userTransaction.rollback();
    throw new RuntimeException("Insufficient fund.");
} else {
    userTransaction.commit();
}

Em nosso exemplo, o métodobegin() inicia uma nova transação. Se a validação do saldo falhar, chamaremosrollback(), que fará rollback em ambos os bancos de dados. Caso contrário,the call to commit()commits the changes to both databases.

É importante observar quecommit() erollback() encerram a transação atual.

Por fim, o uso da demarcação programática nos dá a flexibilidade do controle de transações refinado.

6. Conclusão

Neste artigo, discutimos o problema que o JTA tenta resolver. Os exemplos de códigoillustrate controlling transaction with annotations and programmatically, envolvendo 2 recursos transacionais que precisam ser coordenados em uma única transação.

Como de costume, o exemplo de código pode ser encontradoover on GitHub.