简介面向JavaWeb学习者与课程设计需求的音乐网站项目源码包整体基于Eclipse、Tomcat、MySQL、JDK环境构建内含可直接放入Tomcat下运行验证的WAR包以及完整源码与全部依赖JAR包大幅降低环境配置门槛。项目实现音乐在线播放、下载、分类、排行等核心功能并配有丰富页面素材、动态演示与音频样例适合快速了解JavaWeb前后端交互流程。压缩包共881个文件主要包含JSP页面80个、Java源文件60个、Class文件122个、JAR包44个另有222张PNG图片、118张GIF动图、116张JPG图片、23个MP3音频等资源整体约189.75MB目录按页面、样式、源码与依赖划分便于检索。目前已有915人学习下载既可直接部署体验也可按自身需求参考或改编是JavaWeb入门实践、课程设计或毕业设计的有用素材。1. 小型音乐网站一份能跑通、能改、能写进报告里的 JavaWeb 完整案例如果你正在找“基于 JavaWeb 的小型音乐网站”多半不是想学什么高深架构而是期末课程设计或毕业设计快到截止日期了手上缺一个能跑、能讲、能交差的完整案例。市面上这类源码不少但大量是半成品要么只有登录和一张表要么数据库脚本缺字段要么在 IDEA 里怎么都部署不上去。这份小型音乐网站的资源是一套前后台都齐全的 JavaWeb 项目完整案例——用户注册登录、歌曲列表分页、搜索、播放、后台管理都在数据库用的是 MySQL符合绝大多数课程设计报告的要求。适合刚学完 Servlet 和 JSP、想拿到一个干净案例照着复现的在校学生也适合想快速上手 JavaWeb 完整流程、不想从零搭框架的入门者。2. 先把项目的底子搭明白数据库表设计 IDEA 运行 JavaWeb 项目配置拿到这份资源的第一步不是急着看代码而是先理解它的数据库和运行方式。很多同学拿到案例第一件事就是点运行结果要么连不上数据库要么 404。我习惯先把表结构看一遍再在 IDEA 里把 Tomcat 部署配置捋清楚这样后面跑起来会省很多时间。2.1 三张表把网站撑起来user、song、playlist 的结构与取舍小型音乐网站的核心数据就三类用户、歌曲、收藏/歌单。这份案例里用了三张表来存分别是用户表user、歌曲表song、收藏表playlist。先看建表脚本这是整个案例的地基。CREATE DATABASE IF NOT EXISTS music_web DEFAULT CHARACTER SET utf8mb4; USE music_web; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT MD5 加密后的密码, nickname VARCHAR(50) DEFAULT 乐迷, role INT DEFAULT 0 COMMENT 0普通用户 1管理员, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE song ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, singer VARCHAR(50) DEFAULT 未知歌手, album VARCHAR(100) DEFAULT 暂无专辑, duration INT DEFAULT 0 COMMENT 时长单位秒, file_path VARCHAR(200) NOT NULL COMMENT 音频相对路径如 upload/music/xxx.mp3, cover_path VARCHAR(200) DEFAULT upload/cover/default.jpg, play_count INT DEFAULT 0, status INT DEFAULT 1 COMMENT 1上架 0下架 ); CREATE TABLE playlist ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, song_id INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_song (user_id, song_id) );这里有几个设计点值得注意。第一user表里存的是 MD5 加密后的密码不是明文这是课程设计答辩时老师一定会问的地方。第二song表用file_path存相对路径而不是把 MP3 文件本身塞进数据库音频文件放在项目外的upload目录里数据库只存路径字符串——这个设计避免了数据库体积膨胀也方便替换歌曲文件。第三playlist是典型的关联表只存user_id和song_id两个外键再加上联合唯一键防止重复收藏。如果你拿到的版本里表名或字段略有差异比如song表多了lyric字段不影响整体理解。建议先按这份脚本把数据库建出来再对照实体类看字段映射。2.2 在 IDEA 里把 JavaWeb 跑起来Tomcat、Artifacts 与部署路径数据库建好后接下来是把项目导入 IDEA 并配置 Tomcat。这一步是很多人的第一个大坑——网上搜“idea运行javaweb项目配置”能找到一堆教程但写这份案例时的配置细节很容易被忽略导致运行后 404。我按实操顺序说重点。先确认项目结构。非 Maven 项目的话目录通常是这样的music-web/ ├── src/ │ ├── com/music/bean/ # 实体类 │ ├── com/music/dao/ # 数据访问层 │ ├── com/music/servlet/ # Servlet 控制器 │ └── com/music/util/ # 工具类 ├── web/ │ ├── jsp/ # 页面文件 │ ├── WEB-INF/web.xml # 核心配置文件 │ └── index.jsp └── lib/ # 依赖的 jar 包在 IDEA 里导入后需要做三件事设置 Artifacts、添加 Tomcat Server、配置 Deployment。最关键的一步是 Deployment 里要把项目以war exploded模式添加进去并且注意 Application context 的路径。如果项目名是music_web那访问地址就是http://localhost:8080/music_web/context 路径带不带项目名直接决定了你所有跳转路径是否对得上。!-- web.xml 里的核心配置注意 servlet 映射路径 -- web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list servlet servlet-nameLoginServlet/servlet-name servlet-classcom.music.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping /web-app依赖 jar 方面这种非 Maven 项目一般把mysql-connector、jstl等 jar 放在lib目录里。如果导入后编译报错先看Project Structure → Modules → Dependencies里有没有把 lib 目录加进去以及Artifacts里有没有把 lib 打进去。注意 Jar 包缺失的症状很隐蔽代码编译不报错但运行时ClassNotFoundException。3. 注册与登录从 JDBC 到 Servlet 的第一条完整链路音乐网站最基础的功能是登录注册而这条链路正好把 JDBC、Dao、Servlet、JSP 全部串起来。搞懂这一章后面看列表、搜索、收藏代码基本无障碍。3.1 连接数据库的工具类不用框架也能把 JDBC 写干净先看JdbcUtil这是一个用DriverManager实现的工具类够用且直观。后面如果你想把项目升级成连接池只需要替换这个类里的getConnection方法其他 Dao 层代码不用动。package com.music.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class JdbcUtil { private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/music_web?characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL 驱动加载失败检查 lib 目录是否有驱动 jar); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps ! null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }参数说明characterEncodingutf8是解决中文乱码的第一道关卡写漏掉的话插入的中文进数据库就会变成??。serverTimezoneAsia/Shanghai是 MySQL 8.x 驱动的要求老版本驱动不写也能连新版驱动不写会直接报时区异常。数据库密码如果是你自己的改成你本机的密码就行。3.2 登录的完整链路Dao → Servlet → 页面跳转登录逻辑不复杂但初学者容易在分层上踩坑。UserDao负责查询数据库LoginServlet负责接收请求、调 Dao、存 Session、跳转页面。先看 Dao 层代码package com.music.dao; import com.music.bean.User; import com.music.util.JdbcUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; public class UserDao { public User findByUsernameAndPassword(String username, String password) { String sql SELECT id, username, nickname, role FROM user WHERE username ? AND password ?; // 使用 PreparedStatement 防 SQL 注入 try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setNickname(rs.getString(nickname)); user.setRole(rs.getInt(role)); return user; } } } catch (Exception e) { e.printStackTrace(); } return null; } }这段代码有两个细节值得在答辩时提。第一SQL 里用的是?占位符加PreparedStatement不是字符串拼接SELECT ... WHERE username username 这是防止 SQL 注入的标准写法——老师看到字符串拼接的版本基本会扣分。第二查询结果只返回用户信息不返回密码避免密码在 Session 和页面间反复传递。对应的LoginServletpackage com.music.servlet; import com.music.bean.User; import com.music.dao.UserDao; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // GET 请求直接转发到登录页面 req.getRequestDispatcher(/jsp/login.jsp).forward(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); String remember req.getParameter(remember); // 最基本的非空校验 if (username null || password null || username.trim().isEmpty() || password.trim().isEmpty()) { req.setAttribute(error, 用户名和密码不能为空); req.getRequestDispatcher(/jsp/login.jsp).forward(req, resp); return; } // 这里做 MD5 加密后再比对 String encryptedPwd Md5Util.md5(password); User user userDao.findByUsernameAndPassword(username.trim(), encryptedPwd); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(loginUser, user); // 勾选了记住我写一个 Cookie if (on.equals(remember)) { Cookie cookie new Cookie(rememberUsername, username); cookie.setMaxAge(7 * 24 * 60 * 60); // 7 天有效期 resp.addCookie(cookie); } resp.sendRedirect(req.getContextPath() /song/list); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/jsp/login.jsp).forward(req, resp); } } }逻辑说明doGet负责展示登录页doPost负责处理提交。校验顺序是——先看空值再查数据库最后区分登录成功与失败。成功时把user对象放进 Session失败时回传error属性并转发回登录页。注意这里的Md5Util.md5()是工具类的静态方法密码在数据库里存的是密文不是明文比对。3.3 Session 与“记住我”的取舍谁该存 Session谁该存 Cookie很多同学会把用户所有信息都塞进 Session或者相反什么都不存。我的习惯是Session 里只存必要的用户标识和昵称——loginUser这个User对象已经足够。后续页面上要显示昵称、要判断是否登录都从这个对象取不需要再查库。“记住我”这个功能在小型案例里用 Cookie 最合适。Cookie 的安全性不如 Session所以只存用户名不存密码。上面代码里rememberUsername这个 Cookie 有效期设了 7 天下次打开登录页时可以从 Cookie 里带出用户名省去重复输入但密码仍然要重新敲。退出登录的代码是配套的反例和正例放一起看就能理解// 退出登录销毁 Session同时清除记住我的 Cookie HttpSession session req.getSession(false); if (session ! null) { session.invalidate(); } Cookie cookie new Cookie(rememberUsername, null); cookie.setMaxAge(0); resp.addCookie(cookie); resp.sendRedirect(req.getContextPath() /login);这里有个常见的错误做法退出时只调session.invalidate()忘了 Cookie 还有 7 天有效期结果下一个人打开登录页发现用户名还挂在页面上。所以每次退出rememberUsername这个 Cookie 必须清掉setMaxAge(0)就是让浏览器立刻删除这个 Cookie。4. 歌曲列表、搜索与播放把静态资源当成一等公民登录之后的核心页面是歌曲列表和播放页。这一章的难点不在 Java 逻辑而在路径处理——列表要分页搜索要处理中文编码播放要找到 MP3 文件的真实位置。这三块各有各的坑。4.1 分页列表pageNo/pageSize 与 MVC 里的跳坑歌曲列表页不可能把几百首歌一次性全查出来分页是必须的。SongServlet里的核心逻辑如下package com.music.servlet; import com.music.bean.Song; import com.music.dao.SongDao; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.util.List; WebServlet(/song/list) public class SongListServlet extends HttpServlet { private SongDao songDao new SongDao(); private static final int PAGE_SIZE 10; // 每页显示 10 条 Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int pageNo 1; String pageParam req.getParameter(pageNo); if (pageParam ! null !pageParam.isEmpty()) { try { pageNo Integer.parseInt(pageParam); } catch (NumberFormatException e) { pageNo 1; // 参数不合法时回退到第一页 } } int totalCount songDao.count(); int totalPage (int) Math.ceil(totalCount * 1.0 / PAGE_SIZE); if (pageNo 1) pageNo 1; if (pageNo totalPage totalPage 0) pageNo totalPage; ListSong songList songDao.findByPage(pageNo, PAGE_SIZE); req.setAttribute(songList, songList); req.setAttribute(pageNo, pageNo); req.setAttribute(totalPage, totalPage); req.getRequestDispatcher(/jsp/song_list.jsp).forward(req, resp); } }分页参数说明pageNo是页码从 1 开始PAGE_SIZE是每页条数设为 10。totalPage的计算用的是Math.ceil向上取整——比如 37 条数据、每页 10 条就是 4 页。这里有个必踩的坑如果用户手动把 URL 改成pageNo999不做越界处理的话页面会显示空白列表。上面代码里if (pageNo totalPage totalPage 0) pageNo totalPage;就是防这个的。对应的 Dao 方法注意 SQL 的LIMIT ? OFFSET ?写法public ListSong findByPage(int pageNo, int pageSize) { String sql SELECT id, title, singer, album, duration, file_path, cover_path, play_count FROM song WHERE status 1 ORDER BY play_count DESC LIMIT ? OFFSET ?; // 每页条数固定偏移量由页码算出来 int offset (pageNo - 1) * pageSize; try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, pageSize); ps.setInt(2, offset); try (ResultSet rs ps.executeQuery()) { // 解析 ResultSet 转成 Song 对象列表 } } catch (Exception e) { e.printStackTrace(); } return new ArrayList(); }LIMIT ? OFFSET ?和LIMIT ?, ?是两种等价写法前者是“限制条数 偏移量”后者是“从第几条开始 取几条”。注意 MySQL 的LIMIT不能写表达式所以offset必须在 Java 里算好再传进去不能在 SQL 里写LIMIT (pageNo-1)*pageSize, pageSize。4.2 中文歌曲搜索URL 编码在 GET 里的坑搜索功能看起来简单——页面上一个输入框提交后用LIKE模糊查询。但中文搜索在 GET 请求里有一个经典问题搜索关键词会出现在 URL 里Tomcat 解析 URL 参数时用的是什么编码直接决定了搜“周杰伦”是能查出结果还是变成乱码。String keyword req.getParameter(keyword); if (keyword ! null !keyword.trim().isEmpty()) { keyword new String(keyword.getBytes(ISO-8859-1), UTF-8); // 处理乱码的兜底方案上面这行在某些 Tomcat 版本下必不可少 sql SELECT ... FROM song WHERE title LIKE ? OR singer LIKE ?; ps.setString(1, % keyword %); ps.setString(2, % keyword %); }这里说明一下为什么需要new String(keyword.getBytes(ISO-8859-1), UTF-8)。Tomcat 8.0 之前的版本GET 请求的参数默认按 ISO-8859-1 解码你在 JSP 里输入的中文经过浏览器 URL 编码后到服务端就被错误解码了。Tomcat 8.0 之后默认改成了 UTF-8但如果你的 Tomcat 版本比较旧或者server.xml里Connector配了URIEncoding行为就不一样。最稳妥的办法是在测试时先输一个中文关键词如果列表页能搜到说明环境没问题搜不到就看 URL 里的参数是不是%E5%91%A8%E6%9D%B0%E4%BC%A6这样的编码形态。如果你是照着这份案例的 web.xml 配置走的项目里通常已经有一个CharacterEncodingFilter来处理 POST 请求的编码但 GET 请求不经过setCharacterEncoding所以上面的兜底代码是搜索功能不翻车的关键。4.3 播放页HTML5 audio 与 upload 目录的部署策略歌曲播放是音乐网站的门面。页面上用 HTML5 的audio标签播放 MP3但这个标签的src指向哪里是由song表里的file_path字段决定的。audio controls autoplay loop source src${song.filePath} typeaudio/mpeg 您的浏览器不支持 HTML5 音频播放 /audio比如数据库里file_path upload/music/qinghuaci.mp3那页面上最终请求的地址就是http://localhost:8080/music_web/upload/music/qinghuaci.mp3。问题来了这个upload目录如果放在 IDEA 项目目录内部每次重新部署或 clean 之后上传的歌曲文件可能被清空因为 Tomcat 部署的是 Artifacts 的副本不是源项目目录。我一般会这样处理把上传目录放在 Tomcat 的 webapps 之外然后给 Tomcat 配置虚拟目录映射。在server.xml的Host标签里加一行Context path/music_web/upload docBaseD:/music_upload /这样代码里写的upload/music/xxx.mp3路径实际会映射到D:/music_upload/music/xxx.mp3重新部署也不会丢文件。如果不想动server.xml退一步的做法是把上传功能禁掉把歌曲文件手工放到部署目录的upload文件夹里——但要注意IDEA 里点Redeploy时这个目录同样可能被覆盖。5. 常见问题与排查我把这个案例的翻车点都踩了一遍这一步写给拿到资源后两小时内可能崩溃的同学。以下四个问题是我复现这类 JavaWeb 案例时遇到概率最高的每一条都是“现象 → 原因 → 解决”的完整排查路径。5.1 中文乱码插入数据库变成??页面上显示æµè¡现象注册一个中文用户名数据库里存的是??页面上读取出来后显示一串类似æµè¡的乱码。原因三处编码不一致——JSP 页面声明的字符集、请求进入 Servlet 后的解码字符集、数据库连接 URL 里的字符集。三者只要有任何一个不是 UTF-8中文就保不住。最常见的排雷位置是jdbc:mysql://...?characterEncodingutf8这个参数丢了或者项目里没有配编码过滤器。解决统一三处编码。第一数据库连接 URL 必须带characterEncodingutf8这是JdbcUtil里必须有的参数。第二在 web.xml 里注册编码过滤器拦截所有请求filter filter-nameCharacterEncodingFilter/filter-name filter-classcom.music.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping第三JSP 页面顶部统一写% page contentTypetext/html;charsetUTF-8 languagejava %。三处都是 UTF-8 之后乱码基本消失。如果还有问题检查 MySQL 表本身的字符集用SHOW CREATE TABLE song;确认是不是utf8mb4。5.2 IDEA 里运行 Tomcat 后访问 404但代码没有编译错误现象点击运行后控制台显示 Tomcat 启动成功浏览器访问http://localhost:8080/music_web/index.jsp返回 404或者直接访问http://localhost:8080/显示 Tomcat 默认首页。原因IDEA 的 Tomcat 配置中Deployment 里没有添加 Artifact或者添加了但 Application context 写错。另一个可能Artifact 构建失败WEB-INF/classes是空的Servlet 类没有被编译进去。解决打开Run/Debug Configurations → Tomcat Server → Deployment点选择Artifact然后选中项目的war exploded版本。Application context 填/music_web和代码里req.getContextPath()的预期一致。保存后重启 Tomcat。如果还 404检查 IDE 的out/artifacts目录下有没有生成WEB-INF/classes文件夹里面有没有.class文件——没有的话就是编译输出路径配置问题把Project Structure → Project compiler output指到项目out目录。5.3 启动报ClassNotFoundException: com.mysql.cj.jdbc.Driver或时区异常现象Tomcat 启动正常但一访问登录接口就报错后台日志里出现ClassNotFoundException: com.mysql.cj.jdbc.Driver或者The server time zone value йʱ is unrecognized。原因第一个是 MySQL 驱动 jar 没进 Artifacts。很多人往lib目录里放了 jar但Artifacts里没把 lib 目录加进去导致运行时找不到驱动类。第二个是 MySQL 8.x 驱动对时区敏感URL 里没写serverTimezone。解决在File → Project Structure → Artifacts里选中war exploded在Available Elements里找到 lib 目录右键Put into /WEB-INF/lib确认 jar 被打进部署包。然后检查JdbcUtil里 URL 是否包含serverTimezoneAsia/Shanghai和useSSLfalse。注意驱动类名也有讲究——MySQL 5.x 的驱动类是com.mysql.jdbc.DriverMySQL 8.x 改成了com.mysql.cj.jdbc.Driver两个版本别混。5.4 歌曲播放不了页面检查元素发现audio的src是 404现象列表页图片和文字都正常点击播放按钮后没声音浏览器开发者工具的 Network 面板里upload/music/xxx.mp3请求返回 404。原因Path 失效。最典型的是你项目里确实有upload目录但 IDEA 部署时只拷贝了web目录里的内容upload如果放在src同级而不是web目录下就不会被复制到部署目录里。另一种可能数据库里存的路径是upload/music/xxx.mp3但文件实际在web/upload/music/xxx.mp3部署后变成了music_web/upload/xxx.mp3层级对不上。解决先打开 project structure 看部署后的目录结构确认upload在不在web目录下。如果不在把整个upload文件夹复制到web目录下。如果嫌每次部署都手动复制麻烦回到第 4.3 节的做法在server.xml里做路径映射让物理文件路径和 URL 路径彻底解耦。我自己的经验是这类课程设计项目里歌曲文件能正常播放就已经胜过了至少三成下载者——很多人卡在这一步就放弃了。6. 进阶把它变成你自己的课程设计——统一拦截 Filter 与批量测试数据课程设计答辩和网上下载的源码最大的区别在于你有没有做“自己的增量”。这一章给你两个低成本、高回报的改动方向任何一个都能让这份案例看起来不像是原封不动的下载品。6.1 用 Filter 把登录拦截做成统一开关很多案例的问题在于不登录也能直接访问歌曲列表页这会让老师质疑你对会话控制的理解。加一个登录拦截 Filter 是性价比最高的改进——十几行代码但能展示你对 Filter 生命周期和 Session 机制的理解。package com.music.filter; import com.music.bean.User; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 放行登录、注册、静态资源不放行业务页面 String uri request.getRequestURI(); if (uri.endsWith(/login) || uri.endsWith(/register) || uri.contains(/upload/) || uri.endsWith(.js) || uri.endsWith(.css) || uri.endsWith(.jpg)) { chain.doFilter(req, resp); return; } HttpSession session request.getSession(false); User user session ! null ? (User) session.getAttribute(loginUser) : null; if (user null) { // 未登录跳回登录页带上来源路径方便登录后跳回 response.sendRedirect(request.getContextPath() /login); return; } chain.doFilter(req, resp); } }这是典型的白名单放行策略。注意uri.contains(/upload/)必须放行否则audio标签请求 MP3 文件时会被当作未登录拦截播放功能直接废掉。request.getSession(false)里的false很关键——如果 Session 不存在不主动创建避免每次访问都产生无意义的 Session 对象。加完这个 Filter 后你可以顺手在答辩时讲一下为什么静态资源也要放行这基本是必问题。6.2 用一段 SQL 批量造 50 首测试数据下载下来的案例一般只有三五首演示歌曲分页效果很难看。用循环插入的方式快速补充测试数据会让列表页的分页和搜索功能立刻“有东西可看”。-- 用存储过程循环插入 50 首测试歌曲歌手和标题随机组合 DROP PROCEDURE IF EXISTS insert_test_songs; DELIMITER $$ CREATE PROCEDURE insert_test_songs() BEGIN DECLARE i INT DEFAULT 1; WHILE i 50 DO INSERT INTO song (title, singer, album, duration, file_path, cover_path, play_count, status) VALUES ( CONCAT(测试歌曲_, i), CONCAT(歌手_, i % 10), CONCAT(专辑_, i % 5), 180 i * 10, CONCAT(upload/music/song_, i, .mp3), upload/cover/default.jpg, i * 37, 1 ); SET i i 1; END WHILE; END$$ DELIMITER ; CALL insert_test_songs();这段存储过程用CONCAT动态拼接字段把歌曲名编成“测试歌曲_1”到“测试歌曲_50”播放量做成等差数列方便验证按播放量排序的效果。注意实际播放时这些歌曲的file_path对应的 MP3 文件并不存在所以播放页会 404——这没关系分页和搜索功能已经能演示了答辩时不会被追问到每一首歌都能播。文档里头还有不少细节值得自己再挖一遍比如Md5Util的盐值处理、后台管理页面的歌曲上下架逻辑、播放次数自增的时机。我的习惯是拿到任何案例后先把数据库脚本跑通、把代码逐层看一遍然后立刻做上面这两个小改进把案例调教成自己的东西。从那以后我每次拿到新项目都强制先跑一遍数据库、加一遍编码过滤器再谈其他功能——这个习惯帮我避开了至少两小时的排障时间。希望帮到你。本文还有配套的精品资源点击获取