简介:这是一套基于SSM框架开发的电影在线观看系统完整源码,面向Java Web初学者与中级开发者,适用于课程设计、毕业设计或Web全栈技能实战训练。系统采用JSP前端页面配合Layui UI组件,后端整合Spring、SpringMVC与MyBatis,支持用户注册登录、影片浏览、搜索、播放记录等功能,具备典型MVC分层结构与可扩展性。资源包共322个文件,涵盖35个Java业务类、31个XML映射配置、29个JSP视图页、30个JS交互脚本及23个JPG/PNG素材,辅以SQL建表语句与Maven依赖配置,总大小4.32MB,结构清晰、模块职责分明。已有551人学习下载,读者可直接导入Eclipse或IDEA运行调试,快速掌握SSM整合开发流程、Layui前端集成技巧及MySQL数据库设计实践。
1. 为什么一个“电影在线观看系统”要用 SSM + JSP + Layui 组合?它不是过时了,而是稳在刀刃上
很多人看到“Java + SSM + JSP + Layui + Maven + MySQL”这个技术栈第一反应是:这不就是十年前的老项目模板吗?但真实情况恰恰相反——在中小型企业内部系统、教育类实训项目、政务轻量后台、以及大量需要快速交付+长期可维护的业务型 Web 应用中,这套组合仍是落地成本最低、团队协作最顺、运维压力最小的黄金闭环。它不追求前端炫技或微服务拆分,而是把“用户能点开视频、管理员能增删影片、数据能查得准、上线不崩”作为硬性指标来兑现。JSP 负责服务端模板渲染与视频资源路径拼接,Layui 提供开箱即用的表格、弹窗、播放器容器和权限控制 UI 框架,SSM(Spring + SpringMVC + MyBatis)承担请求路由、事务管理与 SQL 映射,Maven 管理所有 jar 包版本与构建生命周期,MySQL 存储影片元数据、用户行为日志与播放记录。这套组合对 Java 初学者友好,对三年以上开发人员也足够清晰可控——没有隐藏的异步陷阱,没有跨域调试黑洞,没有 npm install 后的 node_modules 失效问题。它解决的不是“能不能做”,而是“能不能今天下午就让客户在测试服务器上看到首页轮播图和最新上映列表”。
2. 搭建 SSM 骨架:从 pom.xml 到 DispatcherServlet 的最小可行路径
2.1 Maven 依赖选型逻辑与关键版本锁定
Maven 不是简单罗列<dependency>,而是要建立三层依赖契约:基础运行时契约(JDK 兼容性)、框架协同契约(Spring 与 MyBatis 版本匹配)、前端桥接契约(JSP 标签库与 Servlet 规范对齐)。本项目采用 JDK 8(兼容绝大多数企业环境),因此 Spring 必须选用 5.3.x 系列(Spring 6 要求 JDK 17),MyBatis 用 3.4.6(适配 Spring 5.3 的 SqlSessionFactoryBean 初始化方式),JSP 相关依赖必须显式声明javax.servlet.jsp-api2.3.3(避免 Tomcat 9+ 自带的 jsp-api 冲突)。以下是pom.xml中不可省略的核心段落:
<properties> <spring.version>5.3.30</spring.version> <mybatis.version>3.4.6</mybatis.version> <mysql.connector.version>8.0.33</mysql.connector.version> <jstl.version>1.2</jstl.version> </properties> <dependencies> <!-- Spring 核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis 整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency> <!-- JSP 支持 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>${jstl.version}</version> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> </dependencies>提示:
<scope>provided</scope>表示该 jar 由 Tomcat 容器提供,打包时不会打入 WAR,避免与服务器内置 API 冲突。若使用 Tomcat 10+,需改用 Jakarta EE 命名空间(jakarta.servlet.jsp),此时必须同步升级 Spring 到 5.3.29+ 并启用spring.web.resources.static-path-pattern=/static/**配置。
2.2 web.xml 中 DispatcherServlet 的注册与初始化参数
SSM 项目启动入口是web.xml中的DispatcherServlet,它不是“配置完就完事”,而是决定了整个请求链路的起点与上下文加载方式。必须显式指定contextConfigLocation参数指向 Spring 配置文件,并设置load-on-startup为 1 保证优先加载:
<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>同时,spring-mvc.xml中必须开启注解驱动与静态资源放行:
<mvc:annotation-driven /> <mvc:default-servlet-handler /> <!-- 视图解析器:JSP 文件默认放在 /WEB-INF/jsp/ 下 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean>注意:
<mvc:default-servlet-handler />是关键——它让 DispatcherServlet 在找不到对应 Controller 时,将/static/css/app.css这类请求交还给 Tomcat 默认 Servlet 处理,否则所有静态资源都会 404。这是 JSP 项目区别于前后端分离项目的底层机制。
2.3 MyBatis 数据源与 SqlSessionFactoryBean 的手动装配
SSM 中 MyBatis 不通过@MapperScan自动扫描(那是 Spring Boot 方式),而是通过 XML 手动注入SqlSessionFactoryBean。其核心在于dataSource和configLocation的绑定顺序:必须先定义数据源,再将其注入到SqlSessionFactoryBean,最后由MapperScannerConfigurer扫描接口。典型配置如下:
<!-- 数据源:使用 Druid 替代 BasicDataSource,支持连接池监控 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost:3306/movie_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8" /> <property name="username" value="root" /> <property name="password" value="123456" /> <property name="initialSize" value="5" /> <property name="maxActive" value="20" /> </bean> <!-- SqlSessionFactoryBean --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation" value="classpath:mybatis-config.xml" /> <property name="mapperLocations" value="classpath:mapper/*.xml" /> </bean> <!-- Mapper 接口扫描 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.movie.dao" /> </bean>关键点说明:
mapperLocations使用classpath:mapper/*.xml表示所有src/main/resources/mapper/下的 XML 映射文件会被加载;basePackage必须与 DAO 接口所在包路径完全一致,且接口名(如MovieDao)需与 XML 文件名(MovieDao.xml)严格匹配,否则Invalid bound statement异常必然出现。
3. JSP 页面与 Layui 集成:视频播放、分页与权限控制的三重落地
3.1 JSP 中嵌入 MP4 视频播放器的兼容写法
JSP 不是静态 HTML,它需要动态拼接视频路径并处理浏览器兼容性。不能直接写<video src="${movie.url}">,因为${movie.url}可能是相对路径(如/upload/avengers.mp4),而 Tomcat 默认不开放/upload目录访问。正确做法是:在 Controller 中将视频 URL 转为绝对路径,或配置 Tomcat 的Context元素映射上传目录。更稳妥的方式是在web.xml中添加资源映射:
<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/upload/*</url-pattern> </servlet-mapping>然后在 JSP 中使用标准 HTML5<video>标签,并通过 Layui 的layer弹窗封装播放逻辑:
<c:forEach items="${movieList}" var="movie"> <div class="layui-card"> <div class="layui-card-header">${movie.title}</div> <div class="layui-card-body"> <img src="${pageContext.request.contextPath}${movie.coverPath}" alt="${movie.title}" width="120" height="180" /> <button class="layui-btn layui-btn-sm" onclick="playVideo('${movie.videoUrl}')">播放</button> </div> </div> </c:forEach> <script> function playVideo(videoUrl) { layer.open({ type: 1, title: '在线播放', content: '<video controls width="100%" height="400"><source src="' + videoUrl + '" type="video/mp4"></video>', area: ['800px', '500px'] }); } </script>注意:
videoUrl必须是能被浏览器直接访问的 HTTP 地址(如/upload/avengers.mp4),不能是D:/upload/avengers.mp4这类本地路径。Controller 返回前需调用String url = request.getContextPath() + "/upload/" + filename;构造合法 URL。
3.2 Layui Table 分页与后端数据联动的完整链路
Layui 的table.render()默认走 Ajax,但 SSM 项目中 Controller 必须返回 JSON 格式且符合 Layui 的data字段规范。常见错误是返回List<Movie>而非{ "code": 0, "msg": "", "count": 100, "data": [...] }结构。因此需定义统一响应体:
public class LayuiTableResult<T> { private int code; // 0 成功,1 失败 private String msg; private long count; // 总条数 private List<T> data; // getter/setter 省略 }Controller 方法示例:
@RequestMapping("/movie/list") @ResponseBody public LayuiTableResult<Movie> list(@RequestParam Integer page, @RequestParam Integer limit) { PageHelper.startPage(page, limit); List<Movie> movies = movieService.selectAll(); PageInfo<Movie> pageInfo = new PageInfo<>(movies); return new LayuiTableResult<>(0, "", pageInfo.getTotal(), movies); }JSP 中的 Layui table 初始化:
<table id="movieTable" lay-filter="movieTable"></table> <script> layui.use(['table', 'layer'], function(){ var table = layui.table; table.render({ elem: '#movieTable', url: '${pageContext.request.contextPath}/movie/list', page: true, cols: [[ {field:'id', title:'ID', width:80}, {field:'title', title:'片名', width:200}, {field:'director', title:'导演', width:150}, {field:'duration', title:'时长(分钟)', width:120}, {fixed: 'right', title:'操作', toolbar: '#movieBar', width:180} ]] }); // 工具栏事件 table.on('tool(movieTable)', function(obj){ if(obj.event === 'play'){ playVideo(obj.data.videoUrl); } }); }); </script>提示:
page: true表示开启 Layui 自带分页,url必须是完整路径(含 contextPath),否则跨上下文请求失败;toolbar: '#movieBar'对应页面底部的<script type="text/html" id="movieBar">...</script>模板,用于定义“播放”“编辑”等按钮。
3.3 基于 Session 的简易权限控制:管理员与普通用户视图隔离
SSM 项目不引入 Shiro 或 Spring Security,而是用最朴素的HttpSession实现角色分流。登录成功后将用户角色存入 session:
// LoginController.java @RequestMapping("/login") public String login(String username, String password, HttpSession session) { User user = userService.login(username, password); if (user != null) { session.setAttribute("user", user); session.setAttribute("role", user.getRole()); // role = "admin" or "user" return "redirect:/index.jsp"; } return "login"; }在 JSP 中用 JSTL 判断角色并渲染不同内容:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:if test="${sessionScope.role == 'admin'}"> <a class="layui-btn layui-btn-sm" href="${pageContext.request.contextPath}/movie/add">新增影片</a> </c:if> <c:if test="${sessionScope.role == 'user'}"> <span class="layui-badge">普通用户</span> </c:if>注意:
sessionScope是 JSP EL 的内置对象,无需额外导入;<c:if>标签必须在页面顶部声明<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>,否则会报错According to TLD or attribute directive in tag file, attr is not a valid attribute。
4. MySQL 影片表设计与视频元数据存储策略
4.1 核心表结构:兼顾查询效率与扩展性
电影在线系统不是博客,它的数据模型必须支撑“按类型筛选”“按年份排序”“模糊搜索片名”“统计播放次数”四大高频操作。因此movie表不能只存标题和 URL,还需预计算字段与索引策略:
CREATE TABLE `movie` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(100) NOT NULL COMMENT '片名', `director` VARCHAR(50) DEFAULT NULL COMMENT '导演', `actors` TEXT COMMENT '主演(逗号分隔)', `type` VARCHAR(20) NOT NULL COMMENT '类型:动作/喜剧/科幻', `year` SMALLINT NOT NULL COMMENT '上映年份', `duration` INT NOT NULL COMMENT '时长(分钟)', `cover_path` VARCHAR(255) NOT NULL COMMENT '封面图路径', `video_url` VARCHAR(255) NOT NULL COMMENT '视频文件路径', `description` TEXT COMMENT '简介', `play_count` INT DEFAULT 0 COMMENT '播放次数', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX `idx_type_year` (`type`, `year`), INDEX `idx_title` (`title`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='影片主表';关键设计说明:
cover_path和video_url存储的是相对路径(如/upload/cover/avengers.jpg),由 Web 服务器映射到物理目录;idx_type_year是联合索引,支撑WHERE type='动作' AND year >= 2020类查询;idx_title是单列索引,配合LIKE '%复仇%'实现模糊搜索(虽不如全文索引高效,但满足中小数据量);play_count字段避免每次播放都查表更新,直接UPDATE movie SET play_count = play_count + 1 WHERE id = ?即可。
4.2 视频文件存储路径规范与安全性约束
MySQL 不存储视频二进制数据(BLOB),而是存路径引用。物理文件必须放在 Web 应用可访问的目录下,且需规避路径遍历风险。推荐方案:
- 视频文件统一存放在
webapp/upload/video/目录; - 封面图存放在
webapp/upload/cover/目录; - Controller 中校验文件名合法性,禁止
../、%2e%2e等绕过:
private boolean isValidFilename(String filename) { return filename != null && !filename.contains("..") && !filename.startsWith("/") && filename.matches("^[a-zA-Z0-9_\\-\\.]+$"); }上传后生成唯一文件名(如UUID.randomUUID().toString().replace("-", "") + ".mp4"),再保存到数据库。这样既防止覆盖,又杜绝恶意文件名执行。
4.3 查询优化实战:如何让“最新上映”列表 10ms 内返回
当影片数量超过 1 万条时,ORDER BY create_time DESC LIMIT 10会变慢。解决方案不是加索引,而是冗余字段 + 覆盖索引:
ALTER TABLE movie ADD COLUMN `is_latest` TINYINT DEFAULT 0 COMMENT '是否最新上映(1=是)'; -- 每次新增影片时,将前 10 条设为 1,其余设为 0 UPDATE movie SET is_latest = 0; UPDATE movie SET is_latest = 1 ORDER BY create_time DESC LIMIT 10; -- 创建覆盖索引 CREATE INDEX `idx_latest` ON movie(`is_latest`, `create_time`, `id`, `title`, `cover_path`);查询语句改为:
SELECT id, title, cover_path, create_time FROM movie WHERE is_latest = 1 ORDER BY create_time DESC;效果验证:执行
EXPLAIN可见type=ref、Extra=Using index,表示走覆盖索引,无需回表。实测 5 万条数据下查询耗时稳定在 3~8ms。
5. Maven 构建与部署排错:从 war 包缺失 class 到 JSP 修改不生效的根因定位
5.1 WAR 包中缺少 com.movie.controller 包的三大原因与修复
Maven 打包后发现WEB-INF/classes/com/movie/controller/目录为空,常见于以下场景:
| 现象 | 根因 | 修复方式 |
|---|---|---|
mvn clean package后 target/classes 下无 controller 类 | src/main/java目录未被 Maven 正确识别为源码目录 | 在 IDE 中右键项目 → Maven → Update Project,勾选Force Update;或检查.project文件中<buildCommand><name>org.eclipse.m2e.core.maven2Builder</name></buildCommand>是否存在 |
target/movie.war解压后WEB-INF/classes/下有 dao、service 但无 controller | pom.xml中maven-compiler-plugin编译级别低于 JDK 实际版本 | 显式声明插件并锁定 source/target:xml<br><plugin><br> <groupId>org.apache.maven.plugins</groupId><br> <artifactId>maven-compiler-plugin</artifactId><br> <version>3.8.1</version><br> <configuration><br> <source>1.8</source><br> <target>1.8</target><br> </configuration><br></plugin><br> |
Controller 类存在但DispatcherServlet找不到映射 | @Controller注解类未被 Spring 扫描到 | 检查spring-mvc.xml中<context:component-scan base-package="com.movie" />的base-package是否包含 controller 所在包,且不能漏掉子包(如com.movie.controller) |
5.2 JSP 修改后页面不刷新:不是缓存,是 Tomcat 的 JSP 编译机制
JSP 不是纯静态文件,它会被 Tomcat 编译为 Servlet(存于work/Catalina/localhost/[app]/org/apache/jsp/)。修改 JSP 后不生效,90% 情况是:
- Tomcat 未检测到文件变更:在
conf/context.xml中添加reloadable="true"属性; - JSP 编译缓存未清除:删除
work/Catalina/localhost/[app]/下全部内容; - IDE 自动部署未触发:Eclipse 中需勾选
Project → Properties → MyEclipse → Web → Deploy → Auto Deploy on Save;IntelliJ IDEA 中需在Build → Build Artifacts → Rebuild后点击Redeploy。
最彻底的验证方式:打开work目录下的.java文件(如index_jsp.java),搜索out.print,确认输出内容是否与最新 JSP 一致。
5.3 MySQL 8.0 连接失败:Public Key Retrieval is not allowed的本质与解法
使用 MySQL Connector/J 8.0+ 连接时抛出SQLException: Public Key Retrieval is not allowed,根本原因是 MySQL 8.0 默认启用caching_sha2_password认证插件,而旧版驱动要求显式授权公钥获取。两种解法:
方案一(推荐):修改用户认证方式
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;方案二(临时):URL 中添加参数
jdbc:mysql://localhost:3306/movie_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false注意:
allowPublicKeyRetrieval=true仅用于开发环境,生产环境必须用方案一,否则存在中间人攻击风险。
6. Layui 日期控件与 JSP 动态赋值的精准协同技巧
6.1 Layui date 组件设置最大日期为当前日期的两种写法
Layui 的laydate.render()默认不限制日期范围,但电影系统中“上映年份”字段需限制为 ≤ 当前年份。不能写死max: '2024-01-01',而应动态生成:
方法一:JSP 中生成 JS 变量
<script> var today = new Date(); var maxDate = (today.getFullYear()) + '-' + (today.getMonth() + 1).toString().padStart(2, '0') + '-' + today.getDate().toString().padStart(2, '0'); </script>然后在laydate.render()中引用:
laydate.render({ elem: '#releaseYear', type: 'year', max: maxDate.split('-')[0] // 只取年份 });方法二:后端传参(更安全)
// Controller model.addAttribute("currentYear", Calendar.getInstance().get(Calendar.YEAR));JSP 中直接使用:
<input type="text" id="releaseYear" lay-key="year" /> <script> laydate.render({ elem: '#releaseYear', type: 'year', max: ${currentYear} }); </script>优势对比:方法二避免 JS 日期格式化误差,且不受客户端时区影响;方法一适合纯前端场景,但需注意
new Date()获取的是浏览器本地时间。
6.2 Layui table 单列绑定点击事件的 DOM 代理写法
Layui table 的event机制本质是事件委托,但初学者常误以为tool事件只能用于工具栏按钮。其实可通过cell事件监听任意列:
table.on('cell(movieTable)', function(obj){ if(obj.field === 'title'){ // 点击片名列 layer.msg('你点击了:' + obj.value); // 此处可跳转详情页或弹窗 window.location.href = '${pageContext.request.contextPath}/movie/detail?id=' + obj.data.id; } });关键点:
obj.field是列的field属性值(如title),obj.data是整行数据对象,obj.value是当前单元格值。此写法比给每行加<a onclick="...">更轻量,且支持虚拟滚动。
6.3 JSP 中 JSONArray 的正确导入与遍历(避免import javax.json.*错误)
网络热词中频繁出现jsp jsonarray import,但 JSP 本身不支持javax.json(Java EE 7+ 特性,Tomcat 8.5+ 才内置)。正确做法是:
- 后端组装 JSON 字符串:用
com.alibaba.fastjson.JSON或org.json.JSONObject生成字符串,传入 request; - JSP 中用 JSTL 遍历:若数据已是
List<Map<String, Object>>,直接用<c:forEach>;
// Controller List<Map<String, Object>> tags = new ArrayList<>(); Map<String, Object> tag = new HashMap<>(); tag.put("name", "科幻"); tag.put("count", 12); tags.add(tag); request.setAttribute("tags", tags);<c:forEach items="${tags}" var="tag"> <span class="layui-badge">${tag.name}(${tag.count})</span> </c:forEach>严禁写法:
<%@ page import="javax.json.JsonArray" %>—— Tomcat 未提供该类,编译即报错。JSON 处理必须在后端完成,JSP 只负责展示。
本文还有配套的精品资源,点击获取