Add AI to a Spring Boot Project in a Few Lines with Spring AI 2.0
This guide walks through integrating Spring AI 2.0 into a Spring Boot application, covering Maven dependency setup, model configuration, ChatClient usage for synchronous and streaming calls, Redis‑backed chat memory for multi‑turn conversations, and testing with Postman and a UI component.
Spring AI Overview
Multi‑model, multi‑vendor support – a single codebase works with OpenAI, Deepseek, Anthropic, Ollama and other models for chat, text‑to‑image, speech‑to‑text, etc.
ChatClient streaming API – provides call() for synchronous calls and stream() for SSE‑style streaming.
Advisors API – encapsulates RAG, conversation memory, safety filters and other patterns as reusable interceptor chains.
Chat Memory – built‑in, Redis, JDBC and other back‑ends automatically manage multi‑turn conversation history.
RAG – one‑line vector‑store integration lets a model read your documents.
Tool Calling – annotate Java methods with @Tool so the model can invoke real APIs.
Project Integration
Step 1 – Add the Spring AI BOM to pom.xml (version 2.0.0) to manage consistent dependency versions.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Step 2 – Add the OpenAI‑compatible model starter and the WebFlux starter (required for SSE).
<!-- Spring AI OpenAI Starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- SpringBoot WebFlux starter (provides Flux/Mono) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>Step 3 – Configure the model in application.yaml . The example uses Deepseek‑v4‑pro via the OpenAI‑compatible endpoint.
spring:
ai:
openai:
# Access Deepseek through OpenAI‑compatible API
base-url: https://api.deepseek.com
api-key: ${DEEPSEEK_API_KEY}
chat:
model: deepseek-v4-proStep 4 – Define a configuration class that creates a ChatClient bean.
@Configuration
public class SpringAIConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder.build();
}
}Chat Service Implementation
Create ChatController with two endpoints: a synchronous /call and a streaming /stream that returns MediaType.TEXT_EVENT_STREAM_VALUE .
@RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class ChatController {
private final ChatClient chatClient;
@PostMapping("/call")
public String call(@RequestParam String question, @RequestParam String conversationId) {
return this.chatClient.prompt()
.user(question)
.call()
.content();
}
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatEventDto> stream(@RequestParam String question, @RequestParam String conversationId) {
return this.chatClient.prompt()
.user(question)
.stream()
.content()
.map(content -> ChatEventDto.builder()
.eventType(ChatEventType.DATA.getValue())
.eventData(content)
.build())
.concatWith(Flux.just(ChatEventDto.builder()
.eventType(ChatEventType.STOP.getValue())
.build()));
}
}Define ChatEventDto to wrap SSE messages.
@Data @Builder @NoArgsConstructor @AllArgsConstructor @Schema(title = "ChatEventDto", description = "SSE session event result")
public class ChatEventDto {
@Schema(description = "Event type: 1001‑data, 1002‑stop, 1003‑parameter")
private Integer eventType;
@Schema(description = "Message content")
private Object eventData;
}Conversation Memory with Redis
Run Redis‑Stack (required for vector and JSON capabilities) via Docker.
docker run --name redis-stack \
-p 6379:6379 \
-p 8001:8001 \
-d redis/redis-stack:7.4.0-v8Add the Redis chat‑memory starter.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-redis</artifactId>
</dependency>Configure Redis connection details in application.yaml .
spring:
ai:
chat:
memory:
redis:
host: 192.168.3.101
port: 6379
time-to-live: 24h
key-prefix: spring-chatDefine beans for RedisChatMemoryRepository and a ChatMemory that uses a MessageWindowChatMemory with a maximum of 100 messages.
@Configuration
public class SpringAIConfig {
@Value("${spring.ai.chat.memory.redis.host}")
private String redisHost;
@Value("${spring.ai.chat.memory.redis.port}")
private int redisPort;
@Bean
public RedisChatMemoryRepository redisChatMemoryRepository() {
RedisClient redisClient = RedisClient.builder()
.hostAndPort(redisHost, redisPort)
.build();
return RedisChatMemoryRepository.builder()
.jedisClient(redisClient)
.build();
}
@Bean
public ChatMemory chatMemory(RedisChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(100)
.build();
}
}Inject ChatMemory into ChatController and use it to store and retrieve conversation history, enabling true multi‑turn dialogue.
private final ChatMemory chatMemory;
// Retrieve history
List<Message> history = chatMemory.get(conversationId);
// Save user message
chatMemory.add(conversationId, List.of(new UserMessage(question)));
// After model response, save assistant message
chatMemory.add(conversationId, List.of(new AssistantMessage(response)));
// History endpoint
@GetMapping("/history")
public CommonResult<List<Map<String, Object>>> history(@RequestParam String conversationId) {
var result = chatMemory.get(conversationId).stream()
.map(msg -> Map.of(
"role", msg.getMessageType().name(),
"content", msg.getText()))
.collect(Collectors.toList());
return CommonResult.success(result);
}
// Clear history endpoint
@PostMapping("/clearHistory")
public CommonResult<Void> clearHistory(@RequestParam String conversationId) {
chatMemory.clear(conversationId);
return CommonResult.success(null);
}Streaming with Memory
In the streaming endpoint, accumulate the full assistant reply and store it after the stream completes.
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatEventDto> stream(@RequestParam String question, @RequestParam String conversationId) {
List<Message> history = chatMemory.get(conversationId);
chatMemory.add(conversationId, List.of(new UserMessage(question)));
StringBuilder fullResponse = new StringBuilder();
return this.chatClient.prompt()
.messages(history)
.user(question)
.stream()
.content()
.map(content -> {
fullResponse.append(content);
return ChatEventDto.builder()
.eventType(ChatEventType.DATA.getValue())
.eventData(content)
.build();
})
.concatWith(Flux.just(ChatEventDto.builder()
.eventType(ChatEventType.STOP.getValue())
.build()))
.doOnComplete(() -> {
if (!fullResponse.isEmpty()) {
chatMemory.add(conversationId, List.of(new AssistantMessage(fullResponse.toString())));
}
});
}Testing the Service
Using Postman, a call to /call returns a single JSON response. A call to /stream returns incremental SSE messages. After enabling Redis memory, repeated queries reference earlier turns, confirming multi‑turn conversation capability.
Source Code
All code and configuration are available at https://github.com/macrozheng/spring-ai-examples
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
SpringMeng
Focused on software development, sharing source code and tutorials for various systems.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
