JSP电影页面实战:Servlet+JDBC+MySQL全链路开发与优化

发布时间:2026/10/17 6:20:00

JSP电影页面实战:Servlet+JDBC+MySQL全链路开发与优化 1. 从零拆解一个JSP电影页面为什么老技术依然值得动手做一遍做后端开发的人早晚都会碰到JSP。不是因为它在技术选型里有多时髦而是因为大量存量系统、教学项目、企业内部管理后台至今还跑在JSP上。你打开招聘网站搜“Java开发”十个岗位里有三四个会写“熟悉JSP/Servlet者优先”。这不是怀旧这是现实。我这次要聊的是一个看起来极其普通的项目用JSP做一个电影页面。听起来像是大学课程设计的题目对吧但恰恰是这种“简单”的东西最能暴露一个开发者对Web基础的理解深度。页面怎么组织、数据怎么传递、请求怎么处理、样式怎么复用、分页怎么做、搜索怎么实现——这些问题的答案跟你用React还是Vue没有本质区别区别只在于JSP把很多底层细节暴露给了你。这个电影页面项目适合谁看如果你是刚学完Servlet、准备做第一个完整Web应用的学生这篇内容可以当作你的实操手册如果你是从PHP或ASP转过来的开发者想快速理解JSP的运作方式这里有你需要的对照说明如果你是工作了几年的后端突然被安排维护一个老系统那这篇内容能帮你快速找回手感。我打算把这个项目从头到尾拆一遍。不是给你一个压缩包让你自己看而是把每个决策背后的“为什么”讲清楚。为什么用JSP而不是纯HTML为什么数据要放在request域而不是session为什么分页要用数据库limit而不是前端截取这些选择看起来小但每一个都影响你后面写代码的顺畅程度。先说一下这个电影页面大概长什么样首页有电影列表每个电影显示海报、名称、评分、上映年份点击某个电影能进详情页看到简介、导演、演员、时长顶部有搜索框能按名称模糊查底部有分页导航每页显示固定数量。后台部分我们暂时不展开重点放在前台展示和交互上。技术栈方面我选的是最经典的组合JSP Servlet JDBC MySQL。没有用Spring MVC没有用MyBatis不是因为这些不好而是因为这个项目的目的是让你看清请求从浏览器发出到页面渲染的完整链路。框架帮你封装了太多东西初学阶段反而容易迷糊。等你把这个链路走通了再去用框架你会知道框架到底帮你做了什么。数据库设计我放在后面细说但先给你一个概念至少需要两张表一张存电影基本信息一张存电影的分类或标签。如果你还想做用户评论那就再加一张评论表。表结构不用太复杂但字段类型和索引要提前想好不然后面改起来很烦。提示如果你本地还没装MySQL建议用Docker跑一个一条命令的事比在系统里直接装省心得多。具体命令后面会给出。页面样式我建议用Bootstrap 5直接CDN引入不用自己写太多CSS。电影海报用占位图或者公开的图片链接都行重点是逻辑跑通。不要一上来就纠结UI好不好看先把数据从数据库取出来、显示到页面上、点击能跳转、搜索能过滤这些做到了再回头调样式。我见过太多人做这种项目卡在环境配置上三天然后放弃了。所以第一部分我会把环境准备和项目结构讲得细一点让你少走弯路。后面再逐步深入核心逻辑、分页实现、搜索优化和常见问题排查。整个过程我会穿插我自己踩过的坑和实际调试的经验这些是你在教材上不太容易看到的。2. 项目整体设计与技术选型思路2.1 为什么选择JSP而不是纯前端渲染很多人第一反应是都什么年代了还JSP我直接写个HTML用AJAX从后端拿JSON前端渲染不香吗香但你要看场景。这个项目的定位是“快速搭建一个能跑的电影展示页面”JSP的优势在于服务端直接渲染浏览器拿到的是完整HTML不需要等JS加载完再发请求。对于内容展示型页面这种模式首屏速度更快SEO也更友好。另一个原因是JSP天然和Servlet容器集成。你不需要单独部署一个前端服务不需要处理跨域不需要配置反向代理。一个Tomcat跑起来JSP、Servlet、静态资源全在里面。对于小型项目或者教学演示这种一体化部署省去了大量运维成本。当然JSP也有它的缺点。页面逻辑和Java代码容易混在一起维护起来头疼。所以我在这个项目里会刻意控制JSP只负责展示所有业务逻辑放在Servlet里数据库操作封装在DAO层。这样即使后面要换成Thymeleaf或者前后端分离迁移成本也可控。2.2 分层架构Servlet Service DAO JSP我采用的是经典的四层结构虽然对这个规模的项目来说有点“重”但养成好习惯比省几行代码重要。Servlet层接收请求解析参数调用Service把结果放到request域然后转发到JSP。它不直接碰数据库也不写复杂的业务判断。Service层处理业务逻辑比如“搜索关键词为空时返回全部”、“分页页码越界时自动修正”。这一层是Servlet和DAO之间的缓冲。DAO层封装所有JDBC操作提供findAll、findById、search、count等方法。SQL语句只出现在这里。JSP层只做数据展示用JSTL标签和EL表达式尽量不写scriptlet就是那种% %里面写Java代码的写法。这样分层的好处是当你需要改数据库从MySQL换成PostgreSQL时只需要改DAO层的实现Servlet和JSP完全不用动。当你需要给搜索加一个“按导演筛选”的功能时只需要在Service层加逻辑DAO层加一个方法JSP加一个下拉框。2.3 数据库表结构设计电影页面核心就两张表我先把建表语句给你然后解释每个字段的用意。CREATE TABLE movie ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, director VARCHAR(100), actors VARCHAR(500), release_year INT, rating DECIMAL(3,1), duration INT, poster_url VARCHAR(500), description TEXT, category_id INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL );rating用DECIMAL(3,1)而不是FLOAT是因为评分这种数据不需要浮点数的精度而且DECIMAL在排序和比较时更稳定。duration用分钟数存整数展示时再格式化成“2小时15分”。poster_url存图片链接而不是图片本身数据库只负责存路径图片文件放服务器目录或者对象存储里。category_id是外键关联分类表。我没有在数据库层面加FOREIGN KEY约束因为在高并发写入时外键检查会影响性能而且很多互联网公司规范里也不建议用物理外键。逻辑上的关联由代码保证。索引方面title字段建议加一个普通索引因为搜索功能会频繁用LIKE查询。但注意LIKE %关键词%这种前置模糊匹配是用不上索引的后面我会讲怎么优化。2.4 项目目录结构我习惯把项目组织成下面这样你可以直接照着建movie-project/ ├── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── movie/ │ │ ├── dao/ │ │ │ ├── MovieDao.java │ │ │ └── CategoryDao.java │ │ ├── service/ │ │ │ └── MovieService.java │ │ ├── servlet/ │ │ │ ├── MovieListServlet.java │ │ │ ├── MovieDetailServlet.java │ │ │ └── MovieSearchServlet.java │ │ ├── model/ │ │ │ ├── Movie.java │ │ │ └── Category.java │ │ └── util/ │ │ └── DBUtil.java │ └── webapp/ │ ├── WEB-INF/ │ │ └── web.xml │ ├── css/ │ ├── js/ │ ├── images/ │ ├── index.jsp │ ├── detail.jsp │ └── common/ │ ├── header.jsp │ └── footer.jspcommon目录放公共的头部和底部用% include file... %引入。这样改导航栏只需要改一个文件。WEB-INF下面的web.xml是部署描述符Servlet 3.0以后可以用注解代替但有些老容器还是认web.xml所以我两个都保留。3. 核心细节解析与实操要点3.1 数据库连接池为什么不用DriverManager新手最容易犯的错就是在每个DAO方法里写DriverManager.getConnection()。这样每次请求都新建一个物理连接用完关闭开销极大。我实测过用DriverManager的方式100个并发请求下响应时间从平均50ms飙升到800ms以上。正确的做法是用连接池。Tomcat自带的DBCP或者更现代的HikariCP都可以。我选HikariCP因为它性能好、配置简单。在context.xml里配好数据源然后在DBUtil里通过JNDI查找。Resource namejdbc/movieDB authContainer typejavax.sql.DataSource driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/movie_db?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;characterEncodingutf8 usernameroot passwordyourpassword maxTotal20 maxIdle10 maxWaitMillis10000/maxTotal20表示最多20个活跃连接。这个数字怎么定一个经验公式是连接数 CPU核心数 * 2 磁盘数。对于个人项目20足够了。maxWaitMillis10000表示拿不到连接时最多等10秒超过就抛异常避免请求无限阻塞。DBUtil的代码大概长这样public class DBUtil { private static DataSource dataSource; static { try { Context ctx new InitialContext(); dataSource (DataSource) ctx.lookup(java:comp/env/jdbc/movieDB); } catch (NamingException e) { throw new RuntimeException(数据源初始化失败, e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(AutoCloseable... resources) { for (AutoCloseable res : resources) { if (res ! null) { try { res.close(); } catch (Exception ignored) {} } } } }注意close方法里用了可变参数和AutoCloseable这样你可以一次性关闭ResultSet、PreparedStatement和Connection不用写三层嵌套的try-catch。这是我从实际项目里总结出来的小技巧能省不少代码。3.2 电影列表查询SQL怎么写才高效列表页需要查电影的基本信息同时关联分类名称。我一开始写的是子查询SELECT m.*, (SELECT name FROM category WHERE id m.category_id) AS category_name FROM movie m ORDER BY m.created_at DESC LIMIT ?, ?后来改成LEFT JOIN因为子查询在数据量大时会被执行多次而JOIN只扫一次分类表SELECT m.*, c.name AS category_name FROM movie m LEFT JOIN category c ON m.category_id c.id ORDER BY m.created_at DESC LIMIT ?, ?LIMIT ?, ?的两个参数分别是偏移量和每页条数。偏移量 (当前页码 - 1) * 每页条数。比如第3页每页12条偏移量就是24。这里有个坑ORDER BY created_at DESC如果created_at没有索引当电影数量到几万条时排序会非常慢。所以我在created_at上加了索引。另外如果两篇文章的created_at相同排序结果可能不稳定导致分页时出现重复或遗漏。解决办法是再加一个id DESC作为第二排序字段。3.3 搜索功能的实现与优化搜索框的交互逻辑是用户输入关键词点击搜索页面显示匹配的电影。后端用LIKE查询SELECT m.*, c.name AS category_name FROM movie m LEFT JOIN category c ON m.category_id c.id WHERE m.title LIKE CONCAT(%, ?, %) ORDER BY m.rating DESC LIMIT ?, ?CONCAT(%, ?, %)是MySQL的写法用PreparedStatement传参能防止SQL注入。但前面说了前置%会让索引失效。怎么优化方案一如果搜索频率不高数据量在几千条以内全表扫描可以接受不用优化。方案二用全文索引。MySQL 5.7以上支持FULLTEXT索引但中文分词需要配置ngram解析器。配置命令ALTER TABLE movie ADD FULLTEXT INDEX ft_title (title) WITH PARSER ngram;然后查询改成SELECT * FROM movie WHERE MATCH(title) AGAINST(? IN BOOLEAN MODE);方案三如果项目规模再大应该引入Elasticsearch或者Solr。但那是另一个话题了这个项目里用方案一就够了。实操心得搜索关键词要做trim处理去掉首尾空格。如果用户输入的是空字符串直接返回全部列表不要拼LIKE %%虽然结果一样但语义不清晰。另外关键词里的%和_要转义否则用户输入%会匹配所有记录。转义方法是在LIKE后面加ESCAPE \然后把关键词里的%替换成\%。3.4 分页逻辑的完整实现分页需要两个数据当前页的电影列表和总记录数。总记录数用来算总页数。public class PageResultT { private ListT list; private int totalCount; private int pageSize; private int currentPage; public int getTotalPages() { return (totalCount pageSize - 1) / pageSize; } public boolean hasPrev() { return currentPage 1; } public boolean hasNext() { return currentPage getTotalPages(); } }(totalCount pageSize - 1) / pageSize是向上取整的经典写法。比如总数25每页12结果是3页。Servlet里这样处理int page 1; String pageParam request.getParameter(page); if (pageParam ! null !pageParam.isEmpty()) { try { page Integer.parseInt(pageParam); } catch (NumberFormatException e) { page 1; } } if (page 1) page 1; int pageSize 12; int totalCount movieService.countAll(); int totalPages (totalCount pageSize - 1) / pageSize; if (page totalPages totalPages 0) page totalPages; ListMovie movies movieService.findByPage(page, pageSize); request.setAttribute(movies, movies); request.setAttribute(currentPage, page); request.setAttribute(totalPages, totalPages); request.getRequestDispatcher(/index.jsp).forward(request, response);注意if (page totalPages totalPages 0) page totalPages;这一行。如果用户手动在URL里把page改成999不处理的话会查到空列表页面显示空白。自动修正到最后一页体验更好。JSP里的分页导航nav ul classpagination li classpage-item ${currentPage 1 ? disabled : } a classpage-link href?page${currentPage - 1}上一页/a /li c:forEach begin1 end${totalPages} vari li classpage-item ${i currentPage ? active : } a classpage-link href?page${i}${i}/a /li /c:forEach li classpage-item ${currentPage totalPages ? disabled : } a classpage-link href?page${currentPage 1}下一页/a /li /ul /nav如果总页数很多比如100页全部显示会撑爆页面。这时候需要做“省略号分页”只显示当前页附近的几个页码。这个逻辑可以放在JSP里用c:choose判断也可以在后端算好一个页码范围传过来。我倾向于后端算因为JSP里写复杂逻辑容易乱。4. 实操过程与核心环节实现4.1 环境准备从零到跑起来先确保你本地有JDK 8或以上、Maven、MySQL。我用Docker起MySQL命令如下docker run -d --name mysql-movie \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDyourpassword \ -e MYSQL_DATABASEmovie_db \ mysql:8.0 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ciutf8mb4而不是utf8因为utf8在MySQL里最多存3字节存不了emoji和一些生僻字。utf8mb4才是真正的UTF-8。然后建表、插入测试数据。测试数据我建议至少插20条这样分页效果才明显。可以用存储过程批量插入也可以直接写INSERT语句。我贴几条示例INSERT INTO category (name) VALUES (动作), (喜剧), (科幻), (剧情); INSERT INTO movie (title, director, actors, release_year, rating, duration, poster_url, description, category_id) VALUES (示例电影一, 导演A, 演员甲,演员乙, 2023, 8.5, 120, https://example.com/poster1.jpg, 这是一部关于勇气与选择的剧情片。, 4), (示例电影二, 导演B, 演员丙,演员丁, 2022, 7.8, 105, https://example.com/poster2.jpg, 一场跨越时空的科幻冒险。, 3), (示例电影三, 导演C, 演员戊,演员己, 2024, 9.1, 135, https://example.com/poster3.jpg, 笑中带泪的喜剧故事。, 2);海报链接用公开的占位图服务比如https://picsum.photos/300/450?random1这样每张图不一样看起来真实。Maven依赖只需要三个javax.servlet-api、jstl、mysql-connector-java。scope记得设成provided因为Tomcat自带Servlet API打包时不需要带进去。dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies4.2 电影列表页的完整实现MovieListServlet的doGet方法protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); int page parsePage(request.getParameter(page)); int pageSize 12; MovieService service new MovieService(); int totalCount service.countAll(); int totalPages (totalCount pageSize - 1) / pageSize; if (totalPages 0 page totalPages) page totalPages; ListMovie movies service.findByPage(page, pageSize); ListCategory categories service.findAllCategories(); request.setAttribute(movies, movies); request.setAttribute(categories, categories); request.setAttribute(currentPage, page); request.setAttribute(totalPages, totalPages); request.setAttribute(totalCount, totalCount); request.getRequestDispatcher(/index.jsp).forward(request, response); }index.jsp的核心部分% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % !DOCTYPE html html head title电影列表/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/css/bootstrap.min.css relstylesheet /head body % include filecommon/header.jsp % div classcontainer mt-4 div classrow c:forEach items${movies} varmovie div classcol-md-3 mb-4 div classcard h-100 img src${movie.posterUrl} classcard-img-top alt${movie.title} styleheight:300px;object-fit:cover; div classcard-body h5 classcard-title${movie.title}/h5 p classcard-text text-muted ${movie.releaseYear} · ${movie.director} /p span classbadge bg-warning text-dark fmt:formatNumber value${movie.rating} pattern#.#/分 /span /div div classcard-footer a hrefdetail?id${movie.id} classbtn btn-sm btn-primary查看详情/a /div /div /div /c:forEach /div c:if test${empty movies} div classalert alert-info没有找到符合条件的电影。/div /c:if % include filecommon/pagination.jsp % /div % include filecommon/footer.jsp % /body /htmlfmt:formatNumber用来格式化评分pattern#.#保证显示一位小数。object-fit:cover让海报图片保持比例填充不会变形。4.3 详情页的数据传递与展示详情页通过URL参数id来查具体电影。MovieDetailServletprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String idParam request.getParameter(id); if (idParam null || idParam.isEmpty()) { response.sendRedirect(list); return; } try { int id Integer.parseInt(idParam); MovieService service new MovieService(); Movie movie service.findById(id); if (movie null) { response.sendError(HttpServletResponse.SC_NOT_FOUND, 电影不存在); return; } request.setAttribute(movie, movie); request.getRequestDispatcher(/detail.jsp).forward(request, response); } catch (NumberFormatException e) { response.sendRedirect(list); } }detail.jsp里展示演员列表时actors字段存的是逗号分隔的字符串。可以用JSTL的fn:split拆成数组再遍历% taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions % ... c:forEach items${fn:split(movie.actors, ,)} varactor span classbadge bg-secondary${actor}/span /c:forEach时长格式化c:set varhours value${movie.duration / 60} / c:set varminutes value${movie.duration % 60} / 时长fmt:formatNumber value${hours} pattern# /小时${minutes}分钟${movie.duration / 60}在EL里会自动做浮点除法fmt:formatNumber的pattern#取整数部分。${movie.duration % 60}取余数。4.4 搜索与分页的联动搜索和分页要能同时工作。比如用户搜索“科幻”然后点第2页URL应该是search?keyword科幻page2。分页链接里要带上关键词参数。在JSP里拼分页链接时需要判断当前是列表页还是搜索页。我通常传一个baseUrl到request域String baseUrl list?; if (keyword ! null !keyword.isEmpty()) { baseUrl search?keyword URLEncoder.encode(keyword, UTF-8) ; } request.setAttribute(baseUrl, baseUrl);然后分页组件里用${baseUrl}page${i}。这样列表页和搜索页共用同一个分页组件不用写两套。注意URLEncoder.encode对中文关键词编码后URL里会出现%E7%A7%91%E5%B9%BB这样的字符。这是正常的浏览器会自动解码。但如果你在JSP里直接拼链接记得用c:url标签它会自动处理编码c:url value/search varsearchUrl c:param namekeyword value${keyword} / c:param namepage value${i} / /c:url a href${searchUrl}${i}/ac:url比手动拼字符串安全得多尤其是关键词里包含特殊字符时。5. 常见问题与排查技巧实录5.1 中文乱码从请求到响应全链路排查中文乱码是JSP项目里出现频率最高的问题。乱码可能出现在三个环节请求参数、数据库存储、页面输出。请求参数乱码对于POST请求在Servlet里第一行加request.setCharacterEncoding(UTF-8)。对于GET请求Tomcat 8以后默认URI编码是UTF-8一般没问题。但如果你的Tomcat版本较老需要在server.xml的Connector标签里加URIEncodingUTF-8。数据库乱码建库建表时用utf8mb4连接URL里加characterEncodingutf8。注意MySQL 8的驱动类名是com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver。页面输出乱码JSP文件头部加% page contentTypetext/html;charsetUTF-8 languagejava %。HTML的meta标签里也声明charsetUTF-8。排查顺序先看浏览器控制台里响应头的Content-Type有没有charsetUTF-8再看数据库里的数据是不是正常最后看Java代码里有没有设置编码。三步定位基本不会漏。5.2 数据库连接泄漏怎么发现和解决连接泄漏的表现是系统跑一段时间后所有请求都卡住日志里出现“等待连接超时”。原因是某个DAO方法拿了连接但没关闭。排查方法在DBUtil.close()里加日志记录每次关闭的连接。如果发现连接数只增不减说明有地方漏了。更彻底的办法是用连接池的监控功能HikariCP可以配置leakDetectionThreshold60000表示连接超过60秒没关闭就打印警告日志。预防措施所有数据库操作都用try-with-resources写法try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { // 处理结果 } catch (SQLException e) { throw new RuntimeException(查询失败, e); }try-with-resources会自动关闭资源不用手动写finally块。这是Java 7以后的标准写法比老式的finally里嵌套关闭清爽得多。5.3 分页页码越界与空结果处理用户手动改URL参数是常有的事。page0、page-1、pageabc、page99999这些都要处理。我的处理策略是输入情况处理方式非数字默认第1页小于1修正为第1页大于总页数修正为最后一页总记录数为0显示空状态提示不显示分页空状态提示不要只写“暂无数据”最好给一个引导比如“没有找到相关电影试试其他关键词”。用户体验会好很多。5.4 JSP页面报错常见异常速查异常信息可能原因解决方法java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动包没引入检查Maven依赖或WEB-INF/libjavax.naming.NameNotFoundException: jdbc/movieDBJNDI名称不对检查context.xml和lookup名称是否一致org.apache.jasper.JasperException: /index.jsp (line: X)JSP语法错误看行号定位常见是标签未闭合java.sql.SQLException: Access denied数据库账号密码错检查连接URL里的用户名密码NumberFormatException参数转数字失败用try-catch包裹给默认值实操心得JSP报错时Tomcat的日志在logs/localhost.YYYY-MM-DD.log里比浏览器上显示的堆栈更详细。养成看日志的习惯能省很多猜测时间。5.5 性能优化从页面加载3秒到300毫秒我最初版本没加连接池、没加索引、每次查询都SELECT *页面加载要3秒多。后来做了几件事加连接池从DriverManager换成HikariCP响应时间降到1.2秒。加索引在created_at和category_id上加索引降到800毫秒。只查需要的字段列表页不需要descriptionSQL里去掉这个字段降到500毫秒。分页查询优化用LIMIT而不是查全部再截取降到300毫秒。静态资源缓存给CSS和JS加Cache-Control头二次加载更快。每一步的优化效果我都实测过不是拍脑袋。你可以用Chrome的Network面板看每个请求的耗时定位瓶颈在哪。5.6 安全方面SQL注入与XSS防护PreparedStatement能防SQL注入这个必须用。但XSS跨站脚本容易被忽略。如果电影标题里包含scriptalert(1)/script直接输出到JSP里就会执行。防护方法是用c:out标签输出c:out value${movie.title} /c:out默认会转义HTML特殊字符。或者用fn:escapeXml函数${fn:escapeXml(movie.title)}对于富文本内容比如电影简介里可能有换行不能直接转义需要先做HTML过滤只允许安全的标签。这个项目里简介是纯文本用c:out就够了。6. 页面样式与交互细节打磨6.1 响应式布局从手机到桌面都能看Bootstrap的栅格系统用col-md-3表示中等屏幕以上每行4个小屏幕自动变成每行1个或2个。我实测下来col-6 col-md-4 col-lg-3的组合比较合适手机每行2个平板每行3个桌面每行4个。海报图片用aspect-ratio固定比例避免不同尺寸的图片把卡片撑得高低不一.card-img-top { aspect-ratio: 2 / 3; object-fit: cover; }aspect-ratio: 2 / 3是电影海报的常见比例。object-fit: cover保证图片填满区域且不变形。6.2 加载状态与空状态处理数据从数据库查出来需要时间虽然通常很快但网络慢的时候用户会看到白屏。加一个简单的加载提示div idloading classtext-center py-5 div classspinner-border text-primary rolestatus span classvisually-hidden加载中.../span /div /div页面加载完成后用JS隐藏。不过JSP是服务端渲染HTML返回时数据已经在了这个加载提示主要是给AJAX请求用的。如果搜索功能用AJAX实现就需要这个。空状态我放了一个图标加文字提示比单纯一行字好看c:if test${empty movies} div classtext-center py-5 text-muted p classfs-4没有找到相关电影/p p试试其他关键词或者a hreflist查看全部电影/a/p /div /c:if6.3 搜索框的交互优化搜索框加一个清除按钮用户点一下就能清空内容。用一点CSS和JS就能实现div classinput-group input typetext idkeyword namekeyword classform-control placeholder搜索电影名称... value${fn:escapeXml(keyword)} button classbtn btn-primary typesubmit搜索/button /divvalue${fn:escapeXml(keyword)}让搜索后关键词保留在输入框里用户不用重新输入。fn:escapeXml防止关键词里的特殊字符破坏HTML结构。小技巧搜索框加autocompleteoff可以关闭浏览器的自动补全下拉避免遮挡搜索结果。但如果你希望用户能快速选择历史搜索词可以保留autocompleteon看具体需求。7. 项目扩展方向与个人经验总结这个电影页面做完基础功能后还有很多可以扩展的地方。比如加用户评论功能需要新增评论表在详情页加评论列表和提交表单。评论提交用POST请求处理完重定向回详情页避免刷新重复提交。再比如加评分功能用户点星星打分用AJAX提交到后端更新电影的平均分。这需要在前端用JS监听点击事件后端提供RESTful接口返回JSON。还可以加“猜你喜欢”推荐根据用户浏览过的电影分类推荐同分类的其他电影。简单实现就是查同category_id的电影按评分排序取前4个。我在实际做这个项目的过程中最大的体会是不要一开始就追求完美。先把数据从数据库取出来显示到页面上这一步跑通了再加分页再加搜索再加样式。每一步都验证一下出问题容易定位。如果一次性写几百行代码再运行报错了你都不知道从哪查起。另一个体会是日志和异常处理要趁早加。我一开始图省事DAO层的方法直接throws SQLExceptionServlet层也不捕获结果页面报500错误什么信息都没有。后来在Servlet的doGet里加了try-catch把异常信息打到日志里同时在页面上显示友好的错误提示排查效率高了很多。最后分享一个调试JSP的小技巧在JSP里临时加% out.println(调试信息 someVariable); %可以直接在页面上看到变量的值。虽然不优雅但比断点调试快。调试完记得删掉不然上线就尴尬了。这个项目虽然简单但把Web开发的核心链路都串了一遍请求接收、参数解析、业务处理、数据库操作、页面渲染。你把这套流程走通了再去学Spring Boot、MyBatis会发现它们只是把每一步封装得更优雅底层逻辑是一样的。基础打牢了学什么都快。
延伸阅读

更多相关文章

2026/10/17 6:20:00

H5页面实现扫码功能:跨端兼容与性能优化实战

1. 项目概述:为什么H5页面扫码不是“加个按钮就完事”“h5页面实现扫码功能”这八个字,表面看是前端一个常规需求,但实际落地时,我见过太多团队踩进同一个坑:以为调用navigator.mediaDevices.getUserMedia拿到摄像头流…

2026/10/17 6:15:00

合成大西瓜H5魔改全流程:物理参数调优与静态部署实战

合成大西瓜这个项目,前两年火得一塌糊涂的时候我帮朋友魔改过一个端午粽子版,从改素材到部署上线前后折腾了一周多,踩了不知道多少坑。后来陆陆续续又给不同需求改过好几个版本,水仙版、万圣节版、甚至帮某公司做了一个内部团建用…

2026/10/17 7:40:03

基于MATLAB的WCDMA小区搜索三步法仿真详解

简介:面向无线通信学习者和 MATLAB 仿真开发者的 WCDMA 小区搜索代码,聚焦移动终端接入网络时的 P-SCH/S-SCH 同步与小区识别,同时包含 PCPICH 搜索、下行链路生成、扰码、脉冲成形、浮点转定点及结果对比等配套实现。压缩包共 16 个文件&…

2026/10/17 7:40:03

AI技能包开发全指南:从设计到排错,沉淀可复用能力

最近这一个月,我没干别的,就专心折腾一件事:把散落在各种对话里的临时任务,变成一套可以被AI助手按需调用的技能包。所谓技能包,你可以理解成给大模型外挂的一组说明书、脚本和参考数据,用户发起对应需求时…

2026/10/17 7:40:03

医院药品管理系统全栈实战:SpringBoot+Vue+MySQL从0到1开发详解

医院药品管理系统从0到1:SpringBootVueMySQL全栈源码拆解与落地笔记最近刚把手头一套医院药品管理系统(HIS药品管理子系统)的全栈源码整理完毕,技术栈是SpringBoot后端Vue前端MySQL数据库,结构清晰、可以直接跑起来。前…

2026/10/17 7:40:03

RetinaNet实战:校园建筑物识别与分类的工程要点

我先把话放前面:校园建筑物识别与分类这个方向,看着像纯计算机视觉任务,真做起来却是一个要同时处理数据采集、标注规范、目标检测选型、训练调参、部署集成的综合工程。我们用RetinaNet做了一套校园建筑物识别与分类系统,识别对象…

2026/10/17 7:40:03

面试算法题集中训练:六道经典题解析与实战技巧

1. 面试算法题训练的整体思路拆解1.1 为什么选择集中刷题而不是零散练习很多同学准备面试时习惯今天刷一道明天刷一道,看起来每天都在学,实际上效率很低。我自己踩过这个坑,后来发现集中刷同一家公司的题目效果要好得多。原因很简单&#xff…

2026/10/17 7:35:03

如何利用查找我的iPhone找回手机?从关机追踪到超宽带精确定位

说实话,我以前对“查找我的iPhone”这功能一直停留在“就是看看朋友在哪”的认知上。直到前两年自己真丢过一次手机:打车回家才发现手机落在后座,打电话过去已经关机,地图上那个橘色圆点从下午六点开始就再没动过。那十几个小时真…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