NOTE欢迎来到“从零搭建AI小程序全栈实战”系列文章!
此为本系列的第二篇文章,你可以点击跳转到第一篇文章在系列文章中快速跳转。
一、这篇文章将要做的
1.1 回顾
上一篇文章中,我们把笔记本GPU推理服务和服务器Spring Boot骨架都跑起来了,网络也打通了。
我们完成了以下目标:
-
笔记本跑通GPU推理——驱动、CUDA、PyTorch、Flask
-
服务器跑通Spring Boot——JDK、Demo应用
-
打通网络——frps、frpc、验证
1.2 这一篇的目标
-
集成MySQL + 完整CRUD
-
图片上传接口
-
调用笔记本AI推理服务
二、集成 MySQL 与 完整 CRUD
2.1 服务器端 安装 MySQL
在服务器上执行:
sudo apt install mysql-server -ysudo mysql_secure_installation # 按提示设置 root 密码,后面都选 Y创建数据库和用户:
sudo mysql -u root -pCREATE DATIF NOT EXISTS mini_app DEFAULT CHARACTER SET utf8mb4;-- 把 'your_password' 改成自己的强密码,记住它CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'your_password';GRANT ALL PRIVILEGES ON mini_app.* TO 'appuser'@'localhost';FLUSH PRIVILEGES;EXIT;2.2 本地项目添加依赖
在你的 pom.xml 里添加依赖(放在 <dependencies> 里现有的 spring-boot-starter-web 旁边):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope></dependency>2.3 配置数据库连接
在 src/main/resources/application.properties 中加入数据库连接(注意替换密码):
spring.datasource.url=jdbc:mysql://你的IP地址:3306/mini_app?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=truespring.datasource.username=appuserspring.datasource.password=你的密码spring.jpa.hibernate.ddl-auto=updatespring.jpa.show-sql=true2.4 创建实体类 ImageRecord
创建 src/main/java/com/example/demo/entity/ImageRecord.java:
package com.example.demo.entity;import jakarta.persistence.*;import java.time.LocalDateTime;
@Entity@Table(name = "image_records")public class ImageRecord { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String originalFilename;
@Column(columnDefinition = "TEXT") private String imageUrl; // 存储路径或base64 private String recognitionResult; private LocalDateTime createTime;
// 必须有无参构造 public ImageRecord() {}
// 快捷构造 public ImageRecord(String originalFilename, String imageUrl) { this.originalFilename = originalFilename; this.imageUrl = imageUrl; this.createTime = LocalDateTime.now(); }
// Getter / Setter (IDE生成,篇幅限制省略,你全加上) // 至少需要:getId setId getOriginalFilename setOriginalFilename ...}2.5 创建 Repository 和 Controller
创建 src/main/java/com/example/demo/repository/ImageRecordRepository.java:
package com.example.demo.repository;import com.example.demo.entity.ImageRecord;import org.springframework.data.jpa.repository.JpaRepository;public interface ImageRecordRepository extends JpaRepository<ImageRecord, Long> {}创建 src/main/java/com/example/demo/repository/ImageController.java:
package com.example.demo.controller;import com.example.demo.entity.ImageRecord;import com.example.demo.repository.ImageRecordRepository;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import java.util.List;
@RestController@RequestMapping("/api/images")public class ImageController { @Autowired private ImageRecordRepository repository;
@PostMapping public ImageRecord create(@RequestBody ImageRecord record) { return repository.save(record); }
@GetMapping public List<ImageRecord> list() { return repository.findAll(); }
@GetMapping("/{id}") public ImageRecord getById(@PathVariable Long id) { return repository.findById(id).orElse(null); }
@DeleteMapping("/{id}") public String delete(@PathVariable Long id) { repository.deleteById(id); return "deleted"; }}2.6 验证
在服务器通过 curl 测试四个接口,确认数据库读写正常。
# 创建一条记录curl -X POST http://localhost:8080/api/images \ -H "Content-Type: application/json" \ -d '{"originalFilename":"test.jpg", "imageUrl":"/uploads/test.jpg"}'
# 查询所有curl http://localhost:8080/api/images三、添加图片上传接口
3.1 配置文件上传参数
application.properties 追加:
spring.servlet.multipart.max-file-size=10MBspring.servlet.multipart.max-request-size=10MBfile.upload-dir=./uploads3.2 添加文件上传接口
在 ImageController 中追加:
import org.springframework.beans.factory.annotation.Value;import org.springframework.web.multipart.MultipartFile;import java.io.File;import java.io.IOException;import java.nio.file.*;
@Value("${file.upload-dir}")private String uploadDir;
@PostMapping("/upload")public ImageRecord upload(@RequestParam("file") MultipartFile file) throws IOException { // 保证目录存在 Files.createDirectories(Paths.get(uploadDir));
// 保存文件 String filename = System.currentTimeMillis() + "_" + file.getOriginalFilename(); File dest = new File(uploadDir + "/" + filename); file.transferTo(dest);
// 存数据库 ImageRecord record = new ImageRecord(file.getOriginalFilename(), "/uploads/" + filename); return repository.save(record);}3.3 配置静态资源映射(让上传的图片可访问)
新建 src/main/java/com/example/demo/config/WebConfig.java:
package com.example.demo.config;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configurationpublic class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceLocations("file:./uploads/"); }}3.4 验证
我们使用 curl 模拟上传:
curl -X POST http://localhost:8080/api/images/upload \ -F "file=@/home/ubuntu/一张测试图片.jpg"返回结果里应该有 imageUrl: "/uploads/xxxx.jpg",浏览器访问 http://服务器IP:8080/uploads/xxxx.jpg 能显示图片,这样就成功了。
四、调用 Windows端 AI推理服务
4.1 确认 API 可达
先在服务器上验证穿透仍正常(笔记本端 frpc 要开着):
curl http://localhost:5000/health返回 {"cuda":true} 就 OK。
4.2 创建调用 Windows端 API 的服务
创建 src/main/java/com/example/demo/service/AiService.java:
package com.example.demo.service;import org.springframework.stereotype.Service;import org.springframework.web.client.RestTemplate;import java.util.Map;
@Servicepublic class AiService { private final RestTemplate restTemplate = new RestTemplate(); private final String AI_BASE_URL = "http://localhost:5000";
public Map<String, Object> predict(Map<String, Object> input) { return restTemplate.postForObject( AI_BASE_URL + "/predict", input, Map.class); }
public Map<String, Object> health() { return restTemplate.getForObject(AI_BASE_URL + "/health", Map.class); }}4.3 在 Controller 中串联
修改 ImageController,注入 AiService 并添加一个“上传即识别”的接口:
@Autowiredprivate AiService aiService;
@PostMapping("/upload-and-recognize")public ImageRecord uploadAndRecognize(@RequestParam("file") MultipartFile file) throws IOException { // 1. 保存文件(复用上面的上传逻辑) Files.createDirectories(Paths.get(uploadDir)); String filename = System.currentTimeMillis() + "_" + file.getOriginalFilename(); File dest = new File(uploadDir + "/" + filename); file.transferTo(dest);
// 2. 调用笔记本 AI 推理 Map<String, Object> aiInput = Map.of("imageUrl", "/uploads/" + filename); Map<String, Object> result = aiService.predict(aiInput);
// 3. 存库 ImageRecord record = new ImageRecord(file.getOriginalFilename(), "/uploads/" + filename); record.setRecognitionResult(result.toString()); // 实际项目解析具体字段 return repository.save(record);}4.4 全链路测试
我们使用 curl 测试:
curl -X POST http://localhost:8080/api/images/upload-and-recognize \ -F "file=@/home/ubuntu/一张测试图片.jpg"如果返回记录中包含 recognitionResult 字段(即使是 "fake_prediction" 也算成功),代表 Spring Boot → 笔记本 AI 调用链路已打通。
五、打包部署到 Linux
5.1 打包 JAR
5.1.1 确认 JAR 文件名
在打包之前,先看一下项目最终生成的JAR叫什么名字。这个名称由 pom.xml 中的三个标签决定:
<groupId>com.example</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version>拼接规则:{artifactId}-{version}.jar
所以你的JAR文件名叫:demo-0.0.1-SNAPSHOT.jar
5.1.2 在 IDEA 中打包
-
IDEA 右侧边栏点击 Maven(竖排的 “m” 图标)
-
展开
demo→Lifecycle -
先双击 clean(清理上次构建的旧文件,避免混入过期内容)
-
等待
BUILD SUCCESS,再双击 package -
控制台出现以下字样即打包成功:
[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------
5.1.3 找到打包好的 JAR
打包完成后,JAR 文件位于项目的 target 目录下:
你的项目目录/├── src/├── target/│ ├── demo-0.0.1-SNAPSHOT.jar ← 这个就是我们要的│ └── ...├── pom.xml└── ...在 IDEA 中直接查看:
IDEA 左侧项目文件树,找到 target 文件夹,展开即可看到 demo-0.0.1-SNAPSHOT.jar。
右键点击 JAR 文件 → Open In → Explorer(或 Finder),可以直接在文件资源管理器中定位到该文件,方便下一步上传。
5.2 上传到服务器
TIP如果服务器上没有
/opt/app目录,先 SSH 到服务器执行mkdir -p /opt/app创建。
在 Powershell (Win + R 输入 powershell)使用 scp 命令上传服务器:
scp target/demo-xxxx.jar terryc@1.2.3.4:/opt/app-
terryc此处替换为你的用户名,如root。 -
@1.2.3.4此处替换为你的服务器外网IP地址,如@111.111.111.111。 -
:/opt/app此处为你的应用工作目录,你的项目将会在这个目录启动,可替换为其他目录。
5.3 在服务器上启动 Spring Boot 项目
以文件名 /opt/app/demo-0.0.1-SNAPSHOT.jar 为例。
5.3.1 使用 nohup 启动
nohup 是最简单的后台启动方式,适合临时测试:
nohup java -jar /opt/app/demo-0.0.1-SNAPSHOT.jar > /opt/app/app.log 2>&1 &-
nohup:退出SSH后进程不中断 -
> app.log 2>&1:把日志输出到文件 -
&:后台运行
让我们来验证Spring Boot项目是否启动:
tail -f /opt/app/app.log看到 Started DemoApplication in ... seconds 即启动成功。
但这种方式有明显缺点:
-
重启服务器后不会自动启动,需要手动执行
-
想停止只能手动
ps -ef | grep java找到PID然后kill -
更新JAR包时步骤繁琐:停进程 → 换包 → 重启
-
时间久了容易忘记项目在哪个目录、用的哪个JAR
所以,下面我们换成更规范的方式。
5.3.2 使用 service 启动(推荐)
5.3.2.0 为什么要推荐 service ?
| 对比项 | nohup | systemd service |
|---|---|---|
| 开机自启 | ❌ 需要手动设置脚本 | ✅ systemctl enable 一行搞定 |
| 启停操作 | 找PID → kill | systemctl start/stop/restart |
| 崩溃重启 | ❌ 进程挂了就挂了 | ✅ 配置 Restart=on-failure 自动拉起 |
| 日志管理 | 自己用 > 重定向 | journalctl -u demo -f 统一查看 |
| 版本更新 | 手动替换JAR,容易出错 | 配合软链接,换JAR后 systemctl restart 即可 |
| 状态查看 | ps -ef | grep java | systemctl status demo 一眼看清 |
5.3.2.1 创建 demo.service 文件
在服务器上创建 service 配置文件:
sudo vim /etc/systemd/system/demo.service写入以下内容:
[Unit]Description=Spring Boot Demo ApplicationAfter=network.target
[Service]Type=simpleUser=rootWorkingDirectory=/opt/appExecStart=/usr/bin/java -jar /opt/app/demo-current/demo-0.0.1-SNAPSHOT.jarRestart=on-failureRestartSec=10
[Install]WantedBy=multi-user.target关键字段解释:
| 字段 | 含义 |
|---|---|
After=network.target | 等网络就绪后再启动,避免数据库连不上 |
Type=simple | 标准类型,适用于Java这种前台进程 |
WorkingDirectory | 工作目录,上传的文件会存在这里 |
ExecStart | 启动命令,指向 /opt/app/demo-current.jar |
Restart=on-failure | 进程崩溃时自动重启 |
RestartSec=10 | 等10秒再重启,避免频繁重启死循环 |
WantedBy=multi-user.target | 多用户模式下自动启动 |
5.3.2.2 创建 demo-current.jar 软链接
以后更新JAR包时,新版本文件名会是 demo-0.0.2-SNAPSHOT.jar、demo-1.0.0.jar 等。如果 service 文件里写死了具体文件名,每次更新还要改service配置,太麻烦。
用软链接的方式:service 永远指向 demo-current.jar,更新时只需把软链接指向新版本JAR即可。
# 创建软链接,指向当前版本的JARln -s /opt/app/demo-0.0.1-SNAPSHOT.jar /opt/app/demo-current.jar执行后,/opt/app/demo-current.jar 就指向了真实的JAR文件。可以用 ls -l 验证:
ls -l /opt/app/demo-current.jar# 输出:demo-current.jar -> /opt/app/demo-0.0.1-SNAPSHOT.jar5.3.2.3 启动并启用服务
# 重新加载 service 配置sudo systemctl daemon-reload
# 启动服务sudo systemctl start demo
# 设置开机自启sudo systemctl enable demo
# 查看状态sudo systemctl status demo看到 active (running) 即成功。
常用管理命令:
sudo systemctl stop demo # 停止sudo systemctl restart demo # 重启sudo systemctl status demo # 查看状态journalctl -u demo -f # 实时查看日志5.4 验证部署成功
使用 curl 对项目接口进行测试。
# 服务器本地测试curl http://localhost:8080/hello# 应返回:Hello from Server!
curl http://localhost:8080/api/images# 应返回已创建的图片记录列表
# 浏览器访问http://你的服务器公网IP:8080/hellohttp://你的服务器公网IP:8080/api/images六、验收清单
-
GET/POST/DELETE
/api/images四个接口全部正常 -
POST
/api/images/upload能接收图片并返回可访问URL -
POST
/api/images/upload-and-recognize能调通AI并保存识别结果