Friday, May 2, 2025

Dataflow + Terraform: Secure Cloud SQL to BigQuery via PSA/PSC

 

Introduction

Two fully automated, production-grade pipelines demonstrate secure data ingestion from Cloud SQL PostgreSQL into BigQuery, using Apache Beam (Python) and Dataflow Flex Templates — all without public IP exposure.





✅ 1. Private Service Access (PSA) – Internal IP Connectivity

A Cloud SQL instance is provisioned with a private IP via PSA, and Dataflow connects over the internal VPC network.

📌 Key Highlights:

  • Cloud SQL + read replica
  • Private IP via PSA
  • VPC subnet + firewall on port 5432
  • Beam pipeline reads from PostgreSQL → BigQuery
  • Fully managed with Terraform
  • Optional GitHub Actions for CI/CD

🔌 JDBC Format: jdbc:postgresql://<private-ip>:5432/my-database2

🔗 Project Repo: 👉 github.com/.../cloud-sql-psa-with-bigquery


🔒 2. Private Service Connect (PSC) – DNS + SSL Certificate Access

PSC provides hostname-based, IP-less access to Cloud SQL, secured via SSL certificates stored in GCS.

📌 Key Highlights:

  • PSC forwarding rule + private DNS
  • SSL certs: server-ca.pem, client-cert.pem, client-key.pem
  • Enforced permissions (chmod 0600 on private key)
  • Beam pipeline loads certs at runtime from GCS
  • Flex Template accepts PSC host, cert path, and DB credentials
  • psycopg2 uses sslmode=verify-ca
  • Logs: success + errors written to BigQuery

🔗 Project Repo: 👉 github.com/.../cloud-sql-psc-with-bigquery


🛠️ Shared Architecture & Automation (Both Pipelines)

  • terraform/: VPC, Cloud SQL, BigQuery, IAM, DNS
  • sql/: PostgreSQL table schema + init script
  • postgresql_to_bq_flex_template/: Beam logic, Dockerfile, metadata
  • GCS: stores SSL certs + Dataflow staging artifacts
  • Flex Templates fully parameterized via metadata.json
  • Error handling + retry logic + scalable Dataflow workers


📊 Use Case Comparison


Both implementations align with cloud security and automation best practices:

✅ No public IPs

✅ IAM least-privilege access

✅ SSL/TLS enforcement (PSC)

✅ Infrastructure as Code with Terraform

✅ CI/CD extensibility


These blueprints are ideal for teams building secure, private, and scalable data pipelines on Google Cloud Platform using Dataflow, Cloud SQL, and BigQuery.
























Tuesday, February 18, 2025

Integrating Google Cloud Pub/Sub with Terraform and Spring Boot 3 (Java 21)

Introduction

In this blog post, I'll demonstrate how to provision Google Cloud Pub/Sub resources using Terraform and integrate them with a Spring Boot 3 application running Java 21. This setup enables seamless message publishing and subscription processing in a cloud-native environment.




Infrastructure Setup with Terraform

To automate the provisioning of Google Cloud resources, we'll use Terraform to create:

  • A Google Cloud Pub/Sub Topic and Subscription
  • A Service Account with the required IAM roles

Terraform Configuration

Provider Configuration (provider.tf)

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "6.21.0"
    }
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
  zone    = var.zone
}

provider "google-beta" {
  project = var.project_id
  region  = var.region
  zone    = var.zone
}
Variables (variable.tf)
variable "project_id" {
  default = ""
}

variable "region" {
  default = ""
}

variable "zone" {
  default = ""
}

variable "sec_region" {
  default = ""
}

variable "sec_zone" {
  default = ""
}
IAM Configuration (iam.tf)

resource "google_service_account" "pubsub_service_account" {
  project      = var.project_id
  account_id   = "pubsub-service-account-id"
  display_name = "Service Account for Cloud SQL"
}

output "service_account_email" {
  value = google_service_account.pubsub_service_account.email
}

resource "google_project_iam_member" "member-role" {
  for_each = toset(["roles/pubsub.admin"])
  role    = each.key
  project = var.project_id
  member  = "serviceAccount:${google_service_account.pubsub_service_account.email}"
}


Pub/Sub Resources (topic.tf)

resource "google_pubsub_topic" "my_topic" {
  project = var.project_id
  name    = "my_topic"
}

resource "google_pubsub_subscription" "my_subscription" {
  project = var.project_id
  name    = "my_subscription"
  topic   = google_pubsub_topic.my_topic.id
  message_retention_duration = "3600s"
  retain_acked_messages      = true
  ack_deadline_seconds       = 30

  retry_policy {
    minimum_backoff = "10s"
  }
}

Spring Boot 3 Implementation

We'll integrate Google Cloud Pub/Sub with a Spring Boot 3 application using the spring-cloud-gcp-starter-pubsub library.

Dependencies (pom.xml)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>spring-cloud-gcp-starter-pubsub</artifactId>
        <version>6.0.0</version>
    </dependency>
</dependencies>

Configuration (application.yml)
spring:
  application:
    name: demo-pub-sub-api
  cloud:
    gcp:
      project-id: <my-project-id>

