eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – All Access – NPI EA (cat= Spring)
announcement - icon

All Access is finally out, with all of my Spring courses. Learn JUnit is out as well, and Learn Maven is coming fast. And, of course, quite a bit more affordable. Finally.

>> GET THE COURSE
Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

1. Introduction

The generative AI landscape is evolving rapidly, pushing framework maintainers to rethink how applications connect to diverse large language models (LLMs). With the release of Spring AI, Java developers gained a unified, portable interface to interact with cognitive services.

However, as the ecosystem matured, integrating multiple foundational models within a single runtime classpath introduced configuration conflicts. To solve this, Spring AI introduced major architectural updates. These include the specialized spring-ai-starter-model-google-genai starter module and an explicit model selection paradigm.

In this tutorial, we’ll explore how to integrate Google’s Gemini models into a Spring Boot application using the Gemini Developer API (via Google AI Studio). We’ll walk through a streamlined project setup, dive into text generation patterns using both low-level and high-level APIs, apply prompt templates, enable live web grounding, and stream tokens reactively with WebFlux.

2. Project Setup

To get started, we need a standard Spring Boot 3.x application. Because Spring AI modules are actively updated, it’s highly recommended to manage dependency versions using the Spring AI Bill of Materials (BOM).

2.1. Maven Dependencies

First, let’s configure the Spring AI Starter Google GenAI and Spring AI BOM in our pom.xml file. Since Spring AI artifacts are hosted in the Spring Milestones repository during release cycles, we ensure both the repository and dependency management blocks are properly declared:

<project>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>2.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-google-genai</artifactId>
        </dependency>

    </dependencies>
</project>

2.2. Activating the Chat Provider via spring.ai.model.chat

In the latest versions of Spring AI, dropping a starter module onto the classpath no longer automatically activates it. This prevents runtime configuration conflicts when your application references multiple LLM providers.

We must explicitly declare our active chat provider inside our application.properties file:

spring.ai.model.chat=google-genai
spring.ai.google.genai.chat.model=gemini-2.5-flash
spring.ai.google.genai.api-key=${SPRING_AI_GOOGLE_GENAI_API_KEY}

Obtain an API key from the Google AI Studio. Using this placeholder syntax explicitly documents the application’s external runtime dependency while preventing hardcoded secrets from leaking into our source control repository.

2.3. Environment Setup

We can satisfy the property placeholder defined above without writing any custom Java decryption or binding code. We’ll simply export the credentials directly into our environment. The auto-configuration engine will automatically resolve the placeholder value at startup:

export SPRING_AI_GOOGLE_GENAI_API_KEY="YourActualSecureGoogleAIStudioKeyHere"

Once this environment variable is set, Spring Boot seamlessly bridges the variable to the properties file placeholder, making the GoogleGenAiChatModel ready for injection.

3. Core Text and Content Generation Strategies

Spring AI gives us two primary abstraction layers for communicating with Gemini models: the foundational ChatModel bean and the highly configurable, fluent ChatClient API. We isolate our interaction logic into a dedicated service layer to follow enterprise design patterns and keep code clean. Then, we expose these capabilities through standard Spring REST controllers.

3.1. Injecting and Using GoogleGenAiChatModel

The GoogleGenAiChatModel represents the foundational, low-level client abstraction. It handles request serialization, HTTP execution against Google’s RPC endpoints, and raw response parsing.

Let’s look at our ChatService, where we inject this bean along with a ChatClient.Builder to initialize our operational layers:

@Service
public class ChatService {

    private final GoogleGenAiChatModel chatModel;
    private final ChatClient defaultChatClient;
    private final ChatClient fluentChatClient;

    public ChatService(GoogleGenAiChatModel chatModel, ChatClient.Builder chatClientBuilder) {
        this.chatModel = chatModel;
        this.defaultChatClient = chatClientBuilder.build();
        this.fluentChatClient = chatClientBuilder
          .defaultSystem("You are a concise technical writer summarizing software concepts.")
          .build();
    }

    public String simplifiedPrompt(String message) {
        return chatModel.call(message);
    }
}

When we invoke chatModel.call(message), the framework packages the raw string into a default prompt context. It then sends this to the configured model variant, extracts the text block from the response payload, and returns it. We expose this in our ChatController:

@RestController
public class ChatController {

    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @GetMapping("/v1/chat/simple")
    public String simplifiedPrompt(@RequestParam(defaultValue = "Hello") String message) {
        return chatService.simplifiedPrompt(message);
    }
}

This establishes a simple string-in, string-out endpoint. It uses the lowest-level model client abstraction available to verify basic connectivity with the Gemini engine.

