
在 Web 开发中,视频文件上传与存储是常见需求,如教育平台的课程视频、社交平台的用户分享视频等。本文将讲解如何使用 Java SpringBoot 构建后端接口,配合 Vue 前端框架实现视频文件的上传、进度展示与服务器存储功能。
一、技术栈选型
在开始开发前,先明确本次使用的技术栈,确保前后端技术适配:
- 后端:Java 11+、SpringBoot 2.7.x、Spring MVC(处理请求)、Commons FileUpload(文件解析)、Lombok(简化代码)
- 前端:Vue 3(组合式 API)、Vite(构建工具)、Axios(发送 HTTP 请求)、Element Plus(UI 组件,用于上传组件和进度条)
- 存储方式:本次采用服务器本地存储(后续可扩展为 MinIO、OSS 等云存储)
- 开发工具:IntelliJ IDEA(后端)、Visual Studio Code(前端)、Postman(接口测试)
二、后端开发:SpringBoot 接口实现
后端核心任务是接收前端传来的视频文件,进行合法性校验(大小、格式),然后保存到服务器指定目录,并返回上传结果。
2.1 初始化 SpringBoot 项目
- 打开 IntelliJ IDEA,通过「Spring Initializr」创建新项目,选择以下依赖:
- Spring Web(提供 HTTP 接口能力)
- Lombok(减少模板代码)
- Spring Boot DevTools(热部署,提高开发效率)
- 项目结构规划
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| com.example.videoupload ├── config # 配置类(如文件上传配置) ├── controller # 控制器(接收前端请求) ├── exception # 自定义异常(如文件过大、格式错误) ├── service # 业务逻辑层(文件处理) ├── util # 工具类(如文件路径处理) └── VideoUploadApplication.java # 启动类 |
2.2 核心配置:文件上传参数配置
创建config/FileUploadConfig.java,配置文件上传的最大大小、临时目录等参数(默认 SpringBoot 文件上传大小限制较小,需手动调整):
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| package com.example.videoupload.config; import org.springframework.boot.web.servlet.MultipartConfigFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.unit.DataSize; import javax.servlet.MultipartConfigElement; @Configuration public class FileUploadConfig { @Bean public MultipartConfigElement multipartConfigElement() { MultipartConfigFactory factory = new MultipartConfigFactory(); // 1. 设置单个文件最大大小(此处为500MB,可根据需求调整) factory.setMaxFileSize(DataSize.ofMegabytes(500)); // 2. 设置一次请求所有文件的总大小(此处为1GB) factory.setMaxRequestSize(DataSize.ofGigabytes(1)); // 3. 设置临时文件存储目录(默认在系统临时目录,也可自定义) // factory.setLocation("D:/temp/video-upload"); return factory.createMultipartConfig(); } } |
2.3 自定义异常:处理文件上传错误
创建exception/VideoUploadException.java,用于统一处理文件上传中的异常(如格式不支持、文件过大):
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| package com.example.videoupload.exception; import lombok.Getter; @Getter public class VideoUploadException extends RuntimeException { // 错误码(可用于前端区分错误类型) private final Integer code; public VideoUploadException(String message, Integer code) { super(message); this.code = code; } // 常见异常静态方法(简化调用) public static VideoUploadException fileTooLarge() { return new VideoUploadException("视频文件过大,最大支持500MB", 413); } public static VideoUploadException unsupportedFormat() { return new VideoUploadException("不支持的视频格式,仅支持MP4、AVI、MOV", 415); } } |
再创建exception/GlobalExceptionHandler.java,全局捕获异常并返回统一格式:
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| package com.example.videoupload.exception; import lombok.Data; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { // 捕获自定义的视频上传异常 @ExceptionHandler(VideoUploadException.class) public Result handleVideoUploadException(VideoUploadException e) { return new Result(e.getCode(), e.getMessage(), null); } // 捕获SpringBoot默认的文件过大异常(防止漏捕) @ExceptionHandler(org.springframework.web.multipart.MaxUploadSizeExceededException.class) @ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE) public Result handleMaxSizeException() { return new Result(413, "视频文件过大,最大支持500MB", null); } // 统一返回格式封装 @Data public static class Result { private Integer code; private String message; private Object data; public Result(Integer code, String message, Object data) { this.code = code; this.message = message; this.data = data; } } } |
2.4 业务逻辑:文件处理服务
创建service/VideoUploadService.java,封装视频文件的校验、保存逻辑:
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| package com.example.videoupload.service; import com.example.videoupload.exception.VideoUploadException; import com.example.videoupload.util.FileUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.List; @Service @Slf4j public class VideoUploadService { // 从配置文件读取视频存储根路径(建议在application.properties中配置) @Value("${video.upload.base-path}") private String basePath; // 支持的视频格式列表 private final List<String> SUPPORTED_FORMATS = Arrays.asList("mp4", "avi", "mov"); /** * 视频文件上传核心方法 * @param file 前端传来的MultipartFile对象 * @return 保存后的文件访问路径(如:/videos/20251031/abc123.mp4) */ public String uploadVideo(MultipartFile file) { // 1. 校验文件是否为空 if (file.isEmpty()) { throw new VideoUploadException("上传的视频文件为空", 400); } // 2. 校验文件格式 String originalFilename = file.getOriginalFilename(); String fileSuffix = FileUtil.getFileSuffix(originalFilename); // 自定义工具类,获取文件后缀 if (!SUPPORTED_FORMATS.contains(fileSuffix.toLowerCase())) { throw VideoUploadException.unsupportedFormat(); } // 3. 生成唯一文件名(避免重复覆盖,使用UUID+后缀) String uniqueFileName = FileUtil.generateUniqueFileName(fileSuffix); // 4. 构建文件存储路径(按日期分目录,如:basePath/20251031/uniqueFileName) String dateDir = FileUtil.getDateDir(); // 格式:yyyyMMdd String saveDirPath = basePath + File.separator + dateDir; File saveDir = new File(saveDirPath); // 5. 若目录不存在,创建目录 if (!saveDir.exists()) { boolean mkdirs = saveDir.mkdirs(); if (!mkdirs) { log.error("创建视频存储目录失败:{}", saveDirPath); throw new VideoUploadException("服务器存储目录创建失败", 500); } } // 6. 保存文件到指定路径 File saveFile = new File(saveDir, uniqueFileName); try { file.transferTo(saveFile); // 核心方法:将MultipartFile写入本地文件 log.info("视频文件上传成功,保存路径:{}", saveFile.getAbsolutePath()); // 7. 返回文件访问路径(后续可配置静态资源映射,让前端通过URL访问) return "/videos/" + dateDir + "/" + uniqueFileName; } catch (IOException e) { log.error("视频文件保存失败", e); throw new VideoUploadException("服务器保存文件失败", 500); } } } |
2.5 工具类:文件路径与名称处理
创建util/FileUtil.java,封装常用的文件操作工具方法:
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| package com.example.videoupload.util; import java.text.SimpleDateFormat; import java.util.Date; import java.util.UUID; public class FileUtil { /** * 获取文件后缀(如:"test.mp4" -> "mp4") */ public static String getFileSuffix(String originalFilename) { if (originalFilename == null || !originalFilename.contains(".")) { return ""; } return originalFilename.substring(originalFilename.lastIndexOf(".") + 1); } /** * 生成唯一文件名(UUID + 后缀) */ public static String generateUniqueFileName(String suffix) { String uuid = UUID.randomUUID().toString().replace("-", ""); return suffix.isEmpty() ? uuid : uuid + "." + suffix; } /** * 获取日期目录(格式:yyyyMMdd) */ public static String getDateDir() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); return sdf.format(new Date()); } } |
2.6 控制器:接收前端请求
创建controller/VideoUploadController.java,提供 POST 接口供前端调用:
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| package com.example.videoupload.controller; import com.example.videoupload.exception.GlobalExceptionHandler; import com.example.videoupload.service.VideoUploadService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api/video") @RequiredArgsConstructor // Lombok注解,自动注入依赖 public class VideoUploadController { private final VideoUploadService videoUploadService; /** * 视频上传接口 * @param file 前端传来的视频文件(参数名需与前端一致) * @return 统一返回格式(包含访问路径) */ @PostMapping("/upload") public GlobalExceptionHandler.Result upload(@RequestParam("videoFile") MultipartFile file) { String accessPath = videoUploadService.uploadVideo(file); return new GlobalExceptionHandler.Result(200, "视频上传成功", accessPath); } } |
2.7 配置静态资源映射与基础路径
在application.properties中添加配置,指定视频存储路径,并配置静态资源映射(让前端能通过 URL 直接访问上传的视频):
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| # 视频存储根路径(Windows用D:/,Linux用/opt/) video.upload.base-path=D:/video-storage # 静态资源映射:将URL中的"/videos/**"映射到本地的视频存储路径 spring.web.resources.static-locations=file:${video.upload.base-path}/,classpath:/static/ spring.mvc.static-path-pattern=/videos/** |
2.8 后端测试:用 Postman 验证接口
此时后端接口已开发完成,可通过 Postman 测试:
- 打开 Postman,创建 POST 请求,URL 为http://localhost:8080/api/video/upload
- 在「Body」中选择「form-data」,Key 填写videoFile(与 Controller 参数名一致),Value 选择一个 MP4 视频文件
- 点击发送,若返回以下结果,则说明接口正常:
|-------------------------------------------------------------------------------------------------------|
| { "code": 200, "message": "视频上传成功", "data": "/videos/20251031/123e4567e89b12d3a456426614174000.mp4" } |
- 同时查看本地D:/video-storage/20251031/目录,会发现视频文件已保存成功。
(配图 1:Postman 测试视频上传接口的截图,标注请求方式、URL、参数、返回结果)
三、前端开发:Vue 上传页面实现
前端核心任务是创建上传表单,支持视频文件选择、上传进度展示、上传结果提示,并调用后端接口完成文件提交。
3.1 初始化 Vue 项目
- 打开 Visual Studio Code,通过终端执行以下命令创建 Vue 3 项目:
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| # 安装Vite(若未安装) npm install -g create-vite # 创建项目(项目名:video-upload-frontend) create-vite video-upload-frontend --template vue # 进入项目目录 cd video-upload-frontend # 安装依赖 npm install # 安装Element Plus和Axios npm install element-plus @element-plus/icons-vue axios |
- 项目结构规划(核心文件)
|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| src/ ├── components/ # 组件(上传组件) │ └── VideoUpload.vue # 视频上传组件 ├── utils/ # 工具类(Axios配置) │ └── request.js # Axios请求封装 ├── App.vue # 根组件 └── main.js # 入口文件 |
3.2 Axios 配置:请求封装
创建src/utils/request.js,封装 Axios 请求,处理请求拦截、响应拦截、错误提示:
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| import axios from 'axios'; import { ElMessage, ElLoading } from 'element-plus'; // 创建Axios实例 const request = axios.create({ baseURL: 'http://localhost:8080/api', // 后端接口基础路径 timeout: 600000, // 超时时间(10分钟,适配大视频上传) headers: { 'Content-Type': 'multipart/form-data' // 视频上传需用form-data格式 } }); // 请求拦截器:添加loading(可选) let loadingInstance; request.interceptors.request.use( (config) => { // 显示loading(上传大文件时提示用户) loadingInstance = ElLoading.service({ text: '视频上传中,请稍候...', background: 'rgba(0, 0, 0, 0.5)' }); return config; }, (error) => { loadingInstance.close(); ElMessage.error('请求发送失败,请重试'); return Promise.reject(error); } ); // 响应拦截器:处理返回结果 request.interceptors.response.use( (response) => { loadingInstance.close(); const res = response.data; // 若返回码不是200,提示错误 if (res.code !== 200) { ElMessage.error(res.message || '操作失败'); return Promise.reject(new Error(res.message || 'Error')); } else { ElMessage.success(res.message || '操作成功'); return res.data; // 返回数据部分(如访问路径) } }, (error) => { loadingInstance.close(); // 处理413(文件过大)、415(格式错误)等状态码 const status = error.response?.status; if (status === 413) { ElMessage.error('视频文件过大,最大支持500MB'); } else if (status === 415) { ElMessage.error('不支持的视频格式,仅支持MP4、AVI、MOV'); } else { ElMessage.error('服务器异常,请联系管理员'); } return Promise.reject(error); } ); export default request; |
3.3 视频上传组件:核心功能实现
创建src/components/VideoUpload.vue,使用 Element Plus 的Upload组件实现文件选择、进度条展示,并调用封装好的 Axios 请求:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <template> <div class="video-upload-container"> <h2>视频文件上传</h2> <!-- Element Plus Upload组件 --> <el-upload class="upload-demo" action="#" <!-- 此处action设为空,通过http-request自定义请求 --> :http-request="handleUpload" <!-- 自定义上传方法 --> :file-list</doubaocanvas> |