Main Application (DemoPubSubApiApplication.java)

@SpringBootApplication
public class DemoPubSubApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoPubSubApiApplication.class, args);
    }
}
Pub/Sub Integration (PubSubApplication.java)

@Configuration
@Slf4j
public class PubSubApplication {
    private final String topicName = "<MY-TOPIC-NAME>";
    private final String subscriptionName = "<MY-SUBSCRIPTION-NAME>";

    @Bean
    public PubSubInboundChannelAdapter messageChannelAdapter(
            @Qualifier("pubsubInputChannel") MessageChannel inputChannel,
            PubSubTemplate pubSubTemplate) {
        PubSubInboundChannelAdapter adapter = new PubSubInboundChannelAdapter(pubSubTemplate, subscriptionName);
        adapter.setOutputChannel(inputChannel);
        adapter.setAckMode(AckMode.MANUAL);
        return adapter;
    }

    @Bean
    public MessageChannel pubsubInputChannel() {
        return new DirectChannel();
    }

    @Bean
    @ServiceActivator(inputChannel = "pubsubInputChannel")
    public MessageHandler messageReceiver() {
        return message -> {
            log.info("Received Message: {}", new String((byte[]) message.getPayload()));
            BasicAcknowledgeablePubsubMessage originalMessage = message.getHeaders()
                .get(GcpPubSubHeaders.ORIGINAL_MESSAGE, BasicAcknowledgeablePubsubMessage.class);
            originalMessage.ack();
        };
    }
}

REST Controller (WebAppController.java)

@RestController
@Slf4j
public class WebAppController {
    private final PubsubOutboundGateway messagingGateway;

    public WebAppController(PubsubOutboundGateway messagingGateway) {
        this.messagingGateway = messagingGateway;
    }

    @PostMapping("/publishMessage")
    public RedirectView publishMessage(@RequestParam("message") String message) {
        messagingGateway.sendToPubsub(message);
        return new RedirectView("/");
    }
}
Frontend (index.html)
<!DOCTYPE html>
<html>
<head>
    <title>Spring Integration GCP Pub/Sub</title>
</head>
<body>
    <form action="/publishMessage" method="post">
        Publish message: <input type="text" name="message" /> <input type="submit" value="Publish!"/>
    </form>
</body>
</html>

Running the Application


Provision Cloud Resources


terraform init
terraform apply
Set Environment Variables

export GOOGLE_APPLICATION_CREDENTIALS=mypath/application_default_credentials.json
export GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1

Run the Spring Boot Application

mvn spring-boot:run
Publish Messages from the Web Interface

  • Navigate to http://localhost:8080






  • Enter a message and click "Publish!"
  • The message should appear in the logs..
.PubSubApplication    : Message arrived! Payload: Hello pub sub!

GitHub Repository

The full code can be found on GitHub: GitHub - HenryXiloj/demos-gcp

Conclusion

This post demonstrated how to integrate Google Cloud Pub/Sub with Terraform and Spring Boot 3 using Java 21. By leveraging Infrastructure as Code (IaC) with Terraform, we can ensure a consistent and repeatable setup while Spring Boot handles the message processing efficiently.



References

https://spring.io/guides/gs/messaging-gcp-pubsub
























Monday, January 27, 2025

🚀 How to Integrate Ollama with DeepSeek-R1 in Spring Boot

Are you looking to leverage the power of Ollama and DeepSeek-R1 in your Spring Boot application? This post will walk you through the entire process, from understanding what Ollama is to implementing a seamless integration. 




What is Ollama?

Ollama is a powerful tool designed to simplify the deployment and management of large language models (LLMs) locally. It provides an easy-to-use API for interacting with models like DeepSeek-R1, making it an excellent choice for developers who want to integrate AI capabilities into their applications without relying on external cloud services.


With Ollama, you can:

  • Run LLMs locally on your machine.
  • Switch between different model versions effortlessly.
  • Integrate AI capabilities into your applications via a simple API.


Why Integrate Ollama with DeepSeek-R1?

DeepSeek-R1 is a state-of-the-art language model that offers high performance and flexibility. By integrating it with Ollama in your Spring Boot application, you can:

  • Build AI-powered features like chatbots, content generators, and more.
  • Keep your AI logic local, ensuring data privacy and reducing latency.
  • Easily switch between different versions of DeepSeek-R1 based on your application’s needs.


Step 1: Install Ollama

To get started, you’ll need to install Ollama on your system. Run the following command in your terminal:

curl -fsSL https://ollama.com/install.sh | sh

Successful Installation Output:


>>> Installing ollama to /usr/local
>>> Downloading Linux amd64 bundle
>>> Creating ollama user...
>>> Adding ollama user to groups...
>>> Creating ollama systemd service...
Created symlink /etc/systemd/system/default.target.wants/ollama.service
>>> Nvidia GPU detected
>>> API available at 127.0.0.1:11434

Once installed, Ollama will be ready to use, and the API will be available at http://localhost:11434.


To verify it's working: 

ollama serve
ollama list 
ollama pull deepseek-r1:1.5b

