“基于SSM的在线视频网站”,这个标题一出来,基本就知道是什么段位的项目了——既不是玩具级的纯JSP购物车,也够不着SpringCloud那套微服务大厂方案,它恰好卡在“课程设计、毕业设计、个人练手项目”最经典的那个位置。我见过太多人选这个题目,也带过几个师弟师妹在这套架构上踩坑,说实话,这题目做的事很清晰,但能把它做得完整、做得不露怯,很考验功底。这篇文章我就从一个老开发的角度,把这个SSM在线视频网站从框架选型、数据库设计、代码分层、视频上传与播放,到部署上线的整个链路都拆开讲一遍,顺带把那些坑我都标出来,给正在做类似系统的人当个地图用。
1. 项目架构设计与技术选型思路
1.1 为什么是SSM而不是别的
现在的技术圈,新框架一个接一个,SpringBoot几乎成了Java Web的默认答案,有些人看到SSM就说“老掉牙了”。但如果你是要拿这个项目来学东西、应付毕业答辩、或者作为第一份简历里的项目经历,SSM反而是更值得选的一条路。
原因很简单:SSM这三件套——Spring做容器管理和事务、SpringMVC做请求分发、MyBatis做数据访问——每一层都是显式配置、显式调用的,你能清楚地看见一个HTTP请求从浏览器到数据库再返回浏览器,整个完整路径上是哪些代码在干活。SpringBoot把这一切自动配置好了,用起来省事,但也把关键的机制藏起来了,面试官问“SpringMVC的DispatcherServlet是怎么分发的”、“MyBatis的一级缓存是什么时机失效的”,你反而接不上话。用SSM做一遍,这些知识点是自己一行一行撸过的,这是这个选型能带来的真正红利。
再具体一点选型:Spring 5.x、SpringMVC 5.x、MyBatis 3.5.x,开发工具用IDEA,数据库用MySQL 5.7或8.0,数据库连接池用Druid,构建工具用Maven,服务器用Tomcat 8.5或9。这套组合是一个保守但极其稳定的方案,网上资料多、案例多,遇到问题基本不会卡死。
1.2 功能模块怎么切才科学
在线视频网站看起来复杂,但拆成模块其实有固定的套路,我直接把经过验证的划分方式放出来:
- 用户模块:注册、登录、个人信息查看与修改。登录可以做成单角色的普通用户即可,如果有精力,再加一个管理员角色,前后台共用一套登录逻辑,通过角色字段路由到不同首页。
- 视频模块:视频列表展示、按分类筛选、关键词搜索、视频详情页、视频播放。注意,这里的视频指的是“视频条目信息”,不是视频文件本身,文件存储是另一套逻辑。
- 分类模块:视频分类管理,比如按类型(电影、番剧、纪录片)或者按语言、年份分类,一般做成两级分类就够。
- 评论模块:用户对视频进行评论,视频详情页展示评论列表。
- 后台管理模块:管理员可以维护视频信息(上传新的视频条目、修改描述、删除违规内容)、管理分类、管理用户状态(启用/禁用)。后台管理既是实际需求,也是扩展性最好的加分模块,建议一定保留。
功能不需要贪多。贪多的后果是每个模块都做得半吊子,表结构乱、代码耦合、联调的时候到处是问题。把上面六个模块做扎实,功能上已经是一个“五脏俱全”的在线视频系统了。
1.3 数据库表设计的心法与关键字段
数据库设计是这个项目里最先决定成败的一环。表设计不对,后面所有代码都难受。按我常用的方式,核心表至少有这么几张:
用户表
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int/bigint | 主键自增 |
| username | varchar(50) | 登录用户名 |
| password | varchar(128) | 加密后密码,别存明文 |
| phone/email | varchar(50) | 联系方式,可为空 |
| avatar | varchar(255) | 头像路径 |
| role | tinyint | 0普通用户,1管理员 |
| status | tinyint | 0禁用,1正常 |
| create_time | datetime | 注册时间 |
密码加密我用的是MD5加盐或者BCrypt,虽然在答辩里“MD5加密存储密码”这个设计已经足够辩解,但建议直接用BCrypt,面对“为什么你的密码是明文”这种送命题时你能答得更硬气。
视频表
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 视频ID |
| title | varchar(200) | 视频标题 |
| category_id | int | 所属分类 |
| cover_url | varchar(255) | 封面图路径 |
| video_url | varchar(255) | 视频文件访问路径 |
| description | text | 简介内容 |
| play_count | int | 播放次数 |
| uploader_id | int | 上传用户或管理员ID |
| status | tinyint | 上架/下架状态 |
| create_time | datetime | 发布时间 |
说明:视频文件的“存储路径”原则是数据库存相对路径或者虚拟路径,不存磁盘绝对路径。存绝对路径的问题是你换了服务器或者目录调整之后,库里的数据全部变成废数据,而相对路径配合Tomcat虚拟映射能灵活很多,这个后面实操章节我会展开讲。
分类表
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 分类ID |
| name | varchar(50) | 分类名 |
| parent_id | int | 父分类ID,0则是一级 |
| sort_order | int | 排序权重 |
分类表这个parent_id字段是典型的多级分类设计,递归查询时用MyBatis的collection嵌套结果集一次拿回来,一次SQL搞定两级分类,代码也整洁。
评论表:重点字段是id、video_id、user_id、content、create_time。评论分页这几列够了,不需要冗余用户名(关联查询拿就行,多表Join的成本不高)。
关于表关联关系,视频表和分类表是多对一,视频表和评论表是一对多,用户和评论是一对多,用户和视频(管理关系)是一对多。完整的数据库设计可以直接拿去建库,不用再加冗余的业务表。
2. 三层架构下的代码核心细节
2.1 各层类的划分与职责边界
SSM项目的代码结构我在实战中验证下来最顺手的分包逻辑是:
- entity/pojo包:放与数据库表对应的实体类,字段和表字段一一对应。注意这类对象不该加业务逻辑,它就是数据容器。
- mapper/dao包:接口 + 对应的XML映射文件。接口定义方法,XML写SQL。MyBatis之所以用XML写SQL而不是注解,是因为XML可以处理动态SQL(if、foreach),复杂查询时优势明显。
- service包:业务逻辑层。例如注册时校验用户名是否已存在、删除视频时同时清理评论数据,这些都是Service的工作范围。接口加实现类的拆分方式在答辩时很常见,工程上建议保留。
- controller包:接收前端请求、调用Service、返回视图或JSON数据。Controller里不要出现业务SQL或复杂计算,保持薄Controller。
拿用户视频列表业务打个样。用户在前端点击“电影”分类,浏览器发一个GET请求到/video/list?categoryId=3&pageNum=1,Controller收到参数,调用Service层的方法getVideoPage(categoryId, pageNum),Service内部先用PageHelper分页插件设置分页参数,再调用Mapper接口里的selectByCategoryId(categoryId)方法,MyBatis执行XML里的SQL,把结果封装成PageInfo<Video>返回给Controller,Controller把PageInfo转成JSON返回到前端页面。这个链路清晰、可控、好DEBUG,也正是SSM面试题最爱问的“请求流转过程”。
2.2 Spring与SpringMVC配置文件的取舍
这部分基本上每周都有人在群里问“为什么我的项目起不来”,十有八九是配置文件的问题。我直接给出我常用的配置骨架,照着写不会出大错。
spring-mvc.xml是控制器和视图层的配置,核心是三件事:扫描Controller包、开启注解驱动、配置视图解析器。
<context:component-scan base-package="com.example.video.controller"/> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <!-- 静态资源放行 --> <mvc:resources mapping="/static/**" location="/static/"/>applicationContext.xml(或spring-context.xml)是Spring的核心配置,管理Service、Mapper、DataSource、事务。它扫描的范围要排除Controller,避免和SpringMVC的扫描冲突。这是SSM整合里特别容易翻车的一点:两个容器都去扫同一个包,导致Bean重复定义或者事务失效。
<context:component-scan base-package="com.example.video"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>DataSource配置用properties文件外置,Druid连接池,配置项包括初始连接数、最大活跃连接数、空闲时间等:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/video_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码URL里的serverTimezone=Asia/Shanghai是MySQL 8.0连接时的硬性要求,少了它一定报时区错误,这个坑新手几乎都得踩一次。字符编码用utf8而不是utf8mb4也能跑,但既然做视频网站,用户评论里可能有人发emoji,用utf8mb4更稳妥。
事务配置用注解驱动,在spring-context.xml里加上<tx:annotation-driven transaction-manager="transactionManager"/>,然后在Service实现类或方法上标@Transactional。不用配置一大串aop切面,注解足够清晰。
2.3 MyBatis映射文件里的三个高频坑
MyBatis在SSM里是数据访问的核心,灵活但坑也多,这里说三个最常见的:
第一个坑是实体类字段名和表字段名不一致引发的“列找不到”报错。MySQL的命名习惯是下划线风格,Java实体类是驼峰风格,比如数据库字段create_time对实体类属性createTime。如果MyBatis配置不对,运行时它找不到对应的setter就会报错。解决方式是在MyBatis全局配置里打开驼峰映射:
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>这样MyBatis自动把你的下划线字段映射到驼峰属性,不用在resultMap里一个个手动配。但注意,多表联查的查询结果如果想自动封装成VO对象,还是要写resultMap指定映射规则,这个便利是一等一的。
第二个坑是动态SQL里if判断不等于null还要不等于空串。很多新手只判断非null,结果前端传了一个空的搜索关键词,SQL里拼出一个WHERE title LIKE '%%',虽然能查出数据,但效率极低,而且在某些场景下会查错数据。写判断时统一用:
<if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if>第三个坑是resultMap、分页、count查询之间的配合。我用PageHelper做分页时发现,如果分页的SQL里有多表关联,它的count语句偶尔会生成错误,特别是用了LEFT JOIN且Join条件里有子查询的场景。解决方式就是给分页插件指定自定义的countSQL,或者在编写Mapper SQL时尽量避免太过复杂的Join,把复杂查询拆成两步在Service层组装。
3. 核心功能实操:视频上传与播放的实现
3.1 前端页面与后端交互的设计
在线视频网站的前端有两套思路:一套是直接JSP + JSTL + BootStrap,渲染在服务端,这是SSM项目最经典的搭配,不用解决跨域问题,简单直接;另一套是Vue或React做前后端分离,后端只出JSON接口。如果你这个项目是用来毕业设计的,我倾向于前者——JSP这套方案更贴合SSM的授课知识,答辩时老师也更好理解,而且不需要额外再讲一层Node环境。用Vue做的项目当然加分,前提是你能把“为什么不用JSP而用前后端分离”这个设计决策说清楚。
页面规划上,至少要有这几个页面:用户登录/注册页、视频列表页(含分类筛选和搜索)、视频详情页(含播放器和评论区)、后台视频管理页、后台用户管理页。
列表页和后台管理页都要做分页。分页我直接用PageHelper插件,引入依赖后一行代码就能拿到分页结果:
PageHelper.startPage(pageNum, pageSize); List<Video> list = videoMapper.selectByCategory(categoryId); PageInfo<Video> pageInfo = new PageInfo<>(list);PageHelper底层是拦截器机制,它会把当前线程上下文里的分页参数绑定到下一次查询上,所以startPage后面必须紧跟Mapper查询语句,中间不要穿插其他数据库操作。这个顺序问题,我见N个同学踩过:startPage之后去查了别的表,分页就错乱了。
3.2 后端接收文件的关键步骤
视频文件和封面图的上传是MultipartFile机制,SpringMVC对文件上传的支持很好,步骤如下:
- 配置multipart解析器。
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="UTF-8"/> <property name="maxUploadSize" value="104857600"/> <property name="maxUploadSizePerFile" value="104857600"/> </bean>maxUploadSize限制了单次请求最大100MB、单个文件最大100MB。如果你打算录屏传视频,100MB可能不够,可以按自己的需求调整,但要注意Tomcat的请求体大小限制是2MB(默认),改大了需要同步修改Tomcat的配置。
- Controller层接收文件。
@PostMapping("/video/upload") @ResponseBody public Result upload(@RequestParam("file") MultipartFile file, @RequestParam("title") String title) { String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString().replace("-", "") + suffix; // 存到指定目录 String savePath = "E:/upload/video/" + newFileName; file.transferTo(new File(savePath)); // 回显路径 String virtualPath = "/upload/video/" + newFileName; return Result.success(virtualPath); }用UUID重命名文件是我一直坚持的做法:中文文件名很可能造成URL编码和下载时的乱码问题,而且重名文件会互相覆盖。重命名后文件名唯一、不可猜解、不影响后缀,缺点是丢失了原始文件名,如果需要可以再存一份原名到数据库备用。
- 配置虚拟路径映射。
数据库里存的是虚拟路径/upload/video/xxx.mp4,但文件物理上在E盘或者服务器别的目录,需要在SpringMVC的配置里做个资源映射:
<mvc:resources mapping="/upload/**" location="file:E:/upload/"/>这个配置的意思是,浏览器请求/upload/video/123.mp4时,SpringMVC会直接去E:/upload/video/123.mp4找文件。这样文件即使放在Tomcat外部目录,也能被访问到。好处是以后升级系统、重新部署war包时,上传文件不会丢失,这是实战经验。
3.3 播放功能怎么做不翻车
播放功能最稳妥的做法是HTML5的video标签直接播放MP4格式。现在浏览器对MP4/H.264的支持非常成熟,不需要额外引入任何播放器插件:
<video controls preload="metadata" style="width:100%;"> <source src="${video.videoUrl}" type="video/mp4"> 您的浏览器不支持HTML5视频播放。 </video>如果非要花哨一点,可以集成一个开源的播放器(比如业内常用的HTML5播放器库),实现倍速播放、截图、弹幕等功能,但这些是需要花时间联调的。我最推荐的做法是:初期先把原生video标签跑通,比如把视频上传、路径回显、播放成功这一整条链路验证完毕,后续有时间再考虑加播放器界面。一个项目里,核心链路跑通比花里胡哨的功能重要得多。
还有个细节提醒一下:视频格式一定要在服务端做校验,别信前端。前端可以用accept属性过滤,但那是用户可绕过的。在后端拿到MultipartFile后,通过后缀名判断、甚至读文件的头部魔数判断真实格式,否则用户随便传一个exe文件改名为mp4,后端存下来再被访问,轻则播放失败,重则被人上传恶意文件,在那之后服务器的安全系数基本为零了。文件的类型校验是我绝对不会省的一步。
4. 联调、部署与常见问题速查
4.1 本地开发环境搭建步骤
环境搭建这关,基本上是SSM项目劝退率最高的一步,原因在于版本组合问题。我把自己习惯的组合列出来,这个组合我实际跑通过很多遍:
- JDK 1.8(不要用JDK 17,Spring 5和旧Tomcat对高版本JDK支持有兼容问题)
- Tomcat 9.0(8.5也行,9对应Servlet规范4.0)
- Maven 3.6+(负责依赖管理)
- MySQL 5.7或8.0均可
- IDEA 2021及其以后版本
创建项目时注意,直接在Spring Initializr里选SpringBoot然后“撤回”SSM这种做法不现实。标准做法是新建一个Maven项目,手动引入三个框架的坐标依赖,打包方式要选“war”,因为要部署到Tomcat。依赖部分的核心坐标是spring-webmvc、mybatis、mybatis-spring、mysql-connector-java、druid、jstl、jackson-databind、pagehelper。
web.xml里的配置有三件套:ContextLoaderListener监听器加载Spring容器、DispatcherServlet配置SpringMVC、字符编码过滤器CharacterEncodingFilter。其中字符编码过滤器的Order要放在最前面,否则POST请求的中文参数到了后端就是乱码,这属于SSM项目里的经典疑难杂症。
4.2 我实测过的几个典型故障与排查思路
故障一:Tomcat启动时报ClassNotFoundException: DispatcherServlet。
产生原因基本都是缺少javax.servlet-api的依赖或者项目没有正确关联到Tomcat。排查思路:先看依赖里是否引入了spring-webmvc,再看部署结构里有没有lib目录。IDEA里常见操作是把依赖打进war包,直接在Project Structure -> Artifacts里把lib目录加上,这个问题就消失。
故障二:MyBatis提示Invalid bound statement (not found)
这个报错的意思是Mapper接口找到了,但对应的XML找不到。原因几乎只有一个:Mapper接口和XML文件放在同一个包路径下,但XML没有编译到classes目录。Maven默认只把resources目录里的文件打包,如果你把XML放在java目录下,就需要在pom.xml里显式声明:
<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build>这个配置是我见过无数次的救火代码。放进去之后,XML就会老老实实被打包到target里。
故障三:JSON返回给前端时日期格式变成一串数字。
Java里日期序列化成JSON默认会是时间戳,前端拿到的createTime是一串毫秒数,根本没法展示。解决办法有两种,一是在字段上加注解:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime;二是在SpringMVC配置里全局设置ObjectMapper:
<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper"> <bean class="com.fasterxml.jackson.databind.ObjectMapper"> <property name="dateFormat"> <bean class="java.text.SimpleDateFormat"> <constructor-arg value="yyyy-MM-dd HH:mm:ss"/> </bean> </property> </bean> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>从代码整洁度看,我更推荐注解方式,每个字段自己声明格式,直观且不会误伤其他类型。
4.3 部署上线的完整路径
开发完成后最终要部署。简单说一下标准流程:
- Maven打包:执行
mvn clean package,生成war包。 - 上传服务器:这里泛指任何一台性能足够的云主机,配置Tomcat和MySQL环境。
- 部署war包:把war包放进Tomcat的webapps目录,启动Tomcat,它会自动解压项目。
- 数据库初始化:在服务器MySQL上导入建库建表的SQL脚本。
- 配置文件调整:修改jdbc.properties里的数据库地址为你服务器的内网或公网地址。
- 访问验证:浏览器访问
http://服务器IP:8080/项目名/。
部署后常见的问题有一个:图片、视频上传到服务器通常没问题,但访问不到,这种10分钟能排查完——八成是/upload/**的虚拟路径映射在服务器上没有指定到真实路径,或者防火墙没有放开上传目录的访问权限。
4.4 项目还能怎么扩展加亮点
如果做完以上内容你还有余力,这里有几个性价比高的扩展方向:
- 视频搜索走全文索引:现在LIKE查询也能搜,但视频标题一多,性能就开始下降,可以考虑引入Elasticsearch做索引或至少做MySQL全文索引,作为一个亮点写在文档里。
- Redis做播放量缓存:视频详情页每次刷新都去UPDATE
play_count字段,数据库压力不小,用Redis做计数器,异步刷新到数据库,这是一个能拿得出手的答辩论据。 - 对象存储代替本地存储:把视频和封面传到云存储中,数据库只存访问URL,这样做的好处是扩展性和并发访问能力大幅提升,也不用操心磁盘空间问题。
- 用户收藏功能:视频表之外加一张收藏关系表,前端“收藏”按钮联动收藏状态的切换,这个功能的学习价值在于多对多关系在CRUD中的处理,也很适合展示。
我个人建议有精力就做两个:视频播放量的Redis缓存和用户收藏。这两个模块逻辑简单、表结构清晰,能快速落地,而且都能在答辩时自然引出“为什么用Redis”“怎么保证缓存和数据库一致性”这类高质量问题,展示你的设计思考比堆砌功能更受认可。
5. 写在最后的几句经验
做SSM在线视频网站这个项目,我的体会是:它的难点根本不在“代码量”或者“功能复杂度”上,而在于你能否把Spring、SpringMVC、MyBatis这三套东西真正串起来。很多同学在学框架的时候是分开学的,学Spring的时候不知道它怎么接管Service,学MyBatis的时候不知道SqlSession从哪里来,等到做项目才发现,原来把它们整合起来需要的配置和理念,比任何一个框架单独用都更考验理解。所以做这个项目的过程中,建议每一段配置都去查一下“这一行到底在扮演什么角色”,这比复制粘贴整整20个配置文件然后期待它跑起来更有收获。
另外,部署阶段如果有条件,尽量自己完整地走一遍“从本地打包到服务器运行”的流程,不要只在IDEA里点一下运行按钮就算完事。这个流程你毕业后的第一份工作前三个月一定还会碰到,提前熟悉,相当于把学费提前赚回来了。最后,别忘了学一下Git,项目初期就建仓,每个功能模块开发完提交一次,中间改崩了也有回退的余地。这个习惯,我从做这个项目开始一直保持到现在,真心建议你也养成。