一、写在前面:为什么要做SpringBoot+YOLOv8的工程化落地?
最近接手一个工业零件缺陷检测的项目,甲方明确要求:所有服务必须基于Java生态(生产线现有系统都是SpringBoot栈),且接口要满足“7×24小时可用、支持高并发、响应时间≤200ms”的上线标准。
YOLOv8是目前工业场景目标检测的首选,但网上的教程要么是Python脚本级别的调用,要么是Java的demo级代码(没有异常处理、没有性能优化、没有工程化设计),根本没法直接上线。踩了两周坑后,整理出这篇从工程搭建到上线部署的全流程,最终实现的接口支持图片上传/Base64入参、自带限流/异常兜底、推理性能可控,完全满足工业级上线要求。
二、核心目标与前置认知
1. 最终要实现的接口能力
-
支持两种入参:图片文件上传、Base64编码字符串(适配不同前端场景);
-
输出结构化检测结果:缺陷类别、置信度、坐标、检测耗时;
-
工程化特性:接口限流、全局异常处理、模型单例加载、资源自动释放;
-
性能指标:单帧640×640图片推理耗时≤150ms,QPS≥5(边缘工控机场景)。
2. SpringBoot+YOLOv8的核心依赖逻辑
| 组件 | 作用 |
|---|---|
| ONNX Runtime Java | 核心推理引擎,加载YOLOv8的ONNX模型(拒绝Python依赖) |
| JavaCV | 图像处理(替代OpenCV-Java,更轻量,适配多平台) |
| SpringBoot Web | 提供HTTP接口,处理文件上传/Base64解析 |
| SpringBoot AOP | 实现接口限流、耗时统计(非必需,但上线必备) |
| HikariCP | (可选)连接池思想复用,管理模型推理的线程池 |
3. 环境准备(Windows/Linux通用,上线建议Linux)
-
JDK 11(JDK8也兼容,但11的内存管理更优);
-
Maven 3.8+;
-
YOLOv8 ONNX模型(自己训练或官方预训练模型,务必注意导出参数);
-
边缘工控机/服务器:CPU≥4核,内存≥4GB(GPU加速需CUDA 11.8+)。
三、第一步:YOLOv8模型导出(上线的第一道坑)
重点:导出参数错了,Java调用必崩! 很多人卡在这里,核心是要固定输入尺寸、关闭动态维度、选对opset版本。
- 先安装Ultralytics(仅用于导出模型,部署时无需Python):
pip install ultralytics==8.2.83
- 导出ONNX模型(核心参数解释,避免踩坑):
# 以自定义训练的零件缺陷检测模型为例
from ultralytics import YOLO
# 加载训练好的模型(官方预训练模型直接写yolov8s.pt即可)
model = YOLO("runs/detect/train/weights/best.pt")
# 导出命令(每一个参数都不能少!)
model.export(
format="onnx", # 导出格式
opset=17, # YOLOv8推荐opset≥17,Java ONNX Runtime兼容最好
imgsz=640, # 固定输入尺寸,和后续Java配置一致
dynamic=False, # 关闭动态维度(上线场景固定尺寸更稳定)
simplify=True, # 简化模型,提升推理速度
device="cpu", # 先导出CPU版,GPU版后续优化
include=["onnx"] # 仅保留ONNX格式
)
导出成功后会生成best.onnx文件,记住两个关键信息:
-
输入尺寸:640×640(和导出的imgsz一致);
-
类别数:比如缺陷检测分crack(裂纹)、missing(缺角)、deformation(变形),共3类。
四、第二步:SpringBoot工程搭建(工程化设计,拒绝烂代码)
1. 工程结构(按“可上线”标准设计,分层清晰)
com.yolov8.detect/
├── config/ # 配置层(模型参数、线程池、限流)
│ ├── YoloConfig.java # YOLO核心参数配置
│ ├── ThreadPoolConfig.java # 推理线程池配置
│ └── WebConfig.java # Web相关配置
├── controller/ # 接口层(文件上传、Base64接口)
│ ├── DetectController.java
│ └── dto/ # 入参/出参DTO(结构化)
├── service/ # 业务层(核心推理逻辑)
│ ├── YoloDetectService.java
│ └── impl/
│ └── YoloDetectServiceImpl.java
├── util/ # 工具层(图像处理、Base64、异常)
│ ├── ImageUtil.java
│ ├── Base64Util.java
│ └── GlobalExceptionHandler.java
├── exception/ # 自定义异常(上线必备)
│ ├── ModelLoadException.java
│ └── DetectException.java
└── Yolov8DetectApplication.java # 启动类
2. Maven依赖(精准引入,避免冗余)
<?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>2.7.15</version> <!-- 稳定版,适合上线 -->
<relativePath/>
</parent>
<groupId>com.yolov8</groupId>
<artifactId>yolov8-detect-api</artifactId>
<version>1.0.0</version>
<name>yolov8-detect-api</name>
<dependencies>
<!-- SpringBoot Web核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot AOP(限流、耗时统计) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- ONNX Runtime(CPU版,Linux/Windows自动适配) -->
<dependency>
<groupId>ai.onnxruntime</groupId>
<artifactId>onnxruntime</artifactId>
<version>1.16.3</version>
<classifier>${os.detected.classifier}</classifier>
</dependency>
<!-- JavaCV(图像处理,轻量) -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.9</version>
<!-- 排除冗余依赖,减小包体积 -->
<exclusions>
<exclusion>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
<!-- 日志(上线必备,替换默认logback) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.8</version>
</dependency>
<!-- 接口限流 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
</dependencies>
<!-- 自动适配操作系统的ONNX Runtime依赖 -->
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.0</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
五、第三步:核心代码实现(工程化+可上线)
1. 配置层:参数可配置,避免硬编码
YoloConfig.java(核心参数集中管理)
package com.yolov8.detect.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
/**
* YOLOv8核心配置(支持配置文件修改,无需改代码)
*/
@Configuration
public class YoloConfig {
// 模型路径(支持绝对路径/相对路径)
@Value("${yolo.model.path:classpath:model/best.onnx}")
private String modelPath;
// 输入尺寸(和导出模型一致)
@Value("${yolo.input.width:640}")
private int inputWidth;
@Value("${yolo.input.height:640}")
private int inputHeight;
// 置信度阈值(过滤低置信度结果)
@Value("${yolo.conf.threshold:0.6}")
private float confThreshold;
// IOU阈值(NMS去重)
@Value("${yolo.iou.threshold:0.45}")
private float iouThreshold;
// 检测类别(和训练的数据集一致)
@Value("${yolo.class.names:crack,missing,deformation}")
private String classNames;
// getter(上线建议用lombok,这里为了无依赖手写)
public String getModelPath() {
return modelPath;
}
public int getInputWidth() {
return inputWidth;
}
public int getInputHeight() {
return inputHeight;
}
public float getConfThreshold() {
return confThreshold;
}
public float getIouThreshold() {
return iouThreshold;
}
public String[] getClassNames() {
return classNames.split(",");
}
}
ThreadPoolConfig.java(推理线程池,避免线程爆炸)
package com.yolov8.detect.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.*;
/**
* 推理线程池配置(上线必备,控制并发数)
*/
@Configuration
public class ThreadPoolConfig {
/**
* YOLO推理线程池(核心数=CPU核心数,避免资源抢占)
*/
@Bean("yoloDetectThreadPool")
public ExecutorService yoloDetectThreadPool() {
int corePoolSize = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
corePoolSize,
corePoolSize * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 队列长度控制最大并发
new ThreadFactory() {
private int count = 0;
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("yolo-detect-thread-" + count++);
return thread;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:主线程执行,避免请求丢失
);
}
}
2. 工具层:通用能力封装,避免重复代码
ImageUtil.java(图像处理核心)
package com.yolov8.detect.util;
import com.yolov8.detect.config.YoloConfig;
import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Rect;
import org.bytedeco.opencv.opencv_core.Size;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 图像处理工具类(YOLOv8预处理/后处理)
*/
@Component
public class ImageUtil {
@Resource
private YoloConfig yoloConfig;
/**
* 图像预处理:缩放+填充+归一化+转张量
*/
public float[][][][] preprocess(Mat srcMat) {
// 1. 计算缩放比例(保持宽高比,避免拉伸)
float scale = Math.min(
(float) yoloConfig.getInputWidth() / srcMat.cols(),
(float) yoloConfig.getInputHeight() / srcMat.rows()
);
int newW = (int) (srcMat.cols() * scale);
int newH = (int) (srcMat.rows() * scale);
// 2. 缩放图像
Mat resizedMat = new Mat();
opencv_imgproc.resize(srcMat, resizedMat, new Size(newW, newH));
// 3. 填充黑边(补到输入尺寸)
Mat paddedMat = Mat.zeros(yoloConfig.getInputHeight(), yoloConfig.getInputWidth(), opencv_core.CV_8UC3);
resizedMat.copyTo(paddedMat.apply(new Rect(
(yoloConfig.getInputWidth() - newW) / 2,
(yoloConfig.getInputHeight() - newH) / 2,
newW,
newH
)));
// 4. 归一化+转RGB(YOLO输入要求BGR→RGB,值归一化到0~1)
float[][][][] inputTensor = new float[1][3][yoloConfig.getInputHeight()][yoloConfig.getInputWidth()];
for (int c = 0; c < 3; c++) {
for (int h = 0; h < yoloConfig.getInputHeight(); h++) {
for (int w = 0; w < yoloConfig.getInputWidth(); w++) {
inputTensor[0][c][h][w] = (float) paddedMat.ptr(h, w).get(2 - c) / 255.0f;
}
}
}
// 释放Mat,避免内存泄漏(上线必做)
resizedMat.release();
paddedMat.release();
return inputTensor;
}
/**
* 张量扁平化(适配ONNX Runtime输入)
*/
public float[] flatten(float[][][][] tensor) {
int size = 1 * 3 * yoloConfig.getInputHeight() * yoloConfig.getInputWidth();
float[] flat = new float[size];
int idx = 0;
for (int c = 0; c < 3; c++) {
for (int h = 0; h < yoloConfig.getInputHeight(); h++) {
for (int w = 0; w < yoloConfig.getInputWidth(); w++) {
flat[idx++] = tensor[0][c][h][w];
}
}
}
return flat;
}
}
3. 业务层:核心推理逻辑(单例加载模型,避免重复加载)
YoloDetectService.java(接口定义)
package com.yolov8.detect.service;
import com.yolov8.detect.controller.dto.DetectResponse;
import org.springframework.web.multipart.MultipartFile;
/**
* YOLOv8检测服务接口
*/
public interface YoloDetectService {
/**
* 图片文件检测
*/
DetectResponse detectByFile(MultipartFile file);
/**
* Base64字符串检测
*/
DetectResponse detectByBase64(String base64Str);
}
YoloDetectServiceImpl.java(核心实现,上线级代码)
package com.yolov8.detect.service.impl;
import ai.onnxruntime.*;
import com.yolov8.detect.config.YoloConfig;
import com.yolov8.detect.controller.dto.DetectResult;
import com.yolov8.detect.controller.dto.DetectResponse;
import com.yolov8.detect.exception.DetectException;
import com.yolov8.detect.exception.ModelLoadException;
import com.yolov8.detect.service.YoloDetectService;
import com.yolov8.detect.util.Base64Util;
import com.yolov8.detect.util.ImageUtil;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.opencv.global.opencv_imgcodecs;
import org.bytedeco.opencv.opencv_core.Mat;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.nio.FloatBuffer;
import java.util.*;
import java.util.concurrent.ExecutorService;
/**
* YOLOv8检测服务实现(单例加载模型,工程化设计)
*/
@Service
@Slf4j
public class YoloDetectServiceImpl implements YoloDetectService {
@Resource
private YoloConfig yoloConfig;
@Resource
private ImageUtil imageUtil;
@Resource(name = "yoloDetectThreadPool")
private ExecutorService detectThreadPool;
// 模型相关(单例,避免重复加载)
private OrtEnvironment env;
private OrtSession session;
/**
* 服务启动时加载模型(仅加载一次,上线必备)
*/
@PostConstruct
public void initModel() {
long start = System.currentTimeMillis();
try {
log.info("开始加载YOLOv8模型,路径:{}", yoloConfig.getModelPath());
env = OrtEnvironment.getEnvironment();
OrtSession.SessionOptions options = new OrtSession.SessionOptions();
// 1. 线程数优化:使用配置的核心数,避免资源浪费
options.setIntraOpNumThreads(Runtime.getRuntime().availableProcessors() / 2);
// 2. 开启全量图优化(提升推理速度)
options.setGraphOptimizationLevel(OrtSession.SessionOptions.GraphOptimizationLevel.ALL);
// 3. 加载模型(处理classpath路径)
String realModelPath = yoloConfig.getModelPath().startsWith("classpath:")
? this.getClass().getResource("/" + yoloConfig.getModelPath().replace("classpath:", "")).getPath()
: yoloConfig.getModelPath();
session = env.createSession(realModelPath, options);
log.info("YOLOv8模型加载成功,耗时:{}ms", System.currentTimeMillis() - start);
} catch (OrtException e) {
log.error("模型加载失败", e);
throw new ModelLoadException("YOLOv8模型加载失败:" + e.getMessage());
}
}
@Override
public DetectResponse detectByFile(MultipartFile file) {
// 1. 参数校验
if (file.isEmpty()) {
throw new DetectException("上传的图片文件为空");
}
// 2. 读取图片为Mat
Mat srcMat = null;
try {
srcMat = opencv_imgcodecs.imdecode(new Mat(file.getBytes()), opencv_imgcodecs.IMREAD_COLOR);
if (srcMat.empty()) {
throw new DetectException("图片解码失败,请检查文件格式");
}
// 3. 提交线程池推理(异步,避免阻塞主线程)
return doDetect(srcMat);
} catch (Exception e) {
log.error("文件检测失败", e);
throw new DetectException("文件检测失败:" + e.getMessage());
} finally {
// 释放Mat,避免内存泄漏
if (srcMat != null) {
srcMat.release();
}
}
}
@Override
public DetectResponse detectByBase64(String base64Str) {
// 1. 参数校验
if (base64Str == null || base64Str.isEmpty()) {
throw new DetectException("Base64字符串为空");
}
// 2. Base64转字节数组
byte[] imageBytes = Base64Util.decode(base64Str);
// 3. 解码为Mat
Mat srcMat = opencv_imgcodecs.imdecode(new Mat(imageBytes), opencv_imgcodecs.IMREAD_COLOR);
if (srcMat.empty()) {
throw new DetectException("Base64解码为图片失败");
}
// 4. 执行检测
try {
return doDetect(srcMat);
} catch (Exception e) {
log.error("Base64检测失败", e);
throw new DetectException("Base64检测失败:" + e.getMessage());
} finally {
srcMat.release();
}
}
/**
* 核心推理逻辑(封装复用)
*/
private DetectResponse doDetect(Mat srcMat) {
long detectStart = System.currentTimeMillis();
try {
// 1. 图像预处理
float[][][][] inputTensor = imageUtil.preprocess(srcMat);
float[] flatTensor = imageUtil.flatten(inputTensor);
FloatBuffer inputBuffer = FloatBuffer.wrap(flatTensor);
// 2. 构建ONNX输入
OrtSession.InputTensor input = OrtSession.InputTensor.createTensor(
env,
inputBuffer,
Arrays.asList(1L, 3L, (long) yoloConfig.getInputHeight(), (long) yoloConfig.getInputWidth())
);
// 3. 执行推理(YOLOv8的输入节点名固定为images)
OrtSession.Result result = session.run(Collections.singletonMap("images", input));
float[][][] output = (float[][][]) result.get(0).getValue();
// 4. 解析输出结果
List<DetectResult> detectResults = parseOutput(output, srcMat.cols(), srcMat.rows());
// 5. NMS非极大值抑制(去重)
List<DetectResult> finalResults = nonMaxSuppression(detectResults);
// 6. 构建返回结果
DetectResponse response = new DetectResponse();
response.setCode(200);
response.setMsg("检测成功");
response.setDetectTime(System.currentTimeMillis() - detectStart);
response.setResults(finalResults);
return response;
} catch (OrtException e) {
log.error("推理失败", e);
throw new DetectException("模型推理失败:" + e.getMessage());
}
}
/**
* 解析ONNX输出(核心:坐标反解,适配原始图片尺寸)
*/
private List<DetectResult> parseOutput(float[][][] output, int srcW, int srcH) {
List<DetectResult> results = new ArrayList<>();
String[] classNames = yoloConfig.getClassNames();
int nc = classNames.length;
int elementsPerBox = nc + 5; // 4坐标+1置信度+nc类别
int numBoxes = output[0].length / elementsPerBox;
// 计算缩放和填充偏移(反解坐标用)
float scale = Math.min(
(float) yoloConfig.getInputWidth() / srcW,
(float) yoloConfig.getInputHeight() / srcH
);
float padW = (yoloConfig.getInputWidth() - srcW * scale) / 2;
float padH = (yoloConfig.getInputHeight() - srcH * scale) / 2;
for (int i = 0; i < numBoxes; i++) {
int baseIdx = i * elementsPerBox;
// 1. 过滤低置信度
float conf = output[0][baseIdx + 4];
if (conf < yoloConfig.getConfThreshold()) {
continue;
}
// 2. 找最高置信度的类别
float maxClsConf = 0;
int clsIdx = 0;
for (int c = 0; c < nc; c++) {
float clsConf = output[0][baseIdx + 5 + c];
if (clsConf > maxClsConf) {
maxClsConf = clsConf;
clsIdx = c;
}
}
float finalConf = conf * maxClsConf; // 最终置信度=目标置信度×类别置信度
if (finalConf < yoloConfig.getConfThreshold()) {
continue;
}
// 3. 反解坐标(YOLOv8的坐标是中心点+宽高,需转成左上角+右下角)
float cx = output[0][baseIdx];
float cy = output[0][baseIdx + 1];
float w = output[0][baseIdx + 2];
float h = output[0][baseIdx + 3];
// 转换为原始图片坐标
float x1 = (cx - padW - w / 2) / scale;
float y1 = (cy - padH - h / 2) / scale;
float x2 = (cx - padW + w / 2) / scale;
float y2 = (cy - padH + h / 2) / scale;
// 4. 构建检测结果
DetectResult result = new DetectResult();
result.setClassName(classNames[clsIdx]);
result.setConfidence(Math.round(finalConf * 10000) / 10000f); // 保留4位小数
result.setX1(Math.max(0, (int) x1));
result.setY1(Math.max(0, (int) y1));
result.setX2(Math.min(srcW - 1, (int) x2));
result.setY2(Math.min(srcH - 1, (int) y2));
results.add(result);
}
return results;
}
/**
* NMS非极大值抑制(去重重叠框,上线必备)
*/
private List<DetectResult> nonMaxSuppression(List<DetectResult> results) {
if (results.isEmpty()) {
return new ArrayList<>();
}
// 1. 按置信度降序排序
results.sort((a, b) -> Float.compare(b.getConfidence(), a.getConfidence()));
boolean[] suppressed = new boolean[results.size()];
List<DetectResult> finalResults = new ArrayList<>();
// 2. 遍历所有框,抑制重叠框
for (int i = 0; i < results.size(); i++) {
if (suppressed[i]) {
continue;
}
DetectResult current = results.get(i);
finalResults.add(current);
// 抑制当前框的重叠框
for (int j = i + 1; j < results.size(); j++) {
if (suppressed[j]) {
continue;
}
DetectResult other = results.get(j);
if (calculateIOU(current, other) > yoloConfig.getIouThreshold()) {
suppressed[j] = true;
}
}
}
return finalResults;
}
/**
* 计算IOU(交并比)
*/
private float calculateIOU(DetectResult a, DetectResult b) {
int interX1 = Math.max(a.getX1(), b.getX1());
int interY1 = Math.max(a.getY1(), b.getY1());
int interX2 = Math.min(a.getX2(), b.getX2());
int interY2 = Math.min(a.getY2(), b.getY2());
// 无重叠
if (interX1 >= interX2 || interY1 >= interY2) {
return 0f;
}
// 计算交集和并集
int interArea = (interX2 - interX1) * (interY2 - interY1);
int aArea = (a.getX2() - a.getX1()) * (a.getY2() - a.getY1());
int bArea = (b.getX2() - b.getX1()) * (b.getY2() - b.getY1());
return (float) interArea / (aArea + bArea - interArea);
}
/**
* 服务销毁时释放模型资源(上线必备,避免内存泄漏)
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
if (session != null) {
try {
session.close();
env.close();
log.info("YOLOv8模型资源已释放");
} catch (OrtException e) {
log.error("模型资源释放失败", e);
}
}
}
}
4. 接口层:标准化API,适配前端调用
dto/DetectResponse.java(统一返回格式)
package com.yolov8.detect.controller.dto;
import lombok.Data;
import java.util.List;
/**
* 统一检测返回结果(上线标准:固定格式,易解析)
*/
@Data
public class DetectResponse {
// 状态码(200成功,其他失败)
private int code;
// 提示信息
private String msg;
// 检测耗时(ms)
private long detectTime;
// 检测结果列表
private List<DetectResult> results;
}
dto/DetectResult.java(检测结果明细)
package com.yolov8.detect.controller.dto;
import lombok.Data;
/**
* 单条检测结果
*/
@Data
public class DetectResult {
// 类别名称
private String className;
// 置信度
private float confidence;
// 坐标(原始图片)
private int x1;
private int y1;
private int x2;
private int y2;
}
DetectController.java(接口实现)
package com.yolov8.detect.controller;
import com.yolov8.detect.controller.dto.DetectResponse;
import com.yolov8.detect.service.YoloDetectService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
/**
* 目标检测接口(标准化,支持文件/Base64)
*/
@RestController
@RequestMapping("/api/detect")
@Slf4j
public class DetectController {
@Resource
private YoloDetectService yoloDetectService;
/**
* 图片文件上传检测接口
*/
@PostMapping("/file")
public DetectResponse detectByFile(@RequestParam("file") MultipartFile file) {
log.info("接收文件检测请求,文件名:{},大小:{}KB", file.getOriginalFilename(), file.getSize() / 1024);
return yoloDetectService.detectByFile(file);
}
/**
* Base64字符串检测接口
*/
@PostMapping("/base64")
public DetectResponse detectByBase64(@RequestParam("base64") String base64Str) {
log.info("接收Base64检测请求,字符串长度:{}", base64Str.length());
return yoloDetectService.detectByBase64(base64Str);
}
/**
* 健康检查接口(上线必备,用于监控)
*/
@GetMapping("/health")
public String healthCheck() {
return "ok";
}
}
5. 全局异常处理(上线必备,避免接口返回500)
package com.yolov8.detect.util;
import com.yolov8.detect.controller.dto.DetectResponse;
import com.yolov8.detect.exception.DetectException;
import com.yolov8.detect.exception.ModelLoadException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器(上线必备,统一返回格式)
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理检测相关异常
*/
@ExceptionHandler(DetectException.class)
public DetectResponse handleDetectException(DetectException e) {
log.error("检测异常", e);
DetectResponse response = new DetectResponse();
response.setCode(400);
response.setMsg(e.getMessage());
response.setDetectTime(0);
return response;
}
/**
* 处理模型加载异常
*/
@ExceptionHandler(ModelLoadException.class)
public DetectResponse handleModelLoadException(ModelLoadException e) {
log.error("模型加载异常", e);
DetectResponse response = new DetectResponse();
response.setCode(500);
response.setMsg(e.getMessage());
response.setDetectTime(0);
return response;
}
/**
* 处理所有未捕获的异常
*/
@ExceptionHandler(Exception.class)
public DetectResponse handleException(Exception e) {
log.error("系统异常", e);
DetectResponse response = new DetectResponse();
response.setCode(500);
response.setMsg("系统内部异常,请联系管理员");
response.setDetectTime(0);
return response;
}
}
六、第四步:配置文件与上线优化
1. application.yml(可配置化,适配不同环境)
# 服务端口
server:
port: 8080
servlet:
context-path: /yolov8-detect
# YOLOv8核心配置
yolo:
model:
path: classpath:model/best.onnx # 模型路径
input:
width: 640
height: 640
conf:
threshold: 0.6 # 置信度阈值
iou:
threshold: 0.45 # IOU阈值
class:
names: crack,missing,deformation # 检测类别
# 日志配置(上线建议输出到文件)
logging:
level:
com.yolov8.detect: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
file:
name: logs/yolov8-detect.log
max-size: 100MB
max-history: 7
# SpringBoot配置
spring:
servlet:
multipart:
max-file-size: 10MB # 最大文件大小
max-request-size: 10MB
main:
allow-circular-references: true
2. 上线关键优化(必做)
-
接口限流:用Guava的RateLimiter给接口加限流(比如QPS=10),避免高并发压垮服务;
-
模型预热:服务启动后主动调用一次检测接口,预热模型,避免首次调用耗时过长;
-
内存监控:添加内存监控,当内存占用≥80%时触发GC,避免边缘设备OOM;
-
打包优化:用
spring-boot:repackage打包成可执行jar,排除冗余依赖,减小包体积; -
部署脚本:编写启动/停止脚本,添加自启动、异常重启(参考工业部署的systemd脚本)。
七、第五步:部署上线与测试
1. 打包部署
# 打包(跳过测试,加快速度)
mvn clean package -Dmaven.test.skip=true
# 上传jar包到服务器,执行启动命令(后台运行,输出日志)
nohup java -jar yolov8-detect-api-1.0.0.jar --spring.profiles.active=prod > nohup.log 2>&1 &
2. 接口测试(Postman/Curl)
文件上传测试
curl -X POST http://localhost:8080/yolov8-detect/api/detect/file \
-F "file=@test.jpg" \
-H "Content-Type: multipart/form-data"
返回结果示例(结构化,易解析)
{
"code": 200,
"msg": "检测成功",
"detectTime": 120,
"results": [
{
"className": "crack",
"confidence": 0.9876,
"x1": 120,
"y1": 80,
"x2": 180,
"y2": 140
}
]
}
八、踩坑总结(实战经验,掘金用户最爱)
-
模型导出参数错误:动态维度、opset版本不对会导致Java加载模型失败,务必固定imgsz、opset≥17;
-
内存泄漏:Mat对象未release、模型未关闭会导致内存持续上涨,必须在finally块释放资源;
-
坐标反解错误:忘记计算padW/padH会导致检测框偏移,核心是还原原始图片尺寸;
-
首次调用耗时高:模型懒加载导致首次调用≥500ms,服务启动后预热模型即可解决;
-
跨平台依赖问题:ONNX Runtime的Linux/Windows依赖不同,用os-maven-plugin自动适配。
九、总结
这篇文章从工程化角度实现了SpringBoot集成YOLOv8的全流程,最终的接口具备“可上线”的核心特性:工程化分层、参数可配置、异常兜底、性能优化、资源可控。相比demo级代码,这套方案解决了工业场景的核心痛点——稳定性、可维护性、性能可控,完全可以直接移植到工业质检、智慧安防等实际项目中。
后续可以扩展的方向:GPU加速、多版本YOLO兼容、视频流实时检测、分布式推理,这些都会在后续的文章中分享。