做了好几个类似Spring Boot的视频网站项目之后,我觉得可以把我复盘出来的东西认真写一写。这个"基于Spring Boot的视频播放网站"算不上什么特别新奇的项目,但如果你真打算动手做——不管是为了毕设、个人作品集,还是接一个小型商业项目——这里面从技术选型到部署上线的坑,比想象中要多得多。尤其是当你把前端、视频处理、权限控制这些模块串在一起,Spring Boot本身那些看似简单的配置,往往会变成最花时间的部分。
这篇文章我按我实际打过的项目来拆,从整体设计思路、核心表结构、上传与转码流程,到Vue打包后怎么塞进Spring Boot部署、哪些配置容易踩雷,全部会聊到。
1. 内容整体设计与思路拆解
1.1 先想清楚你要做的是"网站"还是"平台"
拿到"基于Spring Boot的视频播放网站"这个需求时,第一步不是急着建工程,而是要先界定边界。视频播放网站往小说只是一个能上传视频、播放视频的Web应用;往大了说,它会牵扯到视频转码、弹幕、评论、会员权限、后台管理、流量统计等一系列模块。
我一般会根据项目目标把它拆成三个版本:
- 基础版:用户注册登录、视频上传、视频列表、视频播放(HTML5播放器直接拉静态视频文件)、后台管理(上架下架、分类管理)。
- 进阶版:视频分片上传与断点续传、FFmpeg转码、视频封面生成、用户收藏点赞评论、浏览记录。
- 完整版:会员体系与付费视频、弹幕系统、推荐系统、视频水印、后台数据看板、权限体系细粒度控制。
标题里只说了"基于Spring Boot的视频播放网站",但当你真正上手时,至少要到进阶版才算是"网站"而不是"静态视频列表页"。因为如果不做转码和分片,直接拿MP4在浏览器里播,移动端兼容性和加载速度会让你崩溃。
1.2 技术选型为什么是这套组合
Spring Boot背后的Spring生态成熟稳定,社区活跃度高,整合第三方组件极其方便。我实际使用中选型如下:
| 模块 | 选型 | 原因 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 稳定,兼容性比3.x好,对JDK8/11友好 |
| 持久层 | MyBatis-Plus | 省去大量单表CRUD代码,分页插件好用 |
| 权限认证 | Spring Security + JWT | 无状态认证,适合前后端分离 |
| 数据库 | MySQL 8.x | 主流,事务支持好 |
| 缓存 | Redis | 用于验证码、热门榜单、播放次数缓存 |
| 视频存储 | MinIO(本地私有化)或阿里云OSS | 本地环境用MinIO,公网项目用OSS |
| 视频处理 | FFmpeg | 转码、截图、水印全靠它 |
| 前端 | Vue 2 / Vue 3 + Element UI | 打包后由Spring Boot托管静态资源,部署简单 |
这里有个值得注意的取舍:为什么不选Spring Boot 3.x?虽然3.x出来很久了,但它基于Jakarta EE,部分老版本依赖(比如某些MyBatis-Plus插件、非官方starter)会出现兼容性问题。如果你的项目有现成的旧业务代码要迁移,或者使用了一些不太活跃的第三方库,2.7.x反而是最稳的。如果是全新项目且团队成员习惯新生态,那3.x也没毛病,只是要做好依赖适配的心理准备。
1.3 项目结构怎么划分才不混乱
Spring Boot项目结构虽然网上说法很多,但我实际打完几个项目后觉得,按业务模块分包比按技术类型分包更顺畅。下面这个结构是我比较常用的:
com.example.videoplayer ├── common # 通用工具、返回结果封装、异常处理 │ ├── Result.java │ ├── ResultCode.java │ └── GlobalExceptionHandler.java ├── config # 配置类(WebMvc、Redis、MinIO、拦截器) ├── controller # 接口层(按业务拆Controller) ├── service # 业务层 │ └── impl ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体 ├── dto # 请求/响应对象(避免直接用实体暴露给前端) ├── utils # JWT工具、MD5工具、FFmpeg命令工具 └── task # 定时任务(清理临时文件、刷新缓存等)按业务模块化背后的逻辑是,视频播放网站的功能会持续叠加,今天加弹幕、明天加推荐,如果所有Controller堆在一个包下,后期维护成本剧增。从第一天起就按业务拆包,比之后重构省心十倍。
2. 核心表结构设计与权限模型
2.1 画清楚六张核心表
视频播放网站的表设计不复杂,但关系到核心体验。我用了六张表撑起主业务:
| 表名 | 核心字段 | 用途 |
|---|---|---|
| user | id, username, password, avatar, role, vip_expire | 用户信息与角色 |
| video | id, title, description, cover_url, video_url, status, category_id, uploader_id, play_count, created_at | 视频主表 |
| video_category | id, name, sort | 视频分类 |
| comment | id, video_id, user_id, content, parent_id, created_at | 评论(支持层级) |
| danmu | id, video_id, user_id, content, time_point | 弹幕(记录视频时间点) |
| play_record | id, user_id, video_id, progress, updated_at | 播放进度记录 |
视频表里有个字段经常被忽略——status。它不能只做简单的"上架/下架",我习惯定义成一组状态:
- 0:转码中(上传后正在后台处理)
- 1:待审核(转码完成,等待管理员审核)
- 2:已发布(审核通过,可正常播放)
- 3:已下架(违规或管理员手动下线)
这样设计的好处是,前端可以明确区分提示文案:"视频正在转码中请稍后""视频正在审核中""视频已下线",而不是笼统地显示"播放失败"。状态机的价值不在于多复杂,而在于让用户在任何情况下都知道发生了什么。
2.2 用户角色跟视频权限怎么拆分
很多初学者会把权限简单的做成"普通用户"和"管理员"两个角色。但如果要做付费视频、VIP会员视频,这个模型就不够用了。
我实际使用的是角色(Role) + 资源状态双重校验:
- 用户表里的
role字段:普通用户、管理员、超管。 - 视频表里的
vip_only字段:标记该视频是否需要VIP。 - 用户表里的
vip_expire字段:记录VIP到期时间。
在后端校验的时候,不能只判断"是不是VIP",而是要判断"VIP是否在有效期内"。这里我写过一个经典bug:用户VIP过期后,因为之前生成的JWT里写了vip=true,导致过期后仍能播放VIP视频。后来改成每次请求都查库校验vip_expire时间,问题才解决。JWT里的信息永远只能做展示,不能做权限判断的唯一依据。
权限拦截器的核心思路是这样的:
@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 从请求头取token // 2. 解析token,能解析出来说明登录过 // 3. 判断请求的接口是否需要VIP权限,需要就校验vip_expire // 4. 校验通过放行,否则返回402/403 return true; } }2.3 数据库层面的防刷设计
做视频网站一定会遇到刷播放量的问题。刚开始我的做法是每次播放请求直接video.play_count + 1,结果一个用户反复刷新页面,播放量就涨得离谱。后面改成:同一个用户对同一个视频,Redis里存一个24小时有效的标识,有过就不计数,没有才加1。这样避免频繁操作MySQL,Redis自身的原子性也防住了并发下的计数错误。
3. 视频上传、转码与播放链路
3.1 为什么上传必须做分片
直接往服务器上传一个几百MB的MP4,表面看没什么问题,实际上一旦网络波动,TCP连接断开,整个文件就要重传。这对用户体验是毁灭性的。
所以我的上传流程是:
- 前端计算文件MD5(通过Web Worker,不卡UI)。
- 后端检查MD5对应的文件是否已存在(秒传判断)。
- 后端生成本次上传的uploadId,前端按固定大小(如5MB)把文件切成多个分片。
- 每个分片独立上传,后端收到后写入临时目录,并记录分片索引。
- 所有分片传完,前端主动请求"合并分片"接口。
- 后端按索引顺序拼接分片,生成完整文件,再扔进视频处理队列。
分片大小需要根据实际网络环境调整。个人项目部署在普通服务器上,5MB一个分片比较合适;如果在内网环境,可以调到10MB甚至20MB;如果用户可能在弱网环境下使用,2MB更稳。没有绝对的标准,但建议设成可配置的项,而不是写死在代码里。
3.2 FFmpeg转码:不转码之前一切都是白干
如果你仅仅把MP4上传后直接给前端播放,很快会遇到几个问题:
- 浏览器对视频编码格式支持不一致(尤其是Safari对某些MP4的H.264编码没问题,但对WebM就可能无法播放)。
- 视频文件体积大,加载缓慢,拖拽进度条卡顿。
- iPhone上部分视频无法正常播放。
解决这些问题的标准方案就是转码为HLS协议输出。用FFmpeg把MP4转成m3u8索引文件和一个个.ts分片,前端使用hls.js播放。这样视频首屏加载快、拖动进度条流畅,兼容性也最强。
实际用到的核心命令大概是这样的:
ffmpeg -i input.mp4 -profile:v baseline -level 3.0 -start_number 0 \ -hls_time 10 -hls_list_size 0 -f hls output.m3u8解释下几个参数:
-profile:v baseline:使用H.264的baseline级别,兼容性最好,老设备也能解。-hls_time 10:每个ts分片时长10秒,个人网站建议10-15秒,太短会导致请求过多。-hls_list_size 0:表示保留所有分片在m3u8列表中,否则默认只保留最近几个分片。
转码是非常耗CPU的操作。在2核4G的小服务器上,一个30分钟的视频可能转码要跑5-10分钟。所以绝不能在前台接口里同步执行转码,必须丢到后台异步处理。我用的是最简单的方案:上传完成后,把转码任务信息扔进数据库任务表,然后用Spring Boot的@Async注解异步执行,另一台机器或同一台机器的后台线程池去消费。
3.3 视频播放鉴权怎么做才不卡顿
视频直链如果不做鉴权,别人拿到URL就能随意播放、下载甚至盗链。但如果你每次播放都先请求后端接口拿一个临时播放地址,再交给播放器,又会增加一次跳转延迟。
我的方案是URL签名防盗链:
- 视频上传后,保存原始路径,不直接暴露。
- 前端请求
/api/video/getPlayUrl?id=xx,后端校验用户权限(是否登录、是否VIP),校验通过后用HMAC算法生成一个带过期时间的签名URL。 - 播放器拿这个签名URL去请求视频,后端解析签名,过期或无效则拒绝。
签名URL的格式类似这样:
/video/2025/04/01/xxxx.mp4?sign=md5(uuid+expireTime+密钥)&expire=1714560000这样即使URL被别人抓包拿到,过期后也会失效。签名密钥一定不能放在前端代码里,只放在后端配置中。
4. Spring Boot项目实战难点记录
4.1 Spring Boot自动装配原理在项目中的实际引用
很多初学者在网上刷到过"Spring Boot自动装配原理"的面试题,但在实际项目中,什么时候真正用到这个知识?我举一个我项目里遇到的例子。
我想给项目加一个统一的日志切面,用来记录每个接口的耗时和异常。刚开始我在pom.xml里引入spring-boot-starter-aop,然后写一个@Aspect类。但系统里有个第三方SDK,它自己带了一个旧版本的AOP相关依赖,启动时直接报了NoSuchMethodError。
排查的路径就是围绕自动装配:Spring Boot在spring.factories或AutoConfiguration.imports中注册了大量自动配置类,它们通过@ConditionalOnClass、@ConditionalOnMissingBean等条件决定是否生效。当项目里同时存在两个版本的AOP库时,自动配置会依据类路径上的实际类来决定装配什么,这时版本冲突就会导致装配出错误的结果。
后来我用mvn dependency:tree查清楚冲突来源,在pom.xml里用exclusion排除掉第三方SDK传递的旧依赖,问题才解决。所谓"熟悉自动装配原理",在实战中就是遇到启动报错或Bean找不到时,能快速从自动配置的角度定位到类路径冲突、条件不满足之类的根因。问我为什么推荐spring-boot-starter-*统一管理版本?因为很多奇怪的无厘头报错,最后查下来都是依赖版本不一致。
4.2 Vue打包放进Spring Boot的具体操作
这是热搜里一个非常高频率的关键词:"vue打包放进springboot中"。很多人在开发环境前后端分离跑得很爽,但部署线上时不想单独整一台Nginx,想直接让Spring Boot把前端静态文件也托管了。
可以,而且很简单,但要注意几个坑。
首先在Vue项目里需要调整打包配置。以Vue 2为例,修改vue.config.js:
module.exports = { publicPath: './', // 关键:用相对路径,否则部署到Spring Boot子路径下会白屏 outputDir: 'dist', assetsDir: 'static', productionSourceMap: false }publicPath如果默认是/,打包出来的JS/CSS路径会以根路径开始,而Spring Boot托管时资源路径通常在/static/或直接用/,一旦路径对不上,最典型的表现就是页面白屏,控制台报404找不到js。
然后,在Spring Boot中把打包好的dist目录放到src/main/resources/static里(或者打到Jar外的指定目录),同时为了支持Vue Router的history模式(非hash模式),需要配置一个路径转发规则:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { // 将非接口路径的请求都转发到index.html,交给前端路由处理 registry.addViewController("/").setViewName("forward:/index.html"); registry.addViewController("/{path:[^\\.]*}").setViewName("forward:/index.html"); } }这个配置的意思是,所有不包含点的路径(即不是静态资源文件的请求)都转发到index.html。否则你在前端路由/video/detail刷新一下,Spring Boot会返回404。
4.3 Spring Boot版本太高导致的依赖问题
搜索词里"springboot版本太高"能成为热搜,说明这事踩坑的人真不少。Spring Boot 2.7升3.x最典型的问题是javax.*变jakarta.*。比如:
- 旧代码
import javax.servlet.http.HttpServletRequest在Spring Boot 3里直接编译不过,得改成jakarta.servlet.http.HttpServletRequest。 - 很多第三方库如果不适配Jakarta,在Spring Boot 3下根本启动不了。
另外有些同学喜欢用最新版Spring Boot(比如3.3.x),然后去搜索资料,网上的教程还停留在2.x时代,照抄之后各种报错。我的建议是:如果你是为了快速做项目而不是尝鲜,选稳定版本而不是最新版本。我用2.7.x完成了好几个项目,一次都没遇到大坑。当然这有个前提:你不依赖JDK17+的新语法特性。Spring Boot 3.x的新特性当然很好,但团队里的协作成本、第三方库兼容风险都要提前测。
4.4 定时任务与配置管理
视频播放网站里,定时任务用到的场景很多,我举几个实际案例:
- 每天凌晨清理临时上传目录中超过24小时未合并的分片文件。
- 每10分钟同步一次Redis中的播放量到MySQL。
- 每天刷新用户VIP过期状态。
Spring Boot里开启定时任务非常简单,启动类上加上@EnableScheduling,然后在具体方法上加@Scheduled(cron = "...")。但真正线上跑的时候,有几个坑值得注意:
- 单机环境下
@Scheduled默认是单线程执行的。如果任务A跑了很久,任务B会排队等。如果你有多个任务且耗时不一,建议自己配置一个线程池:
@Configuration public class SchedulingConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10)); } }- 定时任务要考虑幂等。比如"同步播放量到MySQL"这个任务,如果你部署了两台实例做负载均衡,每台都会跑一次任务,播放量就double了。解决方案是加分布式锁(Redis setnx)或者干脆让定时任务只在某一台机器上开启(配置开关)。
4.5 Spring Boot配置里的那些不用可惜的细节
最后聊几个Spring Boot配置中的实用细节,属于"书上不讲、实际很香"的类型:
第一个是application.yml里的多环境配置。做视频网站时,我习惯拆成application-dev.yml(本地开发)、application-test.yml(测试服务器)、application-prod.yml(生产环境)。启动时用spring.profiles.active=prod指定即可。这背后的逻辑是,不同环境的数据库地址、Redis地址、OSS密钥都不一样,硬写在一个文件里每次部署都要改,太苦了。
第二个是自定义配置项。MinIO的endpoint、AccessKey、SecretKey,签名URL的密钥等,这些不属于Spring Boot标准配置范畴,但你会需要它们,直接在application.yml里自定义:
myconf: minio: endpoint: http://127.0.0.1:9000 access-key: admin secret-key: admin123 bucket: video然后用@ConfigurationProperties绑定到一个配置类里:
@Data @Component @ConfigurationProperties(prefix = "myconf.minio") public class MinIOProperties { private String endpoint; private String accessKey; private String secretKey; private String bucket; }这样配置也规范化了,代码里不会出现魔数。我一直坚持一个原则:任何和环境相关的值,绝不硬编码在业务代码里。
第三个是全局异常处理。Spring Boot项目里接口报错默认返回的是一堆堆栈信息,对前端极不友好。配置一个@RestControllerAdvice类,统一捕获异常并转成JSON格式返回,前端就能根据code字段统一处理错误弹窗。这事越早做越好,因为项目一大,接口一多,再去拦截就非常费力。
5. 部署上线与性能优化的几点心得
项目写完后,真正上线又有一轮新的磨炼。我当时部署选的是宝塔面板 + Docker。原本纠结要不要用K8s,后来想清楚了,个人项目或中小型项目用Docker Compose管理几个容器就够了,K8s的运维成本对一个小团队来说是负担而不是加分项。
部署清单大致是这样的:
- 用Docker跑MySQL和Redis,数据目录挂载到宿主机,避免容器删除后数据丢失。
- 写Dockerfile打包Spring Boot应用,注意Docker镜像使用JDK基础镜像,Jar包直接COPY进去。
- MinIO单独用Docker跑,映射端口9000(API)和9001(控制台)。
- Vue打完包后要么塞进Spring Boot的Jar里,要么单独用Nginx托管,两者都可以。如果视频量大,建议还是单独跑Nginx放静态资源,把Spring Boot纯粹当后端API服务器,这样静态资源和后端接口都互不干扰。
性能优化这部分,我总结了几条实在的:
- 视频播放的核心瓶颈在带宽和视频编码,不在框架。你需要关注的是视频这个文件被访问时是否走本地磁盘IO,尽量给MinIO走内网地址而非公网。
- 热门视频列表不要每次查数据库,缓存到Redis里,设置5分钟过期即可,能扛住大部分流量。
- 视频转码任务堆积时,不要无限增加线程,要看CPU核数。2核机器建议FFmpeg并发数控制在1-2个,否则系统Load会爆炸。
6. 常见问题与排查技巧实录
我整理了一些做这个项目时高频出现的问题,直接列成速查表方便你排查:
| 问题 | 表现 | 排查思路与解决方案 |
|---|---|---|
| 前端打包后放进Spring Boot白屏 | 控制台报错找不到JS文件 | 检查Vue的publicPath是否设为./;清浏览器缓存 |
| 视频播放器转圈不播放 | m3u8请求404 | 确认视频转码是否完成;检查MinIO/OSS的访问权限;确认签名URL是否过期 |
| 上传大视频超时 | Nginx返回504 | 调整Nginx的client_max_body_size和proxy_read_timeout,上传接口走独立路径不过长超时 |
| VIP视频未登录也播放成功 | JWT里vip字段过期 | 每次播放请求必须查库校验vip_expire,不能只依赖JWT断言 |
| 数据库连接池爆掉 | 报错Connection is not available | 排查有没有连接没关闭;调整HikariCP最大连接数;检查慢SQL是否锁表导致连接被占满 |
| 定时任务重复执行 | 播放量翻倍 | 检查是否多实例部署,加上Redis分布式锁 |
有一个隐蔽的问题要特别提一下:如果前端用了hls.js播放m3u8,而后端Nginx对m3u8和ts文件的Content-Type没配好,浏览器不认也会导致播放失败。需要在Nginx配置里加上:
location ~* \.(m3u8|ts)$ { add_header Cache-Control no-cache; types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } }我当初为了这个"能下载但播不了"的问题折腾了整整一个晚上,最后才发现是Content-Type的问题。
7. 最后再分享两个实操小技巧
第一个是关于FFmpeg转码时的进度监控。由于转码是异步的,用户需要知道"视频处理到百分之多少了"。FFmpeg本身会输出time=字段来表示当前处理时间,我写了一个监听器去解析FFmpeg命令行输出的time参数,再用已处理时间 / 视频总时长算出百分比,存到Redis里。前端轮询接口就能实时展示转码进度,体验会好非常多。
第二个是关于Vue打包后静态资源的版本问题。Spring Boot里静态资源默认有缓存,前端改了JS重新打包扔进去,用户浏览器可能还是旧资源。解决方法是让前端打包时给JS和CSS文件名加上hash值(Vue CLI默认会做),同时后端设置带no-cache的响应头,这样每次部署后用户拿到的都是最新的文件,又不会每次都下载重复资源。
做这个项目最大的感受是:Spring Boot本身的上手难度并不高,真正考验人的是整个视频处理链路和部署细节。你越是对那些"看起来不起眼的配置"较真,线上出的幺蛾子就越少。希望这篇总结能让你在动手做"基于Spring Boot的视频播放网站"时,少走点我走过的弯路。