CompletableFuture and its power
- Mark Kendall
- Mar 4
- 6 min read
Okay, let's break down CompletableFuture in Java:
What is CompletableFuture?
CompletableFuture is a class in the java.util.concurrent package.
It represents a Future that may be explicitly completed (setting its value and status).
It also implements the CompletionStage interface, which enables it to support dependent functions and actions that trigger upon its completion.
In essence, it's a powerful tool for writing asynchronous, non-blocking code in Java.1
When was it introduced?
CompletableFuture was introduced in Java 8.
What is its "power"?
The "power" of CompletableFuture lies in its ability to:
Asynchronous Computation:
It allows you to perform computations asynchronously, meaning you can execute tasks in separate threads without blocking the main thread.2 This improves application responsiveness and performance.
Composition:
It provides methods for chaining and combining asynchronous operations.3 You can create complex workflows by linking multiple CompletableFuture instances together.
Non-Blocking Operations:
Unlike the older Future interface, CompletableFuture provides non-blocking methods for retrieving results and handling completions.
Error Handling:
It offers robust mechanisms for handling exceptions that occur during asynchronous computations.4
Flexibility:
It provides a wide range of methods for controlling the completion of futures, handling timeouts, and customizing execution.
Improved concurrency:
It greatly simplifies the coding of concurrent operations, making it much easier to create scalable and efficient applications.5
Key Features and Benefits:
Non-blocking code: Enables applications to remain responsive while performing long-running tasks.6
Chaining operations: Simplifies the creation of complex asynchronous workflows.7
Error handling: Provides a clean and effective way to manage exceptions in asynchronous code.8
Parallel processing: Allows for efficient utilization of multi-core processors.9
In simple terms, CompletableFuture makes it much easier to write code that can do multiple things at once, without having to wait for each thing to finish before starting the next. This leads to faster and more efficient applications.
```java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@SpringBootApplication
public class ApiCallExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ApiCallExampleApplication.class, args);
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
@RestController
class ApiController {
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public ApiController(RestTemplate restTemplate, ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}
@GetMapping("/combinedData")
public CustomerAccountDto getCombinedData() throws ExecutionException, InterruptedException, IOException {
CompletableFuture<CustomerDto> customerFuture = CompletableFuture.supplyAsync(() -> {
try {
// Simulate API call or file read
//String customerJson = restTemplate.getForObject("http://customer-api/customer", String.class);
JsonNode customerNode = objectMapper.readTree(new File("customer.json")); // Read from file
return objectMapper.treeToValue(customerNode, CustomerDto.class);
} catch (IOException e) {
throw new RuntimeException("Error fetching customer data", e);
}
});
CompletableFuture<AccountDto> accountFuture = CompletableFuture.supplyAsync(() -> {
try {
// Simulate API call or file read
//String accountJson = restTemplate.getForObject("http://account-api/account", String.class);
JsonNode accountNode = objectMapper.readTree(new File("account.json")); //Read from file
return objectMapper.treeToValue(accountNode, AccountDto.class);
} catch (IOException e) {
throw new RuntimeException("Error fetching account data", e);
}
});
CompletableFuture.allOf(customerFuture, accountFuture).join(); //Wait for both futures to complete
CustomerDto customer = customerFuture.get();
AccountDto account = accountFuture.get();
return convertToCustomerAccountDto(customer, account);
}
private CustomerAccountDto convertToCustomerAccountDto(CustomerDto customer, AccountDto account) {
CustomerAccountDto dto = new CustomerAccountDto();
dto.setCustomerId(customer.getId());
dto.setCustomerName(customer.getName());
dto.setAccountNumber(account.getAccountNumber());
dto.setBalance(account.getBalance());
return dto;
}
}
// DTOs
class CustomerDto {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class AccountDto {
private String accountNumber;
private double balance;
public String getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
class CustomerAccountDto {
private Long customerId;
private String customerName;
private String accountNumber;
private double balance;
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public String getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
```
Explanation and Key Improvements:
1. Asynchronous Calls with `CompletableFuture`:
* The code now uses `CompletableFuture.supplyAsync()` to make the API calls (or file reads) concurrently. This significantly improves performance compared to sequential calls.
* `CompletableFuture.allOf(customerFuture, accountFuture).join();` waits for both calls to finish before proceeding.
* `customerFuture.get();` and `accountFuture.get();` retrieve the results of the asynchronous operations.
2. DTOs:
* Clear DTO classes (`CustomerDto`, `AccountDto`, `CustomerAccountDto`) are defined to represent the data structures. This improves code readability and maintainability.
3. Error Handling:
* `try-catch` blocks are used to handle potential `IOException` during file reading and `ExecutionException` or `InterruptedException` during asynchronous operations.
* A `RuntimeException` is thrown to propagate errors.
4. ObjectMapper for JSON Processing:
* The `ObjectMapper` from Jackson is used to parse JSON from the API responses (or files) and convert it to DTO objects.
* `objectMapper.readTree()` parses the JSON into a `JsonNode`.
* `objectMapper.treeToValue()` converts the `JsonNode` to a DTO.
5. RestTemplate:
* The code now includes a `RestTemplate` bean, which is the standard way to make HTTP requests in Spring Boot.
* The commented out lines shows how to use the RestTemplate to make external api calls.
* For the sake of this example, the code reads the json from local files.
6. File Based JSON Input:
* The code now reads the json from local files called `customer.json` and `account.json`. This allows the application to run without external API dependencies.
* Make sure to create these files in the root of your project.
7. Clear Conversion Logic:
* The `convertToCustomerAccountDto()` method encapsulates the logic for mapping data from the `CustomerDto` and `AccountDto` to the `CustomerAccountDto`.
To run this code:
1. Create `customer.json` and `account.json`:
* Create two JSON files named `customer.json` and `account.json` in the root directory of your Spring Boot project.
* Example `customer.json`:
```json
{
"id": 123,
"name": "John Doe"
}
```
* Example `account.json`:
```json
{
"accountNumber": "1234567890",
"balance": 1000.50
}
```
2. Add Dependencies:
* Make sure you have the following dependencies in your `pom.xml` or `build.gradle`:
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
```
3. Run the Application:
* Run the `ApiCallExampleApplication`.
4. Access the Endpoint:
* Open a web browser or use a tool like Postman to access the endpoint: `http://localhost:8080/combinedData`.
This improved example provides a robust and efficient way to handle asynchronous API calls and data transformation in a Spring Boot application. Remember to adapt the file paths and API URLs to your specific requirements.
Comentarios