Spring AI's ChatClient Turns a Java CRUD App Into a Poetry Bot in Under 50 Lines
Prologue: As a Java veteran who has written CRUD for ten years, I never imagined that making a machine write poetry would be simpler than writing a query interface.
1. Selection Tip: Spring AI vs Spring AI Alibaba
Before writing code, let's solve a problem that bothered me for a long time.
Searching for "spring-ai" in the Maven repository yields two things that look similar—Spring AI and spring-ai-alibaba. Which one to use?
I later figured out that their relationship is much like JDBC and the MySQL driver:
- Spring AI is the standard interface layer. It defines a unified set of APIs—ChatClient, Function Calling, MCP, RAG—and the code you write is the same regardless of whether the underlying model is OpenAI, Tongyi Qianwen, or a local Ollama model.
- Spring AI Alibaba is an enhanced implementation by Alibaba Cloud based on this standard, deeply integrating the Alibaba Cloud Bailian platform and additionally providing unique features like a Graph workflow engine.
The selection is actually quite simple:
| Your Situation | Recommended Choice |
|---|---|
| Just starting to learn, want to run your first conversation | Spring AI |
| Company already using Alibaba Cloud Bailian | Spring AI Alibaba |
| Want to tinker with local models, switch vendors | Spring AI |
Selection for this series: Spring AI + Tongyi Qianwen (accessed via the OpenAI-compatible interface)
In one sentence: When learning to drive, start with a standard license; don't bind yourself to a specific brand's autonomous driving system right away.
2. Environment Requirements
2.1. Version Requirements
- Java Version
- Minimum: Java 17
- Recommended: Java 17 / Java 21
- Spring Boot Version
- Minimum: Spring Boot 3.2.x
- Recommended: 3.2.x / 3.3.x
Underlying dependency is Spring Framework 6, which mandates JDK 17
2.2. Obtaining an API Key
This series uses Tongyi Qianwen (via the OpenAI-compatible interface) because it has stable access within China and offers free credits upon registration.
- Open the Alibaba Cloud Bailian Platform
- After registering/logging in, activate the model service
- Create a Key under "API Key Management"
2.3. Creating the Project
Create a Spring Boot project; this shouldn't need teaching!
<?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.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yunxi</groupId>
<artifactId>yunxi-spring-ai</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>yunxi-spring-ai</name>
<description>yunxi-spring-ai</description>
<modules>
<module>chapter-01-hello-ai</module>
</modules>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
</project>
Special note: Spring AI is developing rapidly; you might have copied an old version from some blog.
spring-ai-starter-openai is an old name (deprecated)
spring-ai-openai-spring-boot-starter (historical old artifact, alias)⚠️ The 1.0 official version renamed it; new projects should not copy this coordinate from old online demos!
spring-ai-starter-model-openai is the new official standard name for Spring AI 1.0 GA+ (must be used now)
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yunxi</groupId>
<artifactId>yunxi-spring-ai</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>chapter-01-hello-ai</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
spring:
ai:
openai:
api-key: ${AI_API_KEY} # Read from environment variable
base-url: https://dashscope.aliyuncs.com/compatible-mode
chat:
options:
model: qwen3.7-max
server:
port: 8080
Note: api-key is read from the environment variable via ${AI_API_KEY}. Do not write the Key directly into the config file and commit it to Git!
Set on startup: export AI_API_KEY=yourKey
3. The First Line of AI Code
3.1. Simplest Approach
package com.oldbird.ai.chapter01;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Chapter01Application {
public static void main(String[] args) {
SpringApplication.run(Chapter01Application.class, args);
}
@Bean
CommandLineRunner runner(ChatClient.Builder builder) {
return args -> {
ChatClient chatClient = builder.build();
String response = chatClient.prompt()
.user("Please write a five-character quatrain in the style of a Java programmer, on the theme 'The Beauty of Code'")
.call()
.content();
System.out.println(response);
};
}
}
3.2. Execution Result
Start the project, console output:
```java
/**
* @ClassName: CodeBeauty
* @Description: Ode to the Beauty of Code
* @Author: JVM_Poet
* @Date: 2023-10-24
*/
public class CodeBeauty {
public static void main(String[] args) {
System.out.println("类聚千般法,");
System.out.println("封装百脉通。");
System.out.println("空指休生恨,");
System.out.println("回收自随风。");
}
}
```
### 📝 Code Review (Poem Analysis):
* **类聚千般法** (Opening)
**Java Mapping**: `Class` and `Method`.
**Imagery**: In the Java world, "everything is an object." An elegant `Class` aggregates a myriad of methods, building vast and orderly business logic. This is the **beauty of structure**.
* **封装百脉通** (Development)
**Java Mapping**: `Encapsulation`.
**Imagery**: The foremost of the three major object-oriented characteristics. Good encapsulation and interface design make code highly cohesive and loosely coupled, with data flowing like the body's meridians. This is the **beauty of architecture**.
* **空指休生恨** (Turn)
**Java Mapping**: `NullPointerException`.
**Imagery**: NPE is the most familiar "old friend" of Java programmers. When encountering an error, there's no need to smash the keyboard in hatred; calmly add `Optional` or null-check logic. Fixing code is also fixing the mind. This is the **beauty of open-mindedness**.
* **回收自随风** (Conclusion)
**Java Mapping**: `Garbage Collection`.
**Imagery**: The JVM silently runs GC in the background, automatically cleaning up objects that have lost references and freeing memory. No need for manual `free` like in C/C++; useless things drift away like fallen leaves in the wind. This is the **beauty of mechanism**.
*(Note: This poem follows the "Yi Dong" rhyme of Pingshui Yun, complies with tonal patterns, compiles without errors, has no warnings, and can be Run directly.)*
(Each run's result will be slightly different; after all, this is generative AI, not printing a fixed string.)
4. Adding Some Engineering: Separating Controller and Service
The code above is all crammed into the startup class, which is too unprofessional. Let's split it up:
package com.oldbird.ai.chapter01.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String chat(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}
package com.oldbird.ai.chapter01.controller;
import com.oldbird.ai.chapter01.service.ChatService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) {
this.chatService = chatService;
}
@GetMapping("/chat")
public String chat(@RequestParam(defaultValue = "Hello") String message) {
return chatService.chat(message);
}
}
After starting, visit: <http://localhost:8080/chat?message=Write a poem about working overtime in the style of a programmer>
```java /** * @Title: Overtime.java * @Description: Dedicated to every developer battling bugs late at night * @Author: A programmer who hasn't left work yet * @Date: 202X-XX-XX 03:00:00 AM */ ``` The night is a silent `git commit`, Pushed to the remote branch named "early morning." The screen's cold light is the only `stdout`, Outputting my restless `anxiety`. The keyboard taps the mantra of `while(true)`, Trying to find a `break` miracle within a logical deadlock. Takeout boxes pile up like unhandled `Exception`s, And my hairline faces a `StackOverflow` crisis. Product requirements are asynchronous `Promise`s, Forever `pending`, with no `resolve` date. I `try-catch` all the `NullPointer`s, But cannot `catch` the dawn's light outside the window. When the east glows with the hex value `#FFFFFF`, The terminal finally returns a sigh of `0 Errors`. I type `git push --force`, Forcefully overwriting the fatigue into the server's `history`. Now, call `Thread.sleep()` to enter hibernation, Releasing all the `dependency` of this body. Until tomorrow morning, triggered again by the `CronJob`, Starting the next cycle of infinite recursion. ```bash > Process finished with exit code 0 > System entering sleep mode... ```
5. Code Explanation: What Exactly Does ChatClient Do?
A backend veteran seeing the chained call chatClient.prompt().user().call().content() might wonder: what happens underneath?
Essentially, it does what previously required manual coding:
// Without Spring AI, you would need to write this:
HttpRequest request = HttpRequest.newBuilder()
.uri("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions")
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("""
{
"model": "qwen-plus",
"messages": [
{"role": "user", "content": "Write a poem"}
]
}
"""))
.build();
HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());
// Then manually parse JSON, handle exceptions, handle streaming...
Spring AI encapsulates all this dirty work for us. ChatClient is the RestTemplate for large models; switching model providers is like switching a database connection—change the config, not the code.
6. Pitfall Avoidance Guide for This Chapter
6.1. Pitfall 1: Incorrect base-url Path
The compatible interface path for Tongyi Qianwen is /compatible-mode, not /compatible-mode/v1. Adding this extra suffix will result in a 404.
6.2. Pitfall 2: Writing the API Key Directly into yml
Use ${AI_API_KEY} to read from an environment variable, then add application.yml to .gitignore or use environment variable overrides. Committing it to GitHub will get it scanned and stolen by others.
6.3. Pitfall 3: Incorrect Model Name
qwen-plus, qwen-turbo, qwen-max are different models with significant performance differences.
7. Chapter Summary
In this first chapter, we accomplished three things:
- Clarified the relationship between Spring AI and spring-ai-alibaba
- Made AI write a poem with less than 50 lines of code
- Completed the first engineered project structure
In the next chapter, we will solve a problem that makes programmers deeply uncomfortable—hardcoding Prompts in the code is too inelegant. I want to put them in a configuration file.
This article was co-created with DeepSeek