If deepseek-r1:1.5b isn’t listed, pull it.


Test the model with curl:

curl -X POST http://localhost:11434/api/generate \
  -d '{"model": "deepseek-r1:1.5b", "prompt": "Hello", "stream": false}'

Step 2: Application Configuration

Next, configure your Spring Boot application by updating the application.yml file:

spring:
  application:
    name: demo-deepseek-r1.ollama

# Server configuration
server:
  port: 8080
  error:
    include-message: always

# Ollama configuration
ollama:
  endpoint: http://localhost:11434/api/generate
  model: deepseek-r1:1.5b
  timeout:
    connect: 30000
    read: 60000
This configuration sets up the Ollama endpoint, model, and timeout settings for your application.


Step 3: Core Implementation


Create the following records to handle requests and responses:


// OllamaRequest.java
@JsonInclude(JsonInclude.Include.NON_NULL)
public record OllamaRequest(
    String model,
    String prompt,
    boolean stream
) {}

// OllamaResponse.java
@JsonIgnoreProperties(ignoreUnknown = true)
public record OllamaResponse(
    String model,
    String response,
    String created_at,
    boolean done
) {}

Service Layer


Implement the OllamaService to interact with the Ollama API:

@Service
public class OllamaService {

    private final RestTemplate restTemplate;
    private final OllamaProperties properties;

    public OllamaService(OllamaProperties properties) {
        this.properties = properties;

        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(Timeout.ofMilliseconds(properties.getTimeout().getConnect()))
                .setResponseTimeout(Timeout.ofMilliseconds(properties.getTimeout().getRead()))
                .build();

        CloseableHttpClient httpClient = HttpClients.custom()
                .setDefaultRequestConfig(config)
                .build();

        HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
        this.restTemplate = new RestTemplate(requestFactory);
    }

    public String generateResponse(String prompt) {
        try {
            OllamaRequest request = new OllamaRequest(properties.getModel(), prompt, false);
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);

            ResponseEntity<OllamaResponse> response = restTemplate.exchange(
                    properties.getEndpoint(),
                    HttpMethod.POST,
                    new HttpEntity<>(request, headers),
                    OllamaResponse.class
            );

            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
                return response.getBody().response() != null
                        ? response.getBody().response()
                        : "Received empty response from model";
            }
            return "Ollama API returned status: " + response.getStatusCode();
        } catch (RestClientException e) {
            return "Error communicating with Ollama: " + e.getMessage();
        }
    }
}

REST Controller

Create a REST controller to expose the chat endpoint:

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final OllamaService ollamaService;

    public ChatController(OllamaService ollamaService) {
        this.ollamaService = ollamaService;
    }

    @PostMapping
    public ResponseEntity<String> chat(@RequestBody String prompt) {
        if (prompt == null || prompt.isBlank()) {
            return ResponseEntity.badRequest().body("Prompt cannot be empty");
        }
        String response = ollamaService.generateResponse(prompt);
        return ResponseEntity.ok(response);
    }
}


Model Version Compatibility

Here’s a quick reference for DeepSeek-R1 model versions and their requirements:



*Check official model availability at:

Ollama Model Library


Testing the Integration

To test the integration, use the following curl  command or postman:

curl -X POST -H "Content-Type: text/plain" -d "Explain AI in simple terms" http://localhost:8080/api/chat

Ouput









🪟 Bonus: Using Ollama in WSL on Windows

If you're on Windows using WSL, follow these steps to expose the Ollama service to Windows:

🔒 WSL Side: Open the Port

sudo ufw enable
sudo ufw allow 11434
sudo systemctl stop ollama
sudo lsof -i :11434
export OLLAMA_HOST=0.0.0.0
ollama serve
Verify Ollama is listening:
sudo ss -tulnp | grep 11434

🪟 Windows Side: Port Forwarding wit admin permission

$wsl_ip = (wsl hostname -I).Split()[0]

netsh interface portproxy add v4tov4 `
  listenport=11434 listenaddress=0.0.0.0 `
  connectport=11434 connectaddress=$wsl_ip

