跪拜 Guibai
← Back to the summary

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:

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

  1. Java Version
  1. Spring Boot Version

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.

  1. Open the Alibaba Cloud Bailian Platform
  2. After registering/logging in, activate the model service
  3. 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:

  1. Clarified the relationship between Spring AI and spring-ai-alibaba
  2. Made AI write a poem with less than 50 lines of code
  3. 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