3.2. Constructing Prompts With the Fluent ChatClient API

While working directly with the model handles quick operations, production architectures favor the fluent ChatClient API. The ChatClient acts as a facade layer that simplifies prompt assembly and attaches default configuration baselines.

As shown in our service constructor, we pre-configured a specialized fluentChatClient with system instructions. Let’s add the corresponding execution method to ChatService:

public String fluentPrompt(String prompt) {
    return this.fluentChatClient.prompt()
      .user(prompt)
      .call()
      .content();
}

By routing requests through this pre-configured instance, every user query automatically appends the target system instructions before execution. We expose this endpoint in the controller:

@GetMapping("/v1/chat/fluent")
public String fluentPrompt(@RequestParam String prompt) {
    return chatService.fluentPrompt(prompt);
}

This allows us to encapsulate recurring personas or systemic behavior directly within the client wrapper instance. As a result, we don’t need to modify individual incoming text arguments manually.

3.3. Handling Dynamic Inputs via Prompt Templates

Hardcoding parameter logic directly inside strings leads to messy string concatenation and fragile codebases. Spring AI solves this with structured prompt templates, separating instructions from user variables.

Let’s implement a code review capability inside our ChatService using a multiline text block placeholder layout:

public String reviewCode(String language, String codeSnippet) {
    String template = """
            Analyze the following {language} code snippet for memory leaks or inefficiencies.
            Provide an optimized version.

            Code:
            {code}
            """;

    return this.defaultChatClient.prompt()
      .user(u -> u.text(template).params(Map.of("language", language, "code", codeSnippet)))
      .call()
      .content();
}

The client replaces the target tags ({language}, {code}) at runtime, formatting the text cleanly before submission. We expose this via a POST request wrapper in our controller:

@PostMapping("/v1/chat/review")
public String reviewCode(@RequestParam(defaultValue = "Java") String language, @RequestBody String codeSnippet) {
    return chatService.reviewCode(language, codeSnippet);
}

This pattern cleanly decouples structural prompt engineering boundaries from volatile business data, producing a highly reusable, parameter-driven invocation pattern.

3.4. Grounding Responses With Live Google Search Retrieval

LLMs naturally suffer from training cut-off windows and knowledge gaps regarding live, real-world developments. The Google GenAI module exposes an explicit grounding property to seamlessly connect our model with live Google Search indexing.

To activate live search grounding across our entire application, we need to toggle the grounding flag to true inside our configuration properties:

spring.ai.google.genai.chat.google-search-retrieval=true

When this property is enabled, queries regarding current events or breaking news are automatically grounded using fresh search results. Let’s write an endpoint to handle real-time informational requests:

public String searchGroundedPrompt(String currentEventQuery) {
    return this.defaultChatClient.prompt()
      .user(currentEventQuery)
      .call()
      .content();
}

We map this method to our HTTP layer inside our controller configuration:

@GetMapping("/v1/chat/grounded")
public String searchGroundedPrompt(@RequestParam String currentEventQuery) {
    return chatService.searchGroundedPrompt(currentEventQuery);
}

If we pass a request like “Who won the most recent football tournament match yesterday?”, the model uses live Google Search indexing to anchor its response in verified facts, drastically reducing hallucinations. This configuration bridges the gap between static training cutoffs and live, real-world events.

3.5. Processing Real-Time Streaming Responses With Flux

For user-facing interfaces, waiting for a long text block to fully generate on the server results in noticeable latency. Instead, we can stream chunks back to the user, token by token, using Spring Boot’s WebFlux integration.

Let’s add a streaming method to our ChatService that returns a reactive Flux<String> structure:

public Flux<String> streamChatTokens(String prompt) {
    return this.defaultChatClient.prompt()
      .user(prompt)
      .stream()
      .content();
}

We’ll create a specialized StreamingChatController to deliver these tokens cleanly over an open connection pipeline. This controller explicitly produces a text/event-stream media response:

@RestController
public class StreamingChatController {

    private final ChatService chatService;

    public StreamingChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @GetMapping(value = "/v1/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamChatTokens(@RequestParam String prompt) {
        return chatService.streamChatTokens(prompt);
    }
}

When hitting this endpoint, consumers read token data reactively.  This establishes a non-blocking execution pipeline that builds an interactive, low-latency UI flow chunk by chunk.

4. Testing

Testing applications that rely on external generative AI endpoints requires a clear separation of concerns. Running tests shouldn’t trigger external network calls, hit rate limits, or exhaust API quotas during local compilation. To prevent this, we isolate our HTTP layer using Spring Boot’s slice testing along with @MockitoBean.