New-NetFirewallRule -DisplayName "Ollama-WSL" `
  -Direction Inbound -Protocol TCP -LocalPort 11434 -Action Allow
🔁 Optional: Persistence Across Reboots

WSL Side
echo 'export OLLAMA_HOST=0.0.0.0' >> ~/.bashrc
echo 'pkill ollama; ollama serve > /tmp/ollama.log 2>&1 &' >> ~/.bashrc
Windows Side

$action = New-ScheduledTaskAction -Execute "wsl" -Argument "-e bash -c 'ollama serve'"
$trigger = New-ScheduledTaskTrigger -AtStartup
Register-ScheduledTask -TaskName "Ollama-WSL" -Action $action -Trigger $trigger -RunLevel Highest


Source Code

Here on GitHub.


🙌 Final Thoughts

Running LLMs locally has never been easier. With Ollama, DeepSeek-R1, and Spring Boot, you can build blazing-fast AI-powered apps while keeping full control over your data.


Sunday, January 26, 2025

Spring Retry: Handling Transient Failures Gracefully in Java 21

In modern applications, transient failures (e.g., network timeouts, database connection issues, or external API unavailability) are inevitable. To build resilient systems, we need mechanisms to retry failed operations gracefully.

With Java 21 and Spring Boot 3, we can leverage Spring Retry to implement robust retry logic. In this post, I'll show you how to integrate Spring Retry into your application, complete with examples using virtual threads and asynchronous processing.




Why Use Spring Retry?

Spring Retry provides a declarative way to retry operations that may fail due to transient issues. Key features include:

  • Retry Logic: Automatically retry failed operations with configurable attempts and backoff strategies.
  • Fallback Mechanism: Define recovery logic when all retries fail.
  • Integration with Spring: Seamlessly integrates with Spring Boot and other Spring components.


Key Concepts:

@Retryable:

Marks a method as retryable. You can specify the exceptions to retry, the maximum number of attempts, and the backoff strategy.

@Recover:

Defines a fallback method to execute when all retries fail. You can have multiple @Recover methods to handle different exceptions.

Why Multiple @Recover Methods?

Different exceptions may require different recovery logic. For example:

  • A RuntimeException might require logging.
  • An IOException might require returning a default response.

By defining multiple @Recover methods, you can handle each exception type appropriately.

Example 1: Retry with Virtual Threads

This example demonstrates how to retry an HTTP call using virtual threads.

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.concurrent.ExecutionException;

@Service // This makes it a Spring-managed bean
public class VirtualThreadExample {

    @Retryable(
            retryFor = {RuntimeException.class, IOException.class, InterruptedException.class, ExecutionException.class, Exception.class},
            maxAttempts = 4,                                   // Total 4 attempts (1 initial + 3 retries)
            backoff = @Backoff(delay = 1000, multiplier = 2)// Exponential backoff: 1s, 2s, 4s
    )
    public void getResponse(String urlRest) throws IOException {
        System.out.println("Attempting to call: " + urlRest);
        throw new RuntimeException("Negative Test cases for VirtualThreadExample");
    }

    @Recover
    public void recover(RuntimeException e, String urlRest) {
        System.err.println("All retries failed for URL: " + urlRest);
        System.err.println("Error details: " + e.getMessage());
    }

    @Recover
    public void recover(IOException e, String urlRest) {
        System.err.println("All retries failed for URL: " + urlRest);
        System.err.println("Error details: " + e.getMessage());
    }

    @Recover
    public void recover(InterruptedException e, String urlRest) {
        System.err.println("All retries failed for URL: " + urlRest);
        System.err.println("Error details: " + e.getMessage());
    }

    @Recover
    public void recover(ExecutionException e, String urlRest) {
        System.err.println("All retries failed for URL: " + urlRest);
        System.err.println("Error details: " + e.getMessage());
    }

    @Recover
    public void recover(Exception e, String urlRest) {
        System.err.println("All retries failed for URL: " + urlRest);
        System.err.println("Error details: " + e.getMessage());
    }
}

Expected Output:


When the getResponse method is called, it will retry 4 times (1 initial attempt + 3 retries) with exponential backoff. If all retries fail, the appropriate @Recover method will be called.

Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Processed 3 posts in 7060 millis
Program Completed !!
All retries failed for URL: https://jsonplaceholder.typicode.com/posts/3
Error details: Negative Test cases for VirtualThreadExample
All retries failed for URL: https://jsonplaceholder.typicode.com/posts/1
Error details: Negative Test cases for VirtualThreadExample
All retries failed for URL: https://jsonplaceholder.typicode.com/posts/2
Error details: Negative Test cases for VirtualThreadExample


Example 2: Retry with Asynchronous Processing


This example demonstrates how to retry a database operation asynchronously.

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class AsyncExample {

    @Retryable(
            retryFor = {RuntimeException.class}, // Retry on runtime exceptions
            maxAttempts = 4,                    // Total 4 attempts (1 initial + 3 retries)
            backoff = @Backoff(delay = 1000, multiplier = 2) // Exponential backoff: 1s, 2s, 4s
    )
    public void saveUser(String user) {
        System.out.println("Saving user: " + user);
        try {
            Thread.sleep(1000); // Simulate database latency
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Thread interrupted while saving user", e);
        }
        // Simulate a transient failure
        throw new RuntimeException("Failed to save user due to a transient error");
    }

    @Recover
    public void recover(RuntimeException e, String user) {
        System.err.println("All retries failed for user: " + user);
        System.err.println("Error details: " + e.getMessage());
        // Fallback logic (e.g., log the error, notify, or take corrective action)
    }
}

Expected Output:


When the saveUser method is called, it will retry 4 times (1 initial attempt + 3 retries) with exponential backoff. If all retries fail, the @Recover method will be called.

Saving user: JohnDoe
Saving user: JohnDoe
Saving user: JohnDoe
Saving user: JohnDoe
All retries failed for user: JohnDoe
Error details: Failed to save user due to a transient error

Running the Application


To execute these examples, define ApplicationRunner beans in your Spring Boot application:

import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.annotation.EnableRetry;

import java.util.List;
import java.util.concurrent.*;

@SpringBootApplication
@EnableRetry
public class DemoSpringRetryApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoSpringRetryApplication.class, args);
	}

	@Bean
	ApplicationRunner asyncExampleRunner(AsyncExample example) {
		return args -> {
			String user = "JohnDoe";

			// Run the task asynchronously
			CompletableFuture.runAsync(() -> {
						System.out.println("Starting async task for user: " + user);
						example.saveUser(user); // Use the injected bean
						System.out.println("Async task completed for user: " + user);
					}, Executors.newVirtualThreadPerTaskExecutor())
					.exceptionally(ex -> {
						System.err.println("Failed to save user: " + user);
						ex.printStackTrace();
						return null;
					});

			System.out.println("Main thread continues executing...");
			try {
				Thread.sleep(2000); // Simulate main thread work
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("Main thread finished.");
		};
	}

	@Bean
	ApplicationRunner virtualThreadExampleRunner(VirtualThreadExample example) {
		return args -> {
			try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
				// List of posts to process
				List<Integer> posts = List.of(1, 2, 3);
				long start = System.nanoTime();

				// Submit a task for each post
				List<Future<Object>> futures = posts.stream()
						.map(post -> myExecutor.submit(() -> {
							example.getResponse("https://jsonplaceholder.typicode.com/posts/" + post);
							return null; // Explicitly return null for Future<Void>
						}))
						.toList();

				// Wait for all tasks to complete
				for (Future<Object> future : futures) {
					future.get(); // Ensures task completion
				}

				long duration = (System.nanoTime() - start) / 1_000_000;
				System.out.printf("Processed %d posts in %d millis%n", posts.size(), duration);
				System.out.println("Program Completed !!");
			} catch (InterruptedException | ExecutionException e) {
				System.err.println("error " + e.getMessage());
			}
		};
	}
}

Dependencies


Add the following dependencies to your pom.xml:

<dependencies>
    <!-- Spring Retry -->
    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
    </dependency>
    <!-- Spring AOP (required for @EnableRetry) -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
    </dependency>
</dependencies>

Conclusion


By leveraging Spring Retry alongside Java 21's virtual threads and asynchronous processing, you can create resilient systems that gracefully handle transient failures. Whether making HTTP calls or working with databases, retry mechanisms ensure your application stays reliable and robust under pressure.

Key Takeaways:

  • Use @Retryable to define retry logic and @Recover for fallback behavior.
  • Multiple @Recover methods allow you to handle different exceptions appropriately.
  • Virtual threads and asynchronous processing improve concurrency and performance.







Sunday, December 8, 2024

Virtual Threads in Java 21: Simplified Concurrency for Modern Applications

 

With Java 21, Virtual Threads have redefined how we approach concurrency, offering a lightweight and efficient way to handle parallel and asynchronous tasks. Unlike CompletableFuture or ParallelStream, Virtual Threads allow developers to write simple, synchronous-looking code while achieving the scalability of asynchronous solutions.




Key Features of Virtual Threads:

  • Lightweight Concurrency: Virtual Threads are cheap to create and manage, enabling applications to handle thousands or even millions of concurrent tasks.
  • Simpler Programming Model: Code written with Virtual Threads resembles traditional synchronous code, making it easier to understand and maintain.
  • Automatic Thread Management: The JVM manages Virtual Threads' scheduling, eliminating the need to manually manage thread pools.
  • Great for IO-Bound Tasks: Virtual Threads excel in handling IO-bound workloads, such as making multiple HTTP calls concurrently.

Example: Virtual Threads with HTTP Requests

Here’s how you can use Virtual Threads to fetch posts concurrently:

Example: Fetch Posts Using Virtual Threads


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.*;

public class VirtualThreadExample {

    public static void main(String[] args) {
        try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
            // List of posts to process
            List<Integer> posts = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
            long start = System.nanoTime();

            // Submit a task for each post
            List<Future<Object>> futures = posts.stream()
                    .map(post -> myExecutor.submit(() -> {
                        getResponse("https://jsonplaceholder.typicode.com/posts/" + post);
                        return null; // Explicitly return null for Future<Void>
                    }))
                    .toList();

            // Wait for all tasks to complete
            for (Future<Object> future : futures) {
                future.get(); // Ensures task completion
            }

            long duration = (System.nanoTime() - start) / 1_000_000;
            System.out.printf("Processed %d posts in %d millis%n", posts.size(), duration);
            System.out.println("Program Completed !!");
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    public static void getResponse(String urlRest) {
        try {

            InputStream is = getInputStream(urlRest);

            BufferedReader rd = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = rd.readLine()) != null) {
                response.append(line);
            }
            rd.close();
            System.out.println("Response: " + response.toString().trim());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static InputStream getInputStream(String urlRest) throws IOException {
        URL url = new URL(urlRest);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        connection.setRequestProperty("Content-Type", "application/json");
        connection.setRequestProperty("Content-Language", "en-US");
        connection.setConnectTimeout(60000);
        connection.setUseCaches(false);
        connection.setDoInput(true);
        connection.setDoOutput(true);

        return connection.getResponseCode() >= 400
                ? connection.getErrorStream()
                : connection.getInputStream();
    }
}

Performance and Simplicity

Output:

Processed 20 posts in 971 millis
Program Completed !!


Example: Asynchronous Task with CompletableFuture.runAsync()

Virtual Threads also work seamlessly with CompletableFuture, enabling fire-and-forget asynchronous tasks. Here’s an example of saving user data asynchronously:



import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Executor;

public class AsyncExample {

    static class UserRepository {
        public void saveUser(String user) {
            System.out.println("Saving user: " + user);
            try {
                Thread.sleep(1000); // Simulate database latency
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Thread interrupted while saving user", e);
            }
            System.out.println("User saved: " + user);
        }
    }

    public static void main(String[] args) {
        Executor virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
        String user = "JohnDoe";

        // Run the task asynchronously
        CompletableFuture.runAsync(() -> {
            System.out.println("Starting async task for user: " + user);
            UserRepository repository = new UserRepository();
            repository.saveUser(user);
            System.out.println("Async task completed for user: " + user);
        }, virtualThreadExecutor)
        .exceptionally(ex -> {
            System.err.println("Failed to save user: " + user);
            ex.printStackTrace();
            return null;
        });

        System.out.println("Main thread continues executing...");
        try {
            Thread.sleep(2000); // Simulate main thread work
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main thread finished.");
    }
}


Output:

Main thread continues executing...
Starting async task for user: JohnDoe
Saving user: JohnDoe
User saved: JohnDoe
Async task completed for user: JohnDoe
Main thread finished.

Scalability and Readability


Scalability: Virtual Threads scale seamlessly for handling a large number of requests.

Readability: The synchronous-looking code is easy to understand and maintain.


Automatic Resource Management

The ExecutorService created using Executors.newVirtualThreadPerTaskExecutor() in the try-with-resources block will automatically be shut down when the block is exited. This is because ExecutorService implements the AutoCloseable interface, and in a try-with-resources block, the close() method is automatically called.

If you're using the ExecutorService outside of a try-with-resources block, you must explicitly shut it down. This ensures that all threads are properly terminated and resources are released.

Why Virtual Threads?

  1. No Thread Pool Management: Virtual Threads handle concurrency without the need for explicit thread pools or tuning.
  2. Efficient Resource Usage: They use far less memory compared to platform threads.
  3. Modern and Forward-Looking: As of Java 21, Virtual Threads are the future of Java concurrency, making them the ideal choice for most applications.

If you’re still on Java 8 or 17, you might use CompletableFuture or ParallelStream. But with Java 21, Virtual Threads are the most straightforward and efficient way to handle concurrent tasks. Whether you’re making HTTP requests, processing data, or performing database operations, Virtual Threads simplify concurrency while delivering exceptional performance.








Thursday, September 5, 2024

Creating REST APIs with OpenAPI, Spring Boot 3.3.3, Java 21, and Jakarta

 Introduction

In today's software landscape, designing robust and scalable REST APIs is a crucial aspect of application development. With the growing complexity of APIs, ensuring consistency and maintainability across different services becomes challenging. This is where OpenAPI (formerly known as Swagger) comes into play. OpenAPI provides a standard way to define your API specifications, making it easier to generate client SDKs, server stubs, and documentation automatically.

In this post, we’ll explore how to design REST APIs using OpenAPI and generate a RestController class from an OpenAPI YAML file. We’ll leverage Spring Boot 3.3.3 and Java 21 to build a modern, efficient, and maintainable API.



Setting Up Your Spring Boot Project

For this example, we’ll use Spring Boot 3.3.3 and Java 21. 


Project Structure

Once your project is set up, the basic structure will look like this:

demo-open-api-swager
|-- src
|   |-- main
|       |-- java
|           |-- com
|               |-- henry.openapi
|                   |-- service
|-- src
|   |-- test
|       |-- java
|           |-- com
|               |-- henry
|                   |-- ...
|-- pom.xml

Defining the OpenAPI Specification

Now, let’s define our API using an OpenAPI YAML file. This file will describe the API’s endpoints, request and response models, and other relevant details.


1. Create the YAML File

In the src/main/resources directory, create a file named api.yaml and define your API specification. Here’s a simple example:


openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
paths:
  /items:
    get:
      summary: Get all items
      operationId: getAllItems
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Item'
    post:
      summary: Create a new item
      operationId: createItem
      requestBody:
        description: Item to create
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Item'
      responses:
        '201':
          description: Item created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
  
  /items/{id}:
    get:
      summary: Get an item by ID
      operationId: getItemById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
        '404':
          description: Item not found
    put:
      summary: Update an item by ID
      operationId: updateItemById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        description: Item to update
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Item'
      responses:
        '200':
          description: Item updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
        '404':
          description: Item not found
    delete:
      summary: Delete an item by ID
      operationId: deleteItemById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '204':
          description: Item deleted successfully
        '404':
          description: Item not found

components:
  schemas:
    Item:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
      required:
        - name

2. YAML Content Explanation

info: Provides metadata about the API, such as title and version.

paths: Defines the available API endpoints. In this case, we have a /items endpoint with a CRUD operation.

components: Specifies reusable components like schemas. Here, Item is defined with id and name properties.


Configuring Swagger Codegen Plugin

To generate Java classes from the YAML file, we’ll configure the Swagger Codegen Maven plugin.


1. Maven Plugin Configuration

Add the following configuration to your pom.xml:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.henry</groupId>
    <artifactId>demo-open-api-swager</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo-open-api-swager</name>
    <description>Designing Robust REST APIs with OpenAPI in Spring Boot</description>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Starter for Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Boot DevTools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!-- Lombok for reducing boilerplate -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Swagger Models and Annotations -->
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-models</artifactId>
            <version>2.2.4</version>
        </dependency>
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations-jakarta</artifactId>
            <version>2.2.22</version>
        </dependency>
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.16</version>
        </dependency>

        <!-- Nullable support for Jackson -->
        <dependency>
            <groupId>org.openapitools</groupId>
            <artifactId>jackson-databind-nullable</artifactId>
            <version>0.2.6</version>
        </dependency>

        <!-- Springdoc OpenAPI for Spring Boot 3 instead of springfox dependencies -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.6.0</version>
        </dependency>

        <!-- Jakarta Validation API -->
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
        </dependency>

        <!-- Jakarta Annotations API -->
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
        </dependency>

        <!-- Spring Boot Starter for Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Spring Boot Maven Plugin -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <!-- OpenAPI Generator Plugin -->
            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <!-- Till  6.4.0 we can use jakarta instead of javax with <useJakartaEe>true</useJakartaEe>-->
                <version>7.8.0</version>
                <executions>
                    <execution>
                        <id>generate-sources</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
                            <output>${project.basedir}/src/main/generated-sources</output>
                            <generatorName>spring</generatorName>
                            <apiPackage>com.henry.openapi.controller</apiPackage>
                            <modelPackage>com.henry.openapi.model</modelPackage>
                            <invokerPackage>com.henry.openapi</invokerPackage>
                            <generateSupportingFiles>false</generateSupportingFiles>
                            <skipOperationExample>true</skipOperationExample>
                            <generateApis>true</generateApis>
                            <generateModelTests>false</generateModelTests>
                            <generateModelDocumentation>false</generateModelDocumentation>
                            <configOptions>
                                <useJakartaEe>true</useJakartaEe>
                                <serializableModel>true</serializableModel>
                                <dateLibrary>legacy</dateLibrary>
                                <java21>true</java21>
                                <library>spring-boot</library>
                                <delegatePattern>true</delegatePattern>
                                <useBeanValidation>true</useBeanValidation>
                                <useOptional>false</useOptional>
                                <hideGenerationTimestamp>true</hideGenerationTimestamp>
                            </configOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <!-- Maven Compiler Plugin for Java 21 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>21</source>
                    <target>21</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>


Generating REST Controllers

With the YAML file and Maven plugin configured, we can now generate the REST controllers.


1. Run the Maven Command

Execute the following command in your project directory:


mvn clean install
This command will generate the REST controller interfaces based on the OpenAPI YAML file.

2. Review the Generated Code


After running the command, you’ll find the generated code in the ${project.basedir}/src/main/generated-sources directory (or another location depending on your configuration). 
















Here’s an example of what the generated ItemsApiDelegate interface might look like:

package com.henry.openapi.controller;

import com.henry.openapi.model.Item;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;

import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.annotation.Generated;

/**
 * A delegate to be called by the {@link ItemsApiController}}.
 * Implement this interface with a {@link org.springframework.stereotype.Service} annotated class.
 */
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.8.0")
public interface ItemsApiDelegate {

    default Optional<NativeWebRequest> getRequest() {
        return Optional.empty();
    }

    /**
     * POST /items : Create a new item
     *
     * @param item Item to create (required)
     * @return Item created successfully (status code 201)
     * @see ItemsApi#createItem
     */
    default ResponseEntity<Item> createItem(Item item) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * DELETE /items/{id} : Delete an item by ID
     *
     * @param id  (required)
     * @return Item deleted successfully (status code 204)
     *         or Item not found (status code 404)
     * @see ItemsApi#deleteItemById
     */
    default ResponseEntity<Void> deleteItemById(Long id) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * GET /items : Get all items
     *
     * @return Successful response (status code 200)
     * @see ItemsApi#getAllItems
     */
    default ResponseEntity<List<Item>> getAllItems() {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * GET /items/{id} : Get an item by ID
     *
     * @param id  (required)
     * @return Successful response (status code 200)
     *         or Item not found (status code 404)
     * @see ItemsApi#getItemById
     */
    default ResponseEntity<Item> getItemById(Long id) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * PUT /items/{id} : Update an item by ID
     *
     * @param id  (required)
     * @param item Item to update (required)
     * @return Item updated successfully (status code 200)
     *         or Item not found (status code 404)
     * @see ItemsApi#updateItemById
     */
    default ResponseEntity<Item> updateItemById(Long id,
        Item item) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

}

Customizing the Generated Code

Although the generated code provides a good starting point, you may want to customize it to suit your application's specific needs.


1. Implementing the Interface

You can create a class that implements the ItemsApiDelegate interface and adds custom logic:

package com.henry.openapi.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import com.henry.openapi.controller.ItemsApiDelegate;
import com.henry.openapi.model.Item;

@Service
public class ItemsServiceImpl implements ItemsApiDelegate{

	 // Mock in-memory database
    private final List<Item> items = new ArrayList<>();
    private Long currentId = 1L;

    public ItemsServiceImpl() {
        // Initialize with some mock data
    	var it1 = new Item();
    	it1.setId(1L);
    	it1.setName("Item 1");
        items.add(it1);
        
        var it2 = new Item();
        it2.setId(2L);
        it2.setName("Item 2");
        items.add(it2);
    }

    @Override
    public ResponseEntity<List<Item>> getAllItems() {
        return new ResponseEntity<>(items, HttpStatus.OK);
    }

    @Override
    public ResponseEntity<Item> getItemById(Long id) {
        Optional<Item> item = items.stream()
                .filter(i -> i.getId().equals(id))
                .findFirst();
        return item.map(i -> new ResponseEntity<>(i, HttpStatus.OK))
                .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    @Override
    public ResponseEntity<Item> createItem(Item item) {
        item.setId(currentId++); // Simulate auto-increment ID
        items.add(item);
        return new ResponseEntity<>(item, HttpStatus.CREATED);
    }

    @Override
    public ResponseEntity<Item> updateItemById(Long id, Item item) {
        for (int i = 0; i < items.size(); i++) {
            if (items.get(i).getId().equals(id)) {
                item.setId(id); // Keep the same ID
                items.set(i, item);
                return new ResponseEntity<>(item, HttpStatus.OK);
            }
        }
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    @Override
    public ResponseEntity<Void> deleteItemById(Long id) {
        boolean removed = items.removeIf(i -> i.getId().equals(id));
        if (removed) {
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }
}

Testing the Generated API

Now that you have your controllers in place, it’s time to test the API.


1. Run the Application

Start your Spring Boot application using your preferred method:

mvn spring-boot:run


2. Access Swagger UI

Navigate to http://localhost:8080/swagger-ui.html in your browser. Swagger UI will display the endpoints defined in your OpenAPI specification, allowing you to interact with them directly.









3. curl requests for each method in the ItemsServiceImpl class

1. GET /items (Retrieve all items)


curl -X GET "http://localhost:8080/items" -H "accept: application/json"

2. GET /items/{id} (Retrieve an item by ID)

Replace {id} with the actual item ID you want to retrieve. For example, to get the item with ID 1:

curl -X GET "http://localhost:8080/items/1" -H "accept: application/json"


3. POST /items (Create a new item)

You need to provide the item details in JSON format. For example:

curl -X POST "http://localhost:8080/items" \
  -H "Content-Type: application/json" \
  -H "accept: application/json" \
  -d '{
        "name": "New Item"
      }'


4. PUT /items/{id} (Update an item by ID)

Replace {id} with the actual item ID you want to update. For example, to update the item with ID 1:

curl -X PUT "http://localhost:8080/items/1" \
  -H "Content-Type: application/json" \
  -H "accept: application/json" \
  -d '{
        "name": "Updated Item"
      }'


5. DELETE /items/{id} (Delete an item by ID)

Replace {id} with the actual item ID you want to delete. For example, to delete the item with ID 1:

curl -X DELETE "http://localhost:8080/items/1" -H "accept: application/json"














Conclusion


By leveraging OpenAPI and Swagger, you can automate the creation of REST APIs, ensuring consistency and reducing the likelihood of errors. The approach outlined in this post not only saves development time but also provides a clear contract for API consumers.

we covered the entire process of defining an API with OpenAPI, generating code using Swagger Codegen, and customizing the generated controllers in a Spring Boot project. This approach is highly scalable, making it easier to manage and maintain your APIs as they evolve.


Additional Configuration



IntelliJ IDEA  2024.2.1 Community 

File -> Project Structure 













Click On -> Apply Button 


Eclipse IDE for Enterprise Java and Web Developers 2024-06

1. Go to Windows->Preferences->XML(Wild Web Development)

 2. Check checkbox for "Download external resources like referenced DTD,XSD







Source code

Here on GitHub.

References

https://swagger.io/specification/

https://springdoc.org/

https://github.com/swagger-api/swagger-codegen

https://stackoverflow.com/questions/70692260/cvc-elt-1-a-cannot-find-the-declaration-of-element-project

https://stackoverflow.com/questions/70291226/how-to-remove-apiutil-java-from-openapi-geneate-task-with-openapi-generator-grad

https://github.com/springdoc/springdoc-openapi/issues/1977

https://stackoverflow.com/questions/74593513/is-there-a-way-to-configure-openapi-generator-to-use-jakarta-package-during-gene

🚀 Spring Boot 3.5 → 4.0.3 Migration Summary

Import Changes, API Adjustments & Compatibility Results Migration summary from Spring Boot 3.5 → 4.0, including breaking changes, depend...