简介:这是一套基于SSM框架开发的电影在线观看系统完整源码,面向Java Web初学者与课程设计实践者,帮助掌握MVC分层架构、前后端交互及数据库操作等核心技能。资源包含322个文件,涵盖35个Java业务类、31个XML配置与映射文件、29个JSP页面、30个JS脚本、23个JPG和15个PNG图片资源,以及Layui前端组件、MySQL建表SQL和Maven依赖配置等,总大小4.32MB,结构清晰、模块完整,便于理解用户管理、影片展示、播放历史等典型功能实现。已有551人学习下载,提供可直接部署运行的完整工程(支持Eclipse/IDEA),含Controller、Service、Mapper多层实现类及对应实体类,代码规范、注释充分,适合作为Java Web综合实训项目参考或二次开发基础。
1. 这不是又一个“学生管理系统”:SSM+JSP+Layui 构建的电影在线观看系统,专治「页面跳转丢失状态」「Layui Tab 切换后 JS 失效」「JSP 中 MySQL 连接池反复初始化」三类真实线上病灶
很多刚学完 SSM 的人一上来就奔 Spring Boot,但实际交付中仍有大量存量 JSP 系统在持续迭代——尤其像电影在线观看这类强交互、多 Tab 导航、需细粒度权限控制的业务场景。这个项目不是教学 Demo,它用标准 SSM 分层(Spring + SpringMVC + MyBatis)打底,JSP 做视图层,Layui 实现响应式前端,MySQL 存储影片元数据与用户行为。关键在于它解决了三个高频实战痛点:一是用户点击「我的收藏」Tab 后,Layui 的 iframe 页面无法复用已加载的 jQuery 和 Layui 模块,导致按钮点击无响应;二是 JSP 页面中嵌入的 MySQL 查询语句未使用连接池,每次请求新建 Connection,高并发下数据库报Too many connections;三是 SSM 配置中web.xml的DispatcherServlet映射路径与 JSP 资源路径冲突,造成/movie/detail?id=123这类 URL 无法正确路由到 Controller。适合正在维护老系统、需要快速交付带权限管理的影视类 Web 应用的 Java 开发者,也适合作为理解 JSP 在现代 SSM 架构中定位的实操样本。
2. SSM 分层结构如何支撑电影业务逻辑:从 Spring 容器装配到 MyBatis 动态 SQL 的落地细节
2.1 Spring 配置核心:为什么必须用context:component-scan而非 XML 手动注册 Service Bean?
本项目采用注解驱动的 Spring 配置方式,在applicationContext.xml中启用组件扫描:
<context:component-scan base-package="com.movie.service, com.movie.dao"/>提示:
base-package必须精确包含 Service 和 DAO 层包路径,若只写com.movie会导致 Controller 被重复扫描(因 SpringMVC 配置中已扫描com.movie.controller),引发BeanDefinitionOverrideException。此处不使用<bean>标签手动声明,是因为 Service 类上已标注@Service("movieService"),Spring 会自动识别并注入依赖。
Service 层接口MovieService.java与实现类MovieServiceImpl.java之间通过@Autowired注入 DAO:
@Service("movieService") public class MovieServiceImpl implements MovieService { @Autowired private MovieDao movieDao; // 此处注入由 MyBatis-Spring 提供的 MapperFactoryBean @Override public List<Movie> getHotMovies() { return movieDao.selectHotMovies(); } }该设计使业务逻辑与数据访问解耦:Controller 只调用movieService.getHotMovies(),无需感知 DAO 层具体实现。当后续需接入 Redis 缓存热门影片时,只需在MovieServiceImpl中添加@Cacheable注解,无需修改 Controller 或 DAO。
2.2 MyBatis 动态 SQL 实现「按类型/评分/年份」多条件组合查询
电影列表页需支持用户勾选「动作片」「8.5 分以上」「2020 年后」等筛选项。MyBatis 的<where>和<if>标签避免了拼接 SQL 的硬编码风险:
<!-- MovieMapper.xml --> <select id="selectMoviesByCondition" resultType="com.movie.entity.Movie"> SELECT * FROM movie_info <where> <if test="type != null and type != ''"> AND type LIKE CONCAT('%', #{type}, '%') </if> <if test="minScore != null"> AND score >= #{minScore} </if> <if test="yearFrom != null"> AND release_year >= #{yearFrom} </if> </where> ORDER BY score DESC, update_time DESC </select>参数对象MovieQueryCondition.java定义如下:
public class MovieQueryCondition { private String type; // 类型,如 "动作" private Double minScore; // 最低评分 private Integer yearFrom; // 起始年份 // getter/setter 省略 }调用时传入完整条件对象即可:
MovieQueryCondition condition = new MovieQueryCondition(); condition.setType("动作"); condition.setMinScore(8.5); condition.setYearFrom(2020); List<Movie> movies = movieDao.selectMoviesByCondition(condition);注意:
<where>标签会自动处理首条件前的AND,若所有<if>条件都不满足,则生成SELECT * FROM movie_info,而非语法错误的SELECT * FROM movie_info WHERE。这是 MyBatis 区别于手写 JDBC 拼接的关键优势。
2.3 SpringMVC 控制器如何精准路由 JSP 页面并传递分页数据?
MovieController.java中定义/movie/list请求映射,接收分页参数并返回ModelAndView:
@Controller @RequestMapping("/movie") public class MovieController { @Autowired private MovieService movieService; @RequestMapping("/list") public ModelAndView listMovies( @RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "size", defaultValue = "12") int size, MovieQueryCondition condition) { PageHelper.startPage(page, size); // 使用 PageHelper 插件进行物理分页 List<Movie> movies = movieService.selectMoviesByCondition(condition); PageInfo<Movie> pageInfo = new PageInfo<>(movies); ModelAndView mav = new ModelAndView("movie/list"); // 对应 /WEB-INF/jsp/movie/list.jsp mav.addObject("pageInfo", pageInfo); mav.addObject("condition", condition); return mav; } }关键点在于:
@RequestParam默认值defaultValue = "1"避免空参导致NumberFormatExceptionPageHelper.startPage()在查询前开启分页,PageInfo封装总记录数、当前页、页码列表等信息ModelAndView的视图名"movie/list"由 SpringMVC 的InternalResourceViewResolver解析为/WEB-INF/jsp/movie/list.jsp,确保 JSP 文件不被直接 URL 访问(安全防护)
list.jsp中通过 JSTL 获取分页数据:
<c:forEach items="${pageInfo.list}" var="movie"> <div class="movie-item"> <h3>${movie.title}</h3> <p>评分:${movie.score} | 年份:${movie.releaseYear}</p> </div> </c:forEach> <!-- 分页导航 --> <div class="pagination"> <a href="${pageContext.request.contextPath}/movie/list?page=1&size=${pageInfo.pageSize}">首页</a> <c:if test="${pageInfo.hasPreviousPage}"> <a href="${pageContext.request.contextPath}/movie/list?page=${pageInfo.prePage}&size=${pageInfo.pageSize}">上一页</a> </c:if> <span>第 ${pageInfo.pageNum} 页,共 ${pageInfo.pages} 页</span> <c:if test="${pageInfo.hasNextPage}"> <a href="${pageContext.request.contextPath}/movie/list?page=${pageInfo.nextPage}&size=${pageInfo.pageSize}">下一页</a> </c:if> <a href="${pageContext.request.contextPath}/movie/list?page=${pageInfo.pages}&size=${pageInfo.pageSize}">末页</a> </div>3. Layui Tab 与 JSP 混合开发的三大陷阱及绕过方案
3.1 问题根源:Layui Tab 的 iframe 加载机制导致 JSP 页面 JS 环境隔离
当用户点击左侧菜单「影片分类」,Layui 执行:
// menu.js element.on('nav(demo)', function(elem){ var url = elem.attr('lay-href'); if(url){ layui.use('tab', function(tab){ tab.add({ title: elem.text(), url: url, // 如 '/movie/category.jsp' id: 'category' }); }); } });此时/movie/category.jsp在独立 iframe 中加载,其内部<script>标签引入的jquery.min.js和layui.all.js与父页面的 JS 环境完全隔离。导致layui.use(['table', 'form'], ...)在 iframe 内失效,表格渲染失败。
3.1.1 绕过方案:改用content模式替代url模式加载 Tab
将 Tab 加载方式从 iframe 改为 DOM 内容注入,复用父页面已加载的 Layui 模块:
// 修改 menu.js 中的 tab.add 调用 tab.add({ title: elem.text(), content: '<div id="category-content"></div>', // 占位 div id: 'category' }); // 然后动态加载 JSP 内容到该 div $.get('/movie/category.jsp', function(html){ $('#category-content').html(html); // 手动触发 Layui 模块初始化 layui.use(['table', 'form'], function(){ var table = layui.table; var form = layui.form; // 初始化表格 table.render({ elem: '#movie-table', url: '/movie/api/list', cols: [[ {field:'title', title:'片名'}, {field:'score', title:'评分'} ]] }); }); });此方案要求/movie/category.jsp不再包含<html><head><body>结构,仅保留业务 HTML 片段(如<table id="movie-table"></table>),由父页面统一管理资源加载。
3.2 JSP 页面中 Layui 表单提交后如何保持 Tab 活跃状态?
用户在「影片上传」Tab 中提交表单,若直接response.sendRedirect()会刷新整个页面,Tab 切换状态丢失。正确做法是使用 AJAX 提交,并在成功回调中操作 Tab:
<!-- upload.jsp --> <form class="layui-form" action="/movie/upload" method="post" id="uploadForm"> <div class="layui-form-item"> <label class="layui-form-label">片名</label> <div class="layui-input-block"> <input type="text" name="title" required lay-verify="required"> </div> </div> <button class="layui-btn" lay-submit lay-filter="upload">立即提交</button> </form> <script> layui.use(['form', 'layer'], function(){ var form = layui.form; var layer = layui.layer; form.on('submit(upload)', function(data){ $.ajax({ url: '/movie/upload', type: 'POST', data: data.field, success: function(res){ if(res.code === 0){ layer.msg('上传成功', {icon: 1}); // 刷新当前 Tab 内容,而非跳转 location.reload(); // 或更优:重新加载表格数据 } } }); return false; // 阻止表单默认提交 }); }); </script>3.3 Layui Tabs 刷新页面后如何恢复上次激活的 Tab?
浏览器刷新后,所有 Tab 状态丢失。解决方案是将当前 Tab ID 存入localStorage,并在页面加载时读取并激活:
// 在 tab.add 后添加存储逻辑 tab.add({ title: elem.text(), url: url, id: tabId }); localStorage.setItem('activeTabId', tabId); // 存储激活 Tab ID // 页面加载完成后恢复 $(function(){ var lastTabId = localStorage.getItem('activeTabId'); if(lastTabId){ tab.tabChange(lastTabId); } });注意:
localStorage存储的是字符串,需确保tabId全局唯一(建议用 URL 路径哈希或时间戳生成)。
4. MySQL 数据库设计与连接池配置:从表结构到 Druid 连接泄漏检测
4.1 影片核心表movie_info与关联表的设计依据
本系统 MySQL 表结构围绕「用户-影片-分类-收藏」关系展开,关键表如下:
| 表名 | 字段说明 | 设计要点 |
|---|---|---|
movie_info | id(PK),title,director,actors,type,score,release_year,duration,cover_url,summary,status(0下架/1上架) | type用 VARCHAR 存储逗号分隔值(如"动作,犯罪"),简化查询;status实现软删除,避免外键级联删除风险 |
user_favorite | id(PK),user_id,movie_id,create_time | 联合索引(user_id, movie_id)加速「用户收藏列表」和「影片被收藏次数」查询 |
movie_category | id(PK),name,sort_order | sort_order控制后台分类展示顺序 |
创建movie_info表的 DDL 示例:
CREATE TABLE `movie_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '片名', `director` varchar(50) DEFAULT NULL COMMENT '导演', `actors` varchar(200) DEFAULT NULL COMMENT '主演(逗号分隔)', `type` varchar(100) DEFAULT NULL COMMENT '类型(逗号分隔)', `score` decimal(2,1) DEFAULT '0.0' COMMENT '评分', `release_year` int(4) DEFAULT NULL COMMENT '上映年份', `duration` int(4) DEFAULT NULL COMMENT '时长(分钟)', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面图 URL', `summary` text COMMENT '剧情简介', `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-下架,1-上架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_score` (`score`) USING BTREE, KEY `idx_type` (`type`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='影片信息表';提示:
type字段虽未用关联表,但通过LIKE CONCAT('%', #{type}, '%')查询可满足前台筛选需求,且避免多表 JOIN 带来的性能损耗。当分类数量超 1000 时,再考虑拆分为movie_type_rel关联表。
4.2 Druid 连接池配置解决 JSP 页面频繁创建 Connection 的问题
原始web.xml中若直接在 JSP 里用Class.forName().getConnection(),每个请求都新建连接。本项目改用 Druid 连接池,在spring-dao.xml中配置:
<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="minIdle" value="5"/> <property name="maxActive" value="20"/> <property name="maxWait" value="60000"/> <property name="timeBetweenEvictionRunsMillis" value="60000"/> <property name="minEvictableIdleTimeMillis" value="300000"/> <property name="validationQuery" value="SELECT 1"/> <property name="testWhileIdle" value="true"/> <property name="testOnBorrow" value="false"/> <property name="testOnReturn" value="false"/> <property name="poolPreparedStatements" value="true"/> <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/> <property name="filters" value="stat,wall,log4j"/> </bean>关键参数说明:
initialSize=5:应用启动时创建 5 个空闲连接,避免首次请求延迟maxActive=20:最大连接数,需根据 MySQLmax_connections设置(如 MySQL 设置为 100,则此处设 20 安全)validationQuery="SELECT 1":空闲连接检测 SQL,确保连接有效性filters="stat,wall,log4j":启用监控统计、防火墙(防 SQL 注入)、日志输出
Druid 监控页面可通过http://localhost:8080/druid访问(需在web.xml中配置DruidStatViewServlet),实时查看连接池活跃数、SQL 执行耗时、慢 SQL 记录。
4.3 Maven 依赖管理中的版本冲突规避策略
pom.xml中明确指定各框架版本,避免传递依赖引发的兼容性问题:
<properties> <spring.version>5.3.30</spring.version> <mybatis.version>3.4.6</mybatis.version> <druid.version>1.2.16</druid.version> <mysql.connector.version>8.0.33</mysql.connector.version> <jstl.version>1.2</jstl.version> </properties> <dependencies> <!-- Spring Core --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis-Spring 集成 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> <!-- 严格匹配 MyBatis 3.4.x --> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> <scope>runtime</scope> </dependency> </dependencies>注意:MyBatis 3.4.6 与 mybatis-spring 1.3.2 是官方验证兼容的组合。若升级 MyBatis 至 3.5.x,必须同步升级 mybatis-spring 至 2.x,否则
SqlSessionFactoryBean初始化失败。
5. JSP 页面中嵌入式 Java 代码的安全边界与性能优化技巧
5.1 禁止在 JSP 中编写业务逻辑:用 JSTL 替代<% %>脚本片段
早期 JSP 常见写法:
<% String userType = (String) session.getAttribute("userType"); if("admin".equals(userType)){ %> <a href="/admin/movie">后台管理</a> <% } %>这种脚本片段难以测试、易出错、违反 MVC 分离原则。正确做法是 Controller 预先判断并设置属性:
// MovieController.java mav.addObject("isAdmin", "admin".equals(session.getAttribute("userType")));JSP 中用 JSTL 判断:
<c:if test="${isAdmin}"> <a href="${pageContext.request.contextPath}/admin/movie">后台管理</a> </c:if>5.2 JSP 中 MySQL 查询结果的缓存策略:避免重复查库
影片详情页/movie/detail.jsp需显示影片信息、评论列表、相关推荐。若每个模块都独立查库,一次请求触发 3 次 DB 查询。优化方案是在 Controller 中一次性查出所有数据:
@RequestMapping("/detail") public ModelAndView detail(@RequestParam Long id) { MovieDetailVO detailVO = movieService.getMovieDetail(id); // 封装影片+评论+推荐 ModelAndView mav = new ModelAndView("movie/detail"); mav.addObject("detail", detailVO); return mav; }MovieDetailVO.java定义:
public class MovieDetailVO { private Movie movie; private List<Comment> comments; private List<Movie> relatedMovies; // getter/setter }JSP 中直接遍历:
<h1>${detail.movie.title}</h1> <div class="comments"> <c:forEach items="${detail.comments}" var="comment"> <p>${comment.content} —— ${comment.userName}</p> </c:forEach> </div> <div class="related"> <c:forEach items="${detail.relatedMovies}" var="rel"> <a href="${pageContext.request.contextPath}/movie/detail?id=${rel.id}">${rel.title}</a> </c:forEach> </div>5.3 JSP 编译优化:启用trimDirectiveWhitespaces减少 HTML 冗余空格
在web.xml中配置 JSP 编译选项,减少输出 HTML 中的空白字符:
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <trim-directive-whitespaces>true</trim-directive-whitespaces> <default-page>index.jsp</default-page> </jsp-property-group> </jsp-config>启用后,JSP 中的换行和缩进不再输出为 HTML 空格,提升前端解析效率,尤其对 Layui 的 DOM 操作更友好(避免因空文本节点干扰layui.element.init())。
提示:
trim-directive-whitespaces是 Servlet 3.0+ 特性,需确认 Tomcat 版本 ≥ 7.0 或 Jetty ≥ 8.0。若使用旧版容器,可通过 Maven 插件maven-jspc-plugin预编译 JSP 并压缩。
本文还有配套的精品资源,点击获取