简介这是一套基于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. 这不是又一个“学生管理系统”SSMJSPLayui 构建的电影在线观看系统专治「页面跳转丢失状态」「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?id123这类 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-packagecom.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注入 DAOService(movieService) public class MovieServiceImpl implements MovieService { Autowired private MovieDao movieDao; // 此处注入由 MyBatis-Spring 提供的 MapperFactoryBean Override public ListMovie 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 idselectMoviesByCondition resultTypecom.movie.entity.Movie SELECT * FROM movie_info where if testtype ! null and type ! AND type LIKE CONCAT(%, #{type}, %) /if if testminScore ! null AND score #{minScore} /if if testyearFrom ! 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); ListMovie 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请求映射接收分页参数并返回ModelAndViewController 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 插件进行物理分页 ListMovie movies movieService.selectMoviesByCondition(condition); PageInfoMovie 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} varmovie div classmovie-item h3${movie.title}/h3 p评分${movie.score} | 年份${movie.releaseYear}/p /div /c:forEach !-- 分页导航 -- div classpagination a href${pageContext.request.contextPath}/movie/list?page1size${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 /div3. 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 idcategory-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不再包含htmlheadbody结构仅保留业务 HTML 片段如table idmovie-table/table由父页面统一管理资源加载。3.2 JSP 页面中 Layui 表单提交后如何保持 Tab 活跃状态用户在「影片上传」Tab 中提交表单若直接response.sendRedirect()会刷新整个页面Tab 切换状态丢失。正确做法是使用 AJAX 提交并在成功回调中操作 Tab!-- upload.jsp -- form classlayui-form action/movie/upload methodpost iduploadForm div classlayui-form-item label classlayui-form-label片名/label div classlayui-input-block input typetext nametitle required lay-verifyrequired /div /div button classlayui-btn lay-submit lay-filterupload立即提交/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; // 阻止表单默认提交 }); }); /script3.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_infoid(PK),title,director,actors,type,score,release_year,duration,cover_url,summary,status(0下架/1上架)type用 VARCHAR 存储逗号分隔值如动作,犯罪简化查询status实现软删除避免外键级联删除风险user_favoriteid(PK),user_id,movie_id,create_time联合索引(user_id, movie_id)加速「用户收藏列表」和「影片被收藏次数」查询movie_categoryid(PK),name,sort_ordersort_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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property nameurl valuejdbc:mysql://localhost:3306/movie_db?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value123456/ property nameinitialSize value5/ property nameminIdle value5/ property namemaxActive value20/ property namemaxWait value60000/ property nametimeBetweenEvictionRunsMillis value60000/ property nameminEvictableIdleTimeMillis value300000/ property namevalidationQuery valueSELECT 1/ property nametestWhileIdle valuetrue/ property nametestOnBorrow valuefalse/ property nametestOnReturn valuefalse/ property namepoolPreparedStatements valuetrue/ property namemaxPoolPreparedStatementPerConnectionSize value20/ property namefilters valuestat,wall,log4j/ /bean关键参数说明initialSize5应用启动时创建 5 个空闲连接避免首次请求延迟maxActive20最大连接数需根据 MySQLmax_connections设置如 MySQL 设置为 100则此处设 20 安全validationQuerySELECT 1空闲连接检测 SQL确保连接有效性filtersstat,wall,log4j启用监控统计、防火墙防 SQL 注入、日志输出Druid 监控页面可通过http://localhost:8080/druid访问需在web.xml中配置DruidStatViewServlet实时查看连接池活跃数、SQL 执行耗时、慢 SQL 记录。4.3 Maven 依赖管理中的版本冲突规避策略pom.xml中明确指定各框架版本避免传递依赖引发的兼容性问题properties spring.version5.3.30/spring.version mybatis.version3.4.6/mybatis.version druid.version1.2.16/druid.version mysql.connector.version8.0.33/mysql.connector.version jstl.version1.2/jstl.version /properties dependencies !-- Spring Core -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency !-- MyBatis-Spring 集成 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version1.3.2/version !-- 严格匹配 MyBatis 3.4.x -- /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.connector.version}/version scoperuntime/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:if5.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 ListComment comments; private ListMovie relatedMovies; // getter/setter }JSP 中直接遍历h1${detail.movie.title}/h1 div classcomments c:forEach items${detail.comments} varcomment p${comment.content} —— ${comment.userName}/p /c:forEach /div div classrelated c:forEach items${detail.relatedMovies} varrel a href${pageContext.request.contextPath}/movie/detail?id${rel.id}${rel.title}/a /c:forEach /div5.3 JSP 编译优化启用trimDirectiveWhitespaces减少 HTML 冗余空格在web.xml中配置 JSP 编译选项减少输出 HTML 中的空白字符jsp-config jsp-property-group url-pattern*.jsp/url-pattern trim-directive-whitespacestrue/trim-directive-whitespaces default-pageindex.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 并压缩。本文还有配套的精品资源点击获取