Create the Transfer service
Now, you will create another new Spring Boot microservice application and implement the Transfer Service. This service will initiate the LRA and act as the logical coordinator - it will call the deposit and withdraw services you just implemented to effect the transfer to process the Cloud Cash Payment.
-
Create a new Java Project for the
transfer
service.In the Explorer of VS Code open
Java Project
and click the plus sign to add a Java Project to your workspace.Select Spring Boot Project.
Select Maven Project.
Specify
3.3.4
as the Spring Boot version.Use
com.example
as the Group Id.Enter
transfer
as the Artifact Id.Use
JAR
as the Packaging Type.Select Java version
21
.Search for
Spring Web
and press EnterPress Enter to continue and create the Java Project
Select the
root
location for your project e.g. side by side with thechecks
,testrunner
andaccounts
projects.When the project opens click Add to Workspace
-
Add MicroTX and Lombok to the
pom.xml
fileOpen the
pom.xml
file in thetransfer
project. Add the following to the pom.xml:<dependency> <groupId>com.oracle.microtx.lra</groupId> <artifactId>microtx-lra-spring-boot-starter</artifactId> <version>23.4.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
-
Create the Spring Boot application configuration
In the
transfer
project, rename the file calledapplication.properties
toapplication.yaml
located in thesrc/main/resources
. This will be the Spring Boot application configuration file. In this file you need to configure the endpoints for the LRA participants and coordinator.spring: application: name: transfer mvc: enforced-prefixes: - /actuator - /rest url-mappings: - "/rest/*" - "/actuator/*" - "/error/*" microtx: lra: coordinator-url: ${MP_LRA_COORDINATOR_URL} propagation-active: true headers-propagation-prefix: "{x-b3-, oracle-tmm-, authorization, refresh-}" account: deposit: url: http://account.application:8080/deposit withdraw: url: http://account.application:8080/withdraw transfer: cancel: url: http://transfer.application:8080/cancel process: url: http://transfer.application:8080/processcancel confirm: url: http://transfer.application:8080/confirm process: url: http://transfer.application:8080/processconfirm
-
Create the Transfer service
You are now ready to implement the main logic for the Cloud Cash Payment/transfer LRA. You will implement this in a new Java file called
TransferService.java
insrc/main/java/com/example/transfer
. Here are the imports you will need for this class and the member variables. Note that this class has the@RestController
and@RequestMapping
annotations, as you saw previously in the Account project, to set up the URL context root for the service.package com.example.transfer; import java.net.URI; import com.oracle.microtx.springboot.lra.annotation.Compensate; import com.oracle.microtx.springboot.lra.annotation.Complete; import com.oracle.microtx.springboot.lra.annotation.LRA; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import static com.oracle.microtx.springboot.lra.annotation.LRA.LRA_HTTP_CONTEXT_HEADER; @RestController @RequestMapping("/") @Slf4j public class TransferService { public static final String TRANSFER_ID = "TRANSFER_ID"; @Value("${account.withdraw.url}") URI withdrawUri; @Value("${account.deposit.url}") URI depositUri; @Value("${transfer.cancel.url}") URI transferCancelUri; @Value("${transfer.cancel.process.url}") URI transferProcessCancelUri; @Value("${transfer.confirm.url}") URI transferConfirmUri; @Value("${transfer.confirm.process.url}") URI transferProcessConfirmUri; }
-
Create the transfer endpoint
This is the main entry point for the LRA. When a client calls this method, a new LRA will be started. The
@LRA
annotation with thevalue
property set toLRA.Type.REQUIRES_NEW
instructs the interceptors/filters to contact Oracle Transaction Manager for Microservices to start a new LRA instance and obtain the LRA ID, which will be injected into theLRA_HTTP_CONTEXT_HEADER
HTTP header. Note that theend
property is set tofalse
which means there will be other actions and participants before the LRA is completed.This method will accept three parameters from the caller, in JSON format in the HTTP body:
fromAccount
is the account from which the funds are to be withdrawn,toAccount
is the account into which the funds are to be deposited, andamount
is the amount to transfer.In the method body, you should first check if the
lraId
was set. If it is null, that indicates that there was some error trying to create the new LRA instance, and you should return an error response and stop.After that, you want to perform the withdrawal, check if it worked, and if so, perform the deposit, and then check if that worked, and if so “complete” the LRA. If there were any failures, compensate the LRA.
/** * Transfer amount between two accounts. * @param fromAccount From an account * @param toAccount To an account * @param amount Amount to transfer * @param lraId LRA Id * @return TO-DO */ @PostMapping("/transfer") @LRA(value = LRA.Type.REQUIRES_NEW, end = false) public ResponseEntity<String> transfer(@RequestParam("fromAccount") long fromAccount, @RequestParam("toAccount") long toAccount, @RequestParam("amount") long amount, @RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId) { if (lraId == null) { return new ResponseEntity<>("Failed to create LRA", HttpStatus.INTERNAL_SERVER_ERROR); } log.info("Started new LRA/transfer Id: " + lraId); boolean isCompensate = false; String returnString = ""; // perform the withdrawal returnString += withdraw(lraId, fromAccount, amount); log.info(returnString); if (returnString.contains("succeeded")) { // if it worked, perform the deposit returnString += " " + deposit(lraId, toAccount, amount); log.info(returnString); if (returnString.contains("failed")) { isCompensate = true; // deposit failed } } else { isCompensate = true; // withdraw failed } log.info("LRA/transfer action will be " + (isCompensate ? "cancel" : "confirm")); // call complete or cancel based on outcome of previous actions RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); headers.set(TRANSFER_ID, lraId); HttpEntity<String> request = new HttpEntity<String>("", headers); ResponseEntity<String> response = restTemplate.postForEntity( (isCompensate ? transferCancelUri : transferConfirmUri).toString(), request, String.class); returnString += response.getBody(); // return status return ResponseEntity.ok("transfer status:" + returnString); }
-
Create a method to perform the withdrawal
This method should perform the withdrawal by calling the Withdraw service in the Account Spring Boot application. The
lraId
,accountId
andamount
need to be passed to the service, and you must set theLRA_HTTP_CONTEXT_HEADER
to the LRA ID.private String withdraw(String lraId, long accountId, long amount) { log.info("withdraw accountId = " + accountId + ", amount = " + amount); log.info("withdraw lraId = " + lraId); UriComponentsBuilder builder = UriComponentsBuilder.fromUri(withdrawUri) .queryParam("accountId", accountId) .queryParam("amount", amount); RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); headers.set(LRA_HTTP_CONTEXT_HEADER, lraId.toString()); HttpEntity<String> request = new HttpEntity<String>("", headers); ResponseEntity<String> response = restTemplate.postForEntity( builder.buildAndExpand().toUri(), request, String.class); return response.getBody(); }
-
Create a method to perform the deposit
This method is similar the previous one, no new concepts are introduced here.
private String deposit(String lraId, long accountId, long amount) { log.info("deposit accountId = " + accountId + ", amount = " + amount); log.info("deposit lraId = " + lraId); UriComponentsBuilder builder = UriComponentsBuilder.fromUri(depositUri) .queryParam("accountId", accountId) .queryParam("amount", amount); RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); headers.set(LRA_HTTP_CONTEXT_HEADER, lraId.toString()); HttpEntity<String> request = new HttpEntity<String>("", headers); ResponseEntity<String> response = restTemplate.postForEntity( builder.buildAndExpand().toUri(), request, String.class); return response.getBody(); }
-
Create a method to process the confirm action for this participant
This participant does not need to take any actions for the confirm action, so just return a successful response.
@PostMapping("/processconfirm") @LRA(value = LRA.Type.MANDATORY) public ResponseEntity<String> processconfirm(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId) { log.info("Process confirm for transfer : " + lraId); return ResponseEntity.ok(""); }
-
Create a method to process the cancel action for this participant
This participant does not need to take any actions for the cancel action, so just return a successful response.
@PostMapping("/processcancel") @LRA(value = LRA.Type.MANDATORY, cancelOn = HttpStatus.OK) public ResponseEntity<String> processcancel(@RequestHeader(LRA_HTTP_CONTEXT_HEADER) String lraId) { log.info("Process cancel for transfer : " + lraId); return ResponseEntity.ok(""); }
-
Create the confirm and cancel methods
The logic demonstrated in these two methods would probably be in a client in a real-life LRA, but is included here for instructional purposes and convenience.
The
transfer
method makes a REST call to confirm (or cancel) at the end of its processing. The confirm or cancel method suspends the LRA (using theNOT_SUPPORTED
value
in the@LRA
annotation). Then the confirm or cancel method will make a REST call toprocessconfirm
orprocesscancel
which import the LRA with theirMANDATORY
annotation and then implicitly end the LRA accordingly upon returning./** * Confirm a transfer. * @param transferId Transfer Id * @return TO-DO */ @PostMapping("/confirm") @Complete @LRA(value = LRA.Type.NOT_SUPPORTED) public ResponseEntity<String> confirm(@RequestHeader(TRANSFER_ID) String transferId) { log.info("Received confirm for transfer : " + transferId); RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); headers.set(LRA_HTTP_CONTEXT_HEADER, transferId); HttpEntity<String> request = new HttpEntity<String>("", headers); ResponseEntity<String> response = restTemplate.postForEntity( transferProcessConfirmUri, request, String.class); return ResponseEntity.ok(response.getBody()); } /** * Cancel a transfer. * @param transferId Transfer Id * @return TO-DO */ @PostMapping("/cancel") @Compensate @LRA(value = LRA.Type.NOT_SUPPORTED, cancelOn = HttpStatus.OK) public ResponseEntity<String> cancel(@RequestHeader(TRANSFER_ID) String transferId) { log.info("Received cancel for transfer : " + transferId); RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); headers.set(LRA_HTTP_CONTEXT_HEADER, transferId); HttpEntity<String> request = new HttpEntity<String>("", headers); ResponseEntity<String> response = restTemplate.postForEntity( transferProcessCancelUri, request, String.class); return ResponseEntity.ok(response.getBody()); }
That completes the Transfer service and application.