4.1. Unit Testing via WebLayer MockMvc Mocking

Since our implementation encapsulates all LLM communications within ChatService, we can completely mock this business component. This permits us to cleanly assert controller routing, request parameters, response body expectations, and stream-handling patterns.

Let’s write an isolated test using MockMvc to verify all text-based, template-driven, and reactive stream endpoints without triggering an actual network handshake. First, we set up our simple prompt test to verify basic GET parameter routing and raw response mapping:

@SpringBootTest(properties = {
    "spring.ai.model.chat=google-genai",
    "spring.ai.google.genai.chat.model=gemini-2.5-flash",
    "spring.ai.google.genai.api-key=test-key"
})
@AutoConfigureMockMvc
class ChatControllerUnitTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ChatService chatService;

    @Test
    void whenSimpleEndpointIsInvoked_thenReturnsMockedModelResponse() throws Exception {
        when(chatService.simplifiedPrompt(anyString())).thenReturn("Mocked Gemini Response");

        mockMvc.perform(get("/v1/chat/simple").param("message", "Hello"))
          .andExpect(status().isOk())
          .andExpect(content().string("Mocked Gemini Response"));

        verify(chatService).simplifiedPrompt("Hello");
    }
}

Next, we confirm that our fluent prompt endpoint forwards query parameters accurately to the underlying service. Then it returns the configured system-prompt-backed response:

@Test
void whenFluentEndpointIsInvoked_thenReturnsModelContent() throws Exception {
    when(chatService.fluentPrompt(anyString())).thenReturn("Fluent Response");

    mockMvc.perform(get("/v1/chat/fluent").param("prompt", "Explain DI"))
      .andExpect(status().isOk())
      .andExpect(content().string("Fluent Response"));

    verify(chatService).fluentPrompt("Explain DI");
}

For our template-driven endpoint, we validate that the controller correctly processes a multipart payload consisting of query parameters and a text/plain request body:

@Test
void whenReviewEndpointIsInvoked_thenReturnsModelContent() throws Exception {
    when(chatService.reviewCode(anyString(), anyString())).thenReturn("Optimized Code");

    mockMvc.perform(post("/v1/chat/review")
        .param("language", "Java")
        .contentType(MediaType.TEXT_PLAIN)
        .content("class A { }"))
      .andExpect(status().isOk())
      .andExpect(content().string("Optimized Code"));

    verify(chatService).reviewCode("Java", "class A { }");
}

We’ll now verify that search-grounded queries pass their parameters cleanly through the controller layer without parameter binding errors:

@Test
void whenGroundedEndpointIsInvoked_thenReturnsModelContent() throws Exception {
    when(chatService.searchGroundedPrompt(anyString())).thenReturn("Grounded Response");

    mockMvc.perform(get("/v1/chat/grounded").param("currentEventQuery", "latest match winner"))
      .andExpect(status().isOk())
      .andExpect(content().string("Grounded Response"));

    verify(chatService).searchGroundedPrompt("latest match winner");
}

Finally, we test our reactive endpoint to ensure Spring MVC properly negotiates the text/event-stream media type. It will also format the reactive Flux elements into standard Server-Sent Event (data:) frames:

@Test
void whenStreamEndpointIsInvoked_thenReturnsEventStreamContent() throws Exception {
    when(chatService.streamChatTokens(anyString()))
      .thenReturn(reactor.core.publisher.Flux.just("token-1", "token-2"));

    mockMvc.perform(get("/v1/chat/stream").param("prompt", "Stream tokens"))
      .andExpect(status().isOk())
      .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM))
      .andExpect(content().string("data:token-1\n\ndata:token-2\n\n"));
    
    verify(chatService).streamChatTokens("Stream tokens");
}

By leveraging the modern @MockBean annotation, we inject our mock definitions directly into the application context wrapper, replacing the real bean configuration. This allows us to stub out standard synchronous operations as well as complex reactive Flux stream responses safely and reliably without network overhead.

5. Conclusion

In this tutorial, we configured a Spring Boot application using Spring AI’s updated spring-ai-starter-model-google-genai engine to interact directly with Google AI Studio.

We saw how explicitly defining spring.ai.model.chat=google-genai resolves modern multi-model classpath dependencies. From there, we established a clean architecture using a dedicated ChatService. We built out flexible prompt workflows with the fluent ChatClient API and isolated dynamic parameters using templates. Finally, we enabled live web grounding and streamed responsive outputs via WebFlux Flux.

As always, the complete code samples used in this article are available over on GitHub.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)
guest
0 Comments
Oldest
Newest
Inline Feedbacks
View all comments