JavaWeb小型音乐网站实战:Servlet+JSP+MySQL全程解析

发布时间:2026/9/29 18:50:28
JavaWeb小型音乐网站实战:Servlet+JSP+MySQL全程解析 简介这是一份基于JAVAWEB的小型音乐网站完整项目适合JavaWeb初学者、课程设计或毕业设计参考。项目在Eclipse环境下配合Tomcat、MySQL和jdk构建实现了音乐在线播放、下载、分类、排行等核心功能涵盖用户交互、数据存储与前后端联调的典型开发流程。压缩包共881个文件约189.75MB其中包含80个jsp动态页面、60个java后台源码、44个jar依赖库以及大量png/jpg图片、gif动画、css/js前端样式脚本和xml/sql等配置资源另有23个mp3示例音频解压后即可用于测试与演示。资源内的war包可直接放入Tomcat运行省去环境配置环节完整源码与依赖jar包均已打包既可整体部署也可将其中某个模块抽取出来作为参考。已有915人学习下载对于希望快速上手一个可运行的JavaWeb项目并深入理解其分层结构的学习者而言是一份兼具实用性与参考价值的实践素材。1. 用 JAVAWEB 做一个小型音乐网站先想清楚它到底要证明什么很多 javaweb 项目完整案例要么直接端出 Spring Boot 全家桶要么只停在 CRUD真正动手时才发现 JSP、Servlet、JDBC 之间的分工并不简单。小型音乐网站的好处是数据表不超过十张业务链却很完整建库建表、封装 JDBC、Servlet 接收参数、JSP 渲染歌曲列表、点歌后累计播放次数。让 IDEA 把项目跑起来你能看到 MySQL 里的记录被查询出来最终呈现为页面上一行行歌名和播放按钮。它适合刚学完 Java 基础、手里只有黑马 javaweb 笔记零散片段的新手也适合需要快速提交课程设计或内部小后台的开发者。下文按落地顺序展开先讲选型再落库再写 Servlet 与 JSP最后集中讲最容易翻车的地方。2. 技术选型与工程骨架ServletJSP 为什么值得再跑一遍2.1 直接上 Spring Boot先看“小型”二字需要什么标题里的 JAVAWEB通常指 Servlet JSP JDBC 这套经典组合部署在 Tomcat 上。动手之前要接受一个定位小型不是简陋而是“范围小、链路完整”。单机可部署、数据量在十万级以下、日请求量不大、功能以歌单浏览、关键字搜索、点击播放和收藏记录为核心这就是典型的使用场景。在这个规模下Spring Boot 的自动配置与约定大于配置并不能帮你省下太多事反而会盖住几个关键动作一次 HTTP 请求是怎么被 Servlet 接住的JDBC 连接是在哪个环节创建和关闭的JSP 页面里为什么不能随便写 Java 代码。很多人在简历里写熟悉全家桶却答不出doGet和doPost的边界问题就出在第一步用了太顺手的框架。我的建议很直接如果是毕业设计、课程设计或者想彻底弄懂“请求怎么从前端走到数据库再走回来”原生 ServletJSP 是性价比最高的路线。如果已经做过完整案例只是想快速验证一个音乐站点子那就别犹豫直接上 Spring Boot本文的建表和分层思路同样能复用到那边。不要在原生 Servlet 阶段去模仿 Spring MVC 的注解驱动风格容易半懂不懂。那具体用什么组合Java 8、Tomcat 8.5 或 9、MySQL 5.7 或 8.0、Maven 管理依赖这是最稳的一套。Tomcat 9 对应 Servlet 4.0JSP 和 EL 表达式都自带不需要额外引入 JSP 引擎。2.2 用 IDEA 运行 javaweb 项目配置从 pom 到 Tomcat 的五步清单很多人卡在第一步代码写好了IDEA 里不会配置 Tomcat一直报 404 或者端口占用。先把 Maven 依赖固定下来避免后面因为版本漂移浪费时间。properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties 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 /dependenciesjavax.servlet-api的 scope 设为provided是因为 Tomcat 自己带了一份 Servlet 实现打包时不需要重复打进去jstl用来在 JSP 里做循环和条件渲染必须带上MySQL 驱动没有特殊理由就不要设provided否则部署时 Tomcat 找不到驱动类。项目目录我习惯按标准 Maven 结构拆music-web/ ├── pom.xml ├── src/main/java/com/example/music │ ├── controller/ # Servlet只做参数接收与页面转发 │ ├── service/ # 业务逻辑比如播放计数 │ ├── dao/ # JDBC 数据访问 │ └── entity/ # Song、Singer、User 等实体 ├── src/main/resources/ └── src/main/webapp ├── index.jsp ├── static/ │ ├── css/ │ └── audio/ └── WEB-INF └── jsp/ # 不允许浏览器直接访问的页面注意WEB-INF目录的特殊性放在它下面的 JSP 不能通过 URL 直接访问必须由 Servlet 用forward转发进去这样页面路径不会暴露给用户也防止有人绕过入口直接打开视图页。接着是 IDEA 运行 javaweb 项目配置操作顺序如下打开 Project Structure确认 Artifact 类型是war exploded这能让 Tomcat 直接加载当前编译输出改完代码不用重新打 war。打开 Run 菜单的 Edit Configurations点左上角加号选 Tomcat Server再选 Local。在 Server 页的 Application server 里指向你本地解压的 Tomcat 目录。切到 Deployment 页点加号选 Artifact选择music-web:war explodedApplication context 填/music。点 Apply 后直接运行浏览器访问http://localhost:8080/music/。如果 8080 被占用可以在 Tomcat 的conf/server.xml里把 Connector 端口改掉但我更推荐找到占用端口的进程关掉它否则课程设计答辩时换个环境又报错。运行后没自动弹浏览器也没关系Tomcat Localhost Log 里出现Server startup in xxx ms就说明启动成功了。2.3 webapp 目录下的第一个可访问页面在正式写代码前先做一个能访问的首页验证环境。webapp/index.jsp里放一个最简单的入口% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 % html head title小型音乐网站/title /head body h2小型音乐网站/h2 a href${pageContext.request.contextPath}/songs进入歌曲列表/a /body /html${pageContext.request.contextPath}会动态输出当前应用上下文前缀也就是/music。如果直接在链接里写死/songs当部署时上下文改成别的名字所有链接都会 404。第一个页面跑通之后整个链路的校验标准就明确了index.jsp只是静态验证真正要看的下一步是数据库里的歌曲能不能被 JDBC 查出来并渲染到页面上。所以下一步落库。3. 数据层设计MySQL 建表与 JDBC 分页查询怎么写才不虚3.1 三张表定下音乐站的边界歌曲、歌手、收藏小型音乐网站不需要堆二十张表。我一般只保留三张核心表歌手表、歌曲表、收藏表。用户表看需求如果没有登录和收藏功能甚至可以暂时不放但歌曲和歌手必须拆开因为列表页要显示歌手名点击歌手还能看这个歌手下的所有歌。CREATE DATABASE IF NOT EXISTS music_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE music_db; CREATE TABLE singer ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, avatar_url VARCHAR(255) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE song ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, singer_id INT NOT NULL, album_name VARCHAR(100) DEFAULT , duration INT DEFAULT 0 COMMENT 时长单位秒, play_count INT DEFAULT 0, file_url VARCHAR(255) NOT NULL COMMENT 音频文件名存相对路径, cover_url VARCHAR(255) DEFAULT , status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_singer (singer_id), KEY idx_play_count (play_count), CONSTRAINT fk_song_singer FOREIGN KEY (singer_id) REFERENCES singer(id) ) ENGINEInnoDB; CREATE TABLE favorite ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, song_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_song (user_id, song_id), KEY idx_song (song_id) ) ENGINEInnoDB;数据库字符集选utf8mb4而不是utf8因为utf8在 MySQL 里最多存 3 字节遇到某些特殊字符会报错utf8mb4是超集能覆盖常见生僻字和表情符号。duration单位用秒整数在 Java 里对应Integer不要用字符串存时间play_count初始为 0不设默认值的话每次插入都要显式写容易漏。status字段是做下架功能用的SQL 查询条件里永远带着status 1这样将来要下线一首歌不用删数据。外键要不要加我的结论是加。虽然外键会让批量删除变麻烦但小型项目的价值在于数据的正确性而不是删除效率。插入歌曲前先确认singer_id在歌手表里存在数据库才能约束住脏数据。3.2 JDBC 工具类与 DAO把连接关闭做成习惯处理 JDBC 时新手最容易把DriverManager.getConnection()写在每个 DAO 方法里然后忘记关连接。我习惯先写一个静态工具类把驱动加载、连接获取、资源关闭全部收拢public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/music_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver老项目的com.mysql.jdbc.Driver在新版本里会直接报错。URL 里的serverTimezoneAsia/Shanghai也不能省否则驱动读取数据库时间时可能抛出The server time zone value异常。关闭资源的顺序要先 ResultSet、再 Statement、再 Connection不能反过来。close方法里用ignored变量接收异常是故意为之关闭时报错通常不影响业务但会掩盖真正的查询异常所以回调日志里看一眼即可。接着写实体类Song它对应数据库里 song 表的一行记录public class Song { private Integer id; private String title; private Integer singerId; private String singerName; // 关联查询出来的冗余字段 private Integer duration; private Integer playCount; private String fileUrl; private Integer status; // getter/setter 由 IDE 自动生成 }这里把singerName直接放进Song实体而不是再套一层Singer对象。为什么歌曲列表页需要同时显示歌名和歌手名如果为了纯面向对象再包一层JSP 里取值就会写song.singer.name稍微不留神就空指针。小型项目里查询结果直接扁平化成页面需要的样子是最省力的做法。3.3 搜索和分页 SQLpageNo、pageSize 与 OFFSET 的计算歌曲列表不可能一次性查全部尤其搜索功能上线后数据量会明显增长。分页参数我用pageNo和pageSize前端只传这两个值后端返回一个PageBeanpublic class PageBean { private int pageNo; // 当前页从 1 开始 private int pageSize; // 每页条数 private int total; // 符合条件总记录数 private ListSong list; public int getTotalPage() { return (total pageSize - 1) / pageSize; } // getter/setter 省略 }getTotalPage用(total pageSize - 1) / pageSize而不是total / pageSize这样总数为 10、每页 3 条时能算出 4 页而不是 3 页。这个边界细节是分页翻不到最后一页的常见原因。DAO 层写搜索方法时我把统计数量和列表查询放在同一个连接里执行避免两次申请连接public PageBean selectByKeyword(String keyword, int pageNo, int pageSize) { PageBean pb new PageBean(); String baseSql FROM song s LEFT JOIN singer si ON s.singer_id si.id WHERE s.status 1 ; String whereSql ; String like % keyword.trim() %; if (keyword ! null !keyword.trim().isEmpty()) { whereSql AND s.title LIKE ? ; } String countSql SELECT COUNT(*) baseSql whereSql; String listSql SELECT s.id, s.title, s.duration, s.play_count, si.name AS singer_name baseSql whereSql ORDER BY s.play_count DESC LIMIT ? OFFSET ?; try (Connection conn DBUtil.getConnection()) { try (PreparedStatement ps conn.prepareStatement(countSql)) { if (whereSql.length() 0) { ps.setString(1, like); } try (ResultSet rs ps.executeQuery()) { if (rs.next()) { pb.setTotal(rs.getInt(1)); } } } try (PreparedStatement ps conn.prepareStatement(listSql)) { int idx 0; if (whereSql.length() 0) { ps.setString(idx, like); } ps.setInt(idx, pageSize); ps.setInt(idx, (pageNo - 1) * pageSize); // 执行查询并封装为 ListSong } } catch (SQLException e) { throw new RuntimeException(查询歌曲失败, e); } return pb; }注意列表 SQL 用的是LIMIT ? OFFSET ?而不是LIMIT ?, ?。前者在 MySQL 里语义更清晰offset 的计算公式是(pageNo - 1) * pageSize页数从 1 开始第 1 页的 offset 是 0。搜索关键字放在LIKE的右侧这在小型项目里够用将来数据量大了再考虑全文索引但千万别做“先全表查出所有歌曲再到内存里 filter”的事。分页查询里最容易出现的问题不是 SQL 写错而是keyword传了空字符串。我在代码里用keyword.trim().isEmpty()做判断就是为了让“用户只点了搜索按钮但没输入任何字”的场景退化成不带条件的普通列表而不是查出一个空结果页。4. 请求处理与页面渲染Servlet 到 JSP 的最小可播放闭环4.1 SongServlet接收参数、装配数据、转发到视图Servlet 的作用只有一个接收 HTTP 参数调用 Service 层拿到数据再把数据放进 request转发给 JSP。不要在这里拼接 HTML也不要直接写数据库操作。WebServlet(/songs) public class SongListServlet extends HttpServlet { private SongService songService new SongService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); String keyword req.getParameter(keyword); int pageNo parseInt(req.getParameter(pageNo), 1); int pageSize parseInt(req.getParameter(pageSize), 10); PageBean pb songService.search(keyword, pageNo, pageSize); req.setAttribute(pageBean, pb); req.setAttribute(keyword, keyword null ? : keyword); req.getRequestDispatcher(/WEB-INF/jsp/song_list.jsp).forward(req, resp); } private int parseInt(String value, int defaultValue) { if (value null || value.trim().isEmpty()) { return defaultValue; } try { return Integer.parseInt(value); } catch (NumberFormatException e) { return defaultValue; } } }注意这里用的是forward而不是sendRedirect。forward是服务器内部跳转浏览器地址栏仍然是/songsrequest 对象里的pageBean能直接传给 JSPsendRedirect会发起第二次请求之前的setAttribute全部丢失适合用在“提交成功后再跳转”的场景。parseInt这个私有方法很关键。用户直接在地址栏输入/songs?pageNoabc时Integer.parseInt会抛异常整个页面 500。兜底逻辑让非法参数退回默认值这是对外提供完整功能的基本防护。Service 层在这里做最简单的一件事把 DAO 返回的PageBean补全public PageBean search(String keyword, int pageNo, int pageSize) { return songDao.selectByKeyword(keyword, pageNo, pageSize); }那播放计数和收藏为什么不直接放 SongService 里可以放但你要先明确一件事计数是“写操作”必须走 POST不能放在doGet里因为用户可以随手刷新地址栏触发重复计数。这也是 4.3 节要解决的问题。4.2 JSP JSTL 渲染列表页面里不再写 Java 代码JSP 最忌讳的就是% for (...) { %满屏乱飞。用 JSTL 的c:forEach渲染列表既能保持页面结构清晰又避免把业务逻辑暴露给前端。% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head meta charsetUTF-8 title歌曲列表/title link relstylesheet href${pageContext.request.contextPath}/static/css/style.css /head body form action${pageContext.request.contextPath}/songs methodget input typetext namekeyword value${keyword} placeholder输入歌名搜索 button typesubmit搜索/button /form table tr th歌名/th th歌手/th th时长/th th播放次数/th th操作/th /tr c:forEach items${pageBean.list} varsong tr td${song.title}/td td${song.singerName}/td td${song.duration}秒/td td${song.playCount}/td td form action${pageContext.request.contextPath}/play methodpost styledisplay:inline input typehidden namesongId value${song.id} button typesubmit播放并计数/button /form /td /tr /c:forEach /table c:if test${pageBean.pageNo 1} a href${pageContext.request.contextPath}/songs?pageNo${pageBean.pageNo - 1}keyword${keyword}上一页/a /c:if span第 ${pageBean.pageNo} / ${pageBean.totalPage} 页/span c:if test${pageBean.pageNo pageBean.totalPage} a href${pageContext.request.contextPath}/songs?pageNo${pageBean.pageNo 1}keyword${keyword}下一页/a /c:if /body /htmlvalue${keyword}这行很多人会漏写导致搜索一次之后输入框内容被清空用户体验很差。分页链接里拼上keyword${keyword}是为了让下一页仍然带着搜索条件否则翻到第二页就变成普通列表了。还有一个细节播放按钮我用了一个内联 form 包起来而不是a标签。因为你不能用一个 GET 链接去完成写操作用a标签意味着爬虫或者预读插件都可能触发播放计数。form 以 POST 提交就能避免这个隐患。4.3 播放计数与收藏POST 与 redirect 的正确位置播放计数必须单独写一个 Servlet 来处理。我把它分成两步第一步 POST 更新数据库里的play_count第二步重定向到歌曲详情页详情页里放一个audio标签真正播放音乐。WebServlet(/play) public class PlayServlet extends HttpServlet { private SongService songService new SongService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding(UTF-8); String songId req.getParameter(songId); if (songId null || songId.trim().isEmpty()) { resp.sendRedirect(req.getContextPath() /songs); return; } int id Integer.parseInt(songId); songService.increasePlayCount(id); resp.sendRedirect(req.getContextPath() /song/detail?songId id); } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } }doGet里直接返回 405目的就是让浏览器地址栏访问/play时拿不到任何操作结果。这样处理之后播放计数的流程变成用户点“播放并计数”按钮浏览器发起 POST/play。后端更新play_count加 1。后端sendRedirect到/song/detail?songId3。详情页是一个 GET 请求把歌曲信息和audio标签渲染出来。这个设计最大的好处是刷新详情页不会重复计数因为计数已经完成在 POST 阶段。不这么做的典型翻车体是点击播放按钮后用href跳转每次刷新页面都在涨播放量最后列表页的热歌榜彻底失真。收藏表favorite的插入逻辑同理WebServlet(/favorite) public class FavoriteServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding(UTF-8); String songId req.getParameter(songId); String userId req.getParameter(userId); // 校验参数 favoriteDao.insert(Integer.parseInt(userId), Integer.parseInt(songId)); resp.sendRedirect(req.getContextPath() /song/detail?songId songId); } }收藏的重复插入由favorite表里的UNIQUE KEY uk_user_song兜底第二次收藏会抛DuplicateKeyException。业务上更友好的做法是先查再插但对于小型项目依赖唯一索引拦住重复然后捕获并忽略异常比一次 select 再加一次 insert 的并发安全性更高。5. javaweb 音乐项目避坑手册五个我在部署时遇到的常见问题5.1 中文乱码页面全是问号现象数据库表、JSP 页面都仔细设置了UTF-8但歌曲名在页面上显示为???或者一串乱码。原因连接 MySQL 的 URL 里漏了characterEncodingutf8。JVM 默认字符集和 MySQL 客户端字符集不一致中文在传入时已经被转成错误的字节序列存进表里的就是乱码。解决把 JDBC URL 补成jdbc:mysql://localhost:3306/music_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8并在每个 Servlet 处理请求的第一行调用req.setCharacterEncoding(UTF-8)。注意setCharacterEncoding要在读取任何参数之前调用放在getParameter之后就没有效果了。5.2 明明加了 MySQL 依赖却报 ClassNotFoundException现象IDEA 里编译不报错一启动 Tomcat 访问首页控制台抛java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因pom.xml里的mysql-connector-java使用的是providedscopeMaven 认为这个 jar 会由运行容器提供所以没有把它打包进WEB-INF/lib。Tomcat 自己当然没有这个驱动运行时自然找不到类。解决把依赖 scope 改成默认的compile或runtime。然后打开 Project Structure 的 Artifacts 面板展开WEB-INF/lib确认里面有一个mysql-connector-java-8.0.33.jar。如果是用旧版 IDEA清理一下缓存再重新部署。这个坑在 IDEA 运行 javaweb 项目配置里出现频率极高几乎每个初学 MySQL 的人都会碰到一次。5.3 分页只能翻到第一页现象列表页点击“下一页”地址栏确实变成了pageNo2但页面展示的还是前十条数据。原因两种可能。第一种是分页链接拼上了pageNo2但下一页的 Servlet 里解析参数时写了int pageNo Integer.parseInt(pageNo)参数名和前端的pageNo不一致第二种是搜索后再翻页keyword参数丢了服务端认为你重新发起了一次不带条件的查询于是从头分页。解决先把 URL 打印出来看确认pageNo和keyword都在然后在 Servlet 的parseInt方法里加默认值和异常兜底保证非法参数回落成 1。翻页链接里对keyword用URLEncoder.encode(keyword, UTF-8)编码否则歌名里带空格或特殊符号时下一页链接会被截断。5.4 歌曲播放按钮点了没反应现象页面上能看到歌曲行点击“播放”按钮后没有任何动静浏览器开发者工具里显示404 Not Found。原因file_url字段里存的是绝对路径比如D:/music/xxx.mp3而页面上的audio标签指向/static/audio/D:/music/xxx.mp3浏览器当然找不到。再或者你用了a href/music/xxx.mp3但 Tomcat 根本没在应用根目录下看到这个文件。解决file_url只存相对文件名例如love-story.mp3音频文件统一放在webapp/static/audio/目录。页面渲染时通过${pageContext.request.contextPath}/static/audio/${song.fileUrl}拼完整地址。Tomcat 默认的 DefaultServlet 能直接服务static目录下的静态文件不用额外配置。千万别把操作系统绝对路径写进数据库项目换一台机器就全部失效。5.5 插入歌曲时外键约束失败现象往song表插入一条数据控制台报Cannot add or update a child row: a foreign key constraint fails。原因singer_id指向的歌手在singer表里不存在。最常见于批量导入数据先导了歌曲表歌手表还没导结果歌曲先被拒收。另一个场景是清空数据后重新灌入清歌表时忘了处理外键关联。解决先插入歌手表再把歌曲表的数据导进来顺序不要颠倒。不要偷懒把外键删掉那是拆掉安全网正确的做法是写一个检查逻辑插入前用SELECT COUNT(*) FROM singer WHERE id ?验证一下或者更简单在管理页面上用一个下拉框选择歌手而不是手填数字。加外键的意义就是让数据库替你挡住这种脏数据。6. 再进一步用连接池替换 DriverManager并用一次压测验证收益6.1 原生 JDBC 连接池为什么值得换当前项目里每次请求都通过DriverManager.getConnection()新建一个物理连接用完立刻关闭。这在只跑通功能的阶段没问题但连接建立要经历 TCP 握手、MySQL 认证、权限校验等过程并发一高连接创建成本会直接把 Tomcat 线程拖垮。6.2 给项目切换到 HikariCP 的最小改动先在 pom 里增加依赖dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version4.0.3/version /dependency然后修改DBUtil把DriverManager替换成 HikariDataSourceimport com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class DBUtil { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl( jdbc:mysql://localhost:3306/music_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(3000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 关闭顺序不变但 connection.close() 这里实际是归还连接池 } }maximumPoolSize设 10 就够了小型音乐站用不到更大的连接数connectionTimeout设 3000 毫秒是防止数据库卡死时请求无限等待快速失败比慢速挂起好排查。6.3 验证方式与我的习惯替换之后不要只看“能跑”用 JMeter 建一个简单的测试计划20 个线程、每个线程循环 10 次请求/songs接口观察聚合报告里的平均响应时间。不追求跑出什么漂亮数字重点是先确认改造后没有报错再对比改造前的基线。连接池的效果取决于并发场景如果只有你一个人在调试感受可能不明显这很正常。我的习惯是先保留一段时间的旧实现用日志记录每次查询用到的连接数确认池子里没有泄漏再删旧代码。顺手把play_count的更新语句改成事务形式确保计数失败时不影响列表页正常显示。希望这篇笔记能帮你把这个标题变成一台真正跑得通、讲得清的小型音乐网站。本文还有配套的精品资源点击获取