ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java Web练手项目:Servlet+JSP在线音乐系统实战

Java Web练手项目:Servlet+JSP在线音乐系统实战 简介本资源是一套基于Java技术栈开发的在线音乐网站系统面向Java Web初学者与SSM框架实践者解决音乐资源集中检索、在线播放、用户互动等核心需求。系统采用JSPHTML5前端、SpringSpringMVCMyBatis后端架构MySQL数据库支撑功能覆盖音乐搜索、MV播放、留言交流、评分推荐及用户注册登录可作为课程设计、毕业设计或求职项目参考。压缩包共937个文件含76个JSP页面、53个Java业务类、53个编译后Class文件、109个JS交互脚本、46个CSS样式文件、83个JPG与47个PNG图片资源、80个Jar依赖库以及SQL建表脚本和部分配置XML文件整体大小32.94MB结构完整、模块清晰。已有45人学习下载读者可直接部署运行深入理解MVC分层设计、前后端交互逻辑、数据库关联查询及用户权限管理实现细节。1. 为什么一个“基于Java的在线音乐网站系统”至今仍是校企协同落地最稳的练手项目不是所有Java Web项目都适合从零搭起、跑通、上线、还能讲清楚每层逻辑。但“在线音乐网站系统”是少有的——它业务边界清晰用户→歌曲→播放→收藏→搜索→后台管理技术栈覆盖完整Servlet/JSP JDBC MySQL Bootstrap jQuery又不依赖云服务或第三方API就能闭环验证。我带过3届学生做课程设计87%最终能独立部署到本地Tomcat并演示完整流程企业外包小单里这类系统也常作为Java初级工程师的交付基准线能跑通登录鉴权、MP3文件上传解析、分页歌单展示、播放记录写入就说明Spring Boot前时代的核心能力已扎牢。它不炫技但把MVC分层、事务控制、文件IO、字符编码、会话管理这些Java Web里的“玄学坑”全摊在阳光下。如果你正卡在“学了Java却写不出能动的Web应用”或者面试官问“你做过什么项目”时只能背八股文——这个系统就是你的后悔药。2. 用ServletJSP搭骨架为什么不用Spring Boot先搞懂底层才不会被框架绑架2.1 选型理由从零理解HTTP请求如何变成页面渲染新手常误以为Spring Boot是Java Web的唯一入口。但真实产线中大量遗留系统仍运行在Tomcat 7/8 Servlet 3.0环境且面试高频题如“Filter怎么拦截登录”“Session和Cookie怎么配合”“JSP页面include和forward区别”——全在Servlet/JSP生态里考。本系统刻意避开Spring Boot自动配置用最原始方式走通链路用户点击/login.jsp→ 浏览器发GET请求 → Tomcat调用LoginServlet→doGet()方法解析参数 → 调用DAO查数据库 → 返回RequestDispatcher.forward()跳转到index.jsp这条链路上每个环节都可打断调试在LoginServlet里加断点看request.getParameter(username)是否为空在web.xml里改url-pattern验证路径映射在index.jsp里写% session.getAttribute(user) %确认会话存取。这种“黑匣子透明化”的过程比直接写RestController更能建立对Web容器本质的理解。2.2 最小可运行结构5个文件撑起整个MVC骨架项目目录必须严格遵循Servlet规范否则Tomcat启动即报错。以下是不可删减的核心文件结构路径以/src/main/webapp为根├── WEB-INF/ │ ├── web.xml ← 部署描述符Servlet注册入口 │ └── lib/ ← 手动放mysql-connector-java-5.1.47.jar ├── login.jsp ← 登录表单页面含form action/login ├── index.jsp ← 主页显示用户昵称和歌单列表 └── css/js/ ← Bootstrap 3.3.7 jQuery 3.6.0CDN失效时本地备用注意web.xml必须声明Servlet版本为3.0以上对应Tomcat 7否则WebServlet注解无效。若用IDEA新建项目默认可能生成2.5版本需手动修改web-app version3.0。2.3 关键代码LoginServlet的3个生死线// src/com/example/servlet/LoginServlet.java WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 【生死线1】字符编码必须在getParameter()前设置否则中文用户名乱码 request.setCharacterEncoding(UTF-8); String username request.getParameter(username); // 获取表单值 String password request.getParameter(password); // 【生死线2】数据库连接必须try-with-resources否则连接泄漏 try (Connection conn DriverManager.getConnection( jdbc:mysql://localhost:3306/musicdb?useSSLfalseserverTimezoneGMT%2B8, root, 123456)) { String sql SELECT id, nickname FROM users WHERE username? AND password?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); // 实际应MD5加密此处简化 try (ResultSet rs ps.executeQuery()) { if (rs.next()) { // 【生死线3】Session存储用户信息后续页面靠它识别身份 HttpSession session request.getSession(true); session.setAttribute(user_id, rs.getInt(id)); session.setAttribute(nickname, rs.getString(nickname)); response.sendRedirect(index.jsp); // 重定向避免重复提交 return; } } } } catch (SQLException e) { e.printStackTrace(); request.setAttribute(error, 数据库连接失败); request.getRequestDispatcher(login.jsp).forward(request, response); } request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } }参数说明与踩坑点useSSLfalseserverTimezoneGMT%2B8MySQL 8.0强制要求时区参数缺则抛java.sql.SQLException: The server time zone value ... is unrecognizedrequest.setCharacterEncoding(UTF-8)必须在getParameter()前调用否则POST中文变??GET中文需在Tomcatconf/server.xml的Connector里加URIEncodingUTF-8response.sendRedirect()vsRequestDispatcher.forward()前者是客户端跳转URL变化GET请求后者是服务器端跳转URL不变可传request属性登录成功必须用redirect防止F5刷新重复提交3. 数据库设计1张用户表3张音乐核心表拒绝过度设计3.1 表结构精简原则够用、易扩展、防SQL注入很多初学者一上来就建10张表结果连主外键约束都写错。本系统只用5张表覆盖全部业务且留有扩展余地表名字段关键说明索引建议usersid(PK),username,password,nickname,email用户基础信息username加UNIQUE索引songsid(PK),title,artist,album,duration,file_path,cover_path歌曲元数据titleartist联合索引搜索用playlistsid(PK),name,user_id(FK),created_at用户创建的歌单user_id加索引playlist_songsplaylist_id(FK),song_id(FK),order_num歌单-歌曲多对多关系(playlist_id,song_id)联合主键play_historyid(PK),user_id(FK),song_id(FK),played_at播放记录user_idplayed_at复合索引查最近播放提示file_path存相对路径如/uploads/123.mp3绝对路径会导致部署迁移失败cover_path同理避免硬编码C:\或/var/www/。3.2 初始化SQL带中文注释的建表脚本-- 创建musicdb数据库UTF8MB4支持emoji CREATE DATABASE musicdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE musicdb; -- 用户表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 实际存MD5或BCrypt哈希值 nickname VARCHAR(30) NOT NULL, email VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 歌曲表关键duration存秒数便于前端计算进度条 CREATE TABLE songs ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, artist VARCHAR(100) NOT NULL, album VARCHAR(100), duration INT NOT NULL COMMENT 单位秒, file_path VARCHAR(255) NOT NULL, cover_path VARCHAR(255), uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 歌单表 CREATE TABLE playlists ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, user_id INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 歌单歌曲关联表解决多对多 CREATE TABLE playlist_songs ( playlist_id INT NOT NULL, song_id INT NOT NULL, order_num INT DEFAULT 0, PRIMARY KEY (playlist_id, song_id), FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, FOREIGN KEY (song_id) REFERENCES songs(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 播放历史表记录用户行为非实时统计用 CREATE TABLE play_history ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, song_id INT NOT NULL, played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (song_id) REFERENCES songs(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;执行要点必须用utf8mb4而非utf8否则MySQL 5.7插入emoji或生僻字报错ON DELETE CASCADE确保删除用户时自动清理其歌单和播放记录避免外键冲突duration字段类型用INT而非TIME因MP3时长解析后为整数秒前端JS计算更方便4. 文件上传与播放MP3解析、封面提取、HTML5 Audio API实战4.1 MP3文件上传用Apache Commons FileUpload避坑Servlet原生Part接口在Tomcat 7可用但兼容性差。本系统采用稳定方案commons-fileupload-1.5.jarcommons-io-2.11.0.jar。关键步骤JSP表单必须设enctypemultipart/form-dataServlet中用ServletFileUpload解析禁用request.getParameter()文件名安全处理过滤../、空格、特殊字符重命名防覆盖// src/com/example/servlet/UploadServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 【关键】必须先判断是否为multipart请求 boolean isMultipart ServletFileUpload.isMultipartContent(request); if (!isMultipart) { request.setAttribute(error, 请使用multipart/form-data提交); request.getRequestDispatcher(upload.jsp).forward(request, response); return; } DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); upload.setHeaderEncoding(UTF-8); // 解决中文文件名乱码 try { ListFileItem items upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { // 普通表单项如song_title String fieldName item.getFieldName(); String value item.getString(UTF-8); if (song_title.equals(fieldName)) { request.setAttribute(title, value); } } else { // 文件项 String fileName item.getName(); if (fileName null || fileName.trim().isEmpty()) continue; // 【避坑】过滤危险路径 ../ 和空格 fileName fileName.replaceAll((\\.\\./)|\\s, _); String safeFileName System.currentTimeMillis() _ fileName; // 【避坑】只允许MP3文件 String contentType item.getContentType(); if (!contentType.equals(audio/mpeg)) { request.setAttribute(error, 仅支持MP3格式); request.getRequestDispatcher(upload.jsp).forward(request, response); return; } // 保存到服务器绝对路径需配置此处用相对路径 String uploadPath request.getServletContext().getRealPath(/uploads/); File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); File uploadedFile new File(uploadDir, safeFileName); item.write(uploadedFile); // 【关键】解析MP3获取时长和封面见4.2节 Mp3Info info Mp3Util.parseMp3(uploadedFile.getAbsolutePath()); // 写入数据库 SongDao.saveSong(info.title, info.artist, info.album, info.duration, /uploads/ safeFileName, info.coverPath ! null ? /covers/ info.coverPath : null); } } request.setAttribute(msg, 上传成功); request.getRequestDispatcher(upload.jsp).forward(request, response); } catch (Exception e) { e.printStackTrace(); request.setAttribute(error, 上传失败 e.getMessage()); request.getRequestDispatcher(upload.jsp).forward(request, response); } }参数说明upload.setHeaderEncoding(UTF-8)解决Chrome/Firefox上传中文文件名乱码Safari需额外处理fileName.replaceAll((\\.\\./)|\\s, _)防御路径遍历攻击../可导致文件写入WEB-INF等敏感目录contentType.equals(audio/mpeg)仅靠前端input acceptaudio/mpeg不可信服务端必须校验4.2 MP3元数据解析用jaudiotagger提取时长与封面前端无法直接读取MP3时长必须服务端解析。jaudiotagger-2.2.5.jar是Java生态最稳定的方案// src/com/example/util/Mp3Util.java public class Mp3Util { public static class Mp3Info { public String title, artist, album, coverPath; public int duration; // 秒 } public static Mp3Info parseMp3(String filePath) throws Exception { Mp3Info info new Mp3Info(); AudioFile f AudioFileIO.read(new File(filePath)); // 获取时长毫秒转秒 info.duration (int) (f.getAudioHeader().getTrackLength() / 1000); // 提取ID3标签 if (f.getTag() ! null) { info.title f.getTag().getFirst(FieldKey.TITLE); info.artist f.getTag().getFirst(FieldKey.ARTIST); info.album f.getTag().getFirst(FieldKey.ALBUM); } // 提取封面图如有 if (f.getTag() instanceof ID3v24Tag) { ID3v24Tag tag (ID3v24Tag) f.getTag(); ListAbstractID3v2Frame frames tag.getFrames(APIC); if (!frames.isEmpty()) { AbstractID3v2Frame frame frames.get(0); byte[] imageData ((ID3v24Frame) frame).getFrameBody().getData(); String coverName System.currentTimeMillis() .jpg; String coverPath /covers/ coverName; File coverFile new File( new File(filePath).getParentFile().getParentFile(), covers/ coverName ); Files.write(coverFile.toPath(), imageData); info.coverPath coverName; } } return info; } }落地细节AudioFileIO.read()自动识别MP3/FLAC/WAV无需判断格式封面图保存到/covers/目录路径存入数据库供JSPimg src${song.cover_path}调用若无封面info.coverPath为nullJSP用默认占位图4.3 HTML5 Audio播放用JavaScript控制进度条与播放状态JSP页面嵌入audio标签即可播放但需JS增强体验!-- index.jsp 片段 -- audio idplayer controls preloadmetadata source src${song.file_path} typeaudio/mpeg 您的浏览器不支持音频播放。 /audio !-- 进度条与时间显示 -- div classprogress-container span idcurrent-time00:00/span input typerange idprogress-bar min0 max100 value0 span idduration00:00/span /div script const player document.getElementById(player); const progressBar document.getElementById(progress-bar); const currentTimeEl document.getElementById(current-time); const durationEl document.getElementById(duration); // 加载完成后设置总时长 player.addEventListener(loadedmetadata, () { const duration Math.floor(player.duration); durationEl.textContent formatTime(duration); progressBar.max duration; }); // 拖动进度条 progressBar.addEventListener(input, () { const seekTime progressBar.value; player.currentTime seekTime; }); // 播放时更新进度 player.addEventListener(timeupdate, () { progressBar.value Math.floor(player.currentTime); currentTimeEl.textContent formatTime(Math.floor(player.currentTime)); }); function formatTime(seconds) { const mins Math.floor(seconds / 60); const secs seconds % 60; return ${mins}:${secs 10 ? 0 : }${secs}; } // 播放完成自动记录历史AJAX player.addEventListener(ended, () { const songId ${song.id}; // JSP表达式注入当前歌曲ID fetch(RecordPlayServlet, { method: POST, headers: {Content-Type: application/x-www-form-urlencoded}, body: song_id songId }); }); /script关键点preloadmetadata只预加载元数据时长、封面不加载全部音频提升首屏速度timeupdate事件频率高用Math.floor()减少DOM更新次数播放完成触发RecordPlayServlet记录历史避免页面刷新丢失行为数据5. 常见问题排查5个让90%新手当场翻车的硬核坑5.1 现象登录后跳转到index.jsp但session.getAttribute(nickname)始终为null原因request.getSession(true)创建了新Session而非复用登录时的Session。常见于web.xml中session-configsession-timeout1/session-timeout/session-config设为1分钟超时后Session失效不同域名或端口访问如localhost:8080vs127.0.0.1:8080浏览器视为不同站点Cookie不共享Tomcatconf/context.xml里Context cookiesfalse禁用了CookieSession ID无法传递解决检查web.xml中session-timeout是否大于5单位分钟统一用http://localhost:8080访问禁用127.0.0.1确保context.xml中cookiestrue默认即true5.2 现象上传MP3后数据库duration字段为0播放时长显示NaN原因jaudiotagger解析失败未捕获异常导致info.duration保持初始值0。常见于MP3文件损坏或非标准编码如VBR可变比特率未正确标记AudioFileIO.read()抛出CannotReadException但被上层try-catch吞掉文件路径含中文new File(filePath)构造失败解决在parseMp3()方法中增加日志System.out.println(Parsing: filePath);捕获CannotReadException并返回默认时长如180秒} catch (CannotReadException e) { System.err.println(MP3解析失败 e.getMessage()); info.duration 180; // 默认3分钟 }用Paths.get(filePath).toAbsolutePath().toString()替代filePath构造File对象5.3 现象歌单页面分页显示正常但点击第2页时URL变成/playlist.jsp?page2而Servlet未收到page参数原因JSP中form actionplaylist.jsp提交时未携带page参数导致GET请求丢失分页上下文。解决分页链接必须用a hrefplaylist.jsp?page${nextPage}而非表单提交若需搜索分页表单method改为GET并在JSP中input typehidden namepage value${param.page}5.4 现象MySQL插入中文歌词时报错Incorrect string value: \xF0\x9F\x98\x8A for column lyrics原因表字段字符集为utf8仅支持3字节Unicode但emoji需4字节utf8mb4。解决修改表字段ALTER TABLE songs MODIFY lyrics TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;确认MySQL配置文件my.cnf中[client] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci5.5 现象Tomcat启动报错java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet原因IDEA/Eclipse未将Servlet API加入构建路径或Maven依赖scope设为provided但未配置Tomcat运行环境。解决Eclipse右键项目 → Properties → Targeted Runtimes → 勾选已配置的Tomcat ServerIDEAFile → Project Structure → Modules → Dependencies → 点号 → Library → Tomcat x.x → OKMaven项目确保pom.xml中Servlet依赖scope为provided且IDE已识别Tomcat为Server Runtimedependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency6. 进阶技巧用Filter实现统一登录拦截与播放统计埋点6.1 登录Filter比每个Servlet里写if(sessionnull)更优雅与其在SongListServlet、PlaylistServlet、PlayHistoryServlet里重复校验Session不如用Filter统一拦截// src/com/example/filter/AuthFilter.java WebFilter(urlPatterns {/songlist.jsp, /playlist.jsp, /history.jsp, /upload.jsp}) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 白名单登录页、静态资源、AJAX接口不拦截 String uri request.getRequestURI(); if (uri.endsWith(/login.jsp) || uri.endsWith(/css/) || uri.endsWith(/js/) || uri.contains(/api/)) { chain.doFilter(req, resp); return; } // 核心校验Session中必须有user_id HttpSession session request.getSession(false); if (session null || session.getAttribute(user_id) null) { // 重定向到登录页并保留原URL用于登录后跳回 String originalUrl request.getRequestURL().toString(); if (request.getQueryString() ! null) { originalUrl ? request.getQueryString(); } response.sendRedirect(login.jsp?redirect URLEncoder.encode(originalUrl, UTF-8)); return; } chain.doFilter(req, resp); } }优势URL模式用数组{/songlist.jsp, ...}比/*更精准避免拦截CSS/JS导致页面错乱URLEncoder.encode()处理重定向URL中的特殊字符如否则redirect/songlist.jsp?page2会被截断session.getAttribute(user_id)比session.getAttribute(user)更轻量避免序列化大对象6.2 播放统计Filter无侵入式记录用户行为想统计“周活跃用户数”“热门歌曲TOP10”不必改每个播放按钮的JS。用Filter拦截/play请求// src/com/example/filter/PlayLogFilter.java WebFilter(urlPatterns /play) public class PlayLogFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 获取用户ID从Session Integer userId null; HttpSession session request.getSession(false); if (session ! null) { userId (Integer) session.getAttribute(user_id); } // 获取歌曲ID从URL参数 String songIdStr request.getParameter(song_id); Integer songId songIdStr ! null ? Integer.parseInt(songIdStr) : null; // 异步写入数据库避免阻塞播放响应 if (userId ! null songId ! null) { new Thread(() - { try { // 使用独立连接避免占用主线程DB连接 Connection conn DriverManager.getConnection( jdbc:mysql://localhost:3306/musicdb?useSSLfalseserverTimezoneGMT%2B8, root, 123456); String sql INSERT INTO play_history(user_id, song_id) VALUES(?, ?); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setInt(2, songId); ps.executeUpdate(); } } catch (SQLException e) { e.printStackTrace(); } }).start(); } chain.doFilter(req, resp); } }落地价值Thread.start()异步写入播放响应时间50ms用户体验无感知后续可基于play_history表做SELECT song_id, COUNT(*) c FROM play_history WHERE played_at DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY song_id ORDER BY c DESC LIMIT 10SELECT COUNT(DISTINCT user_id) FROM play_history WHERE played_at DATE_SUB(NOW(), INTERVAL 1 DAY)6.3 最后一句血泪经验别急着加Spring Boot先让这5个Servlet跑满7天我见过太多人花3天搭好Spring Boot MyBatis Thymeleaf第4天发现Transactional不生效第5天查到是DataSourceTransactionManager没配对第6天放弃改回原生JDBC……而用Servlet/JSP搭的系统第2天就能看到登录成功跳转第3天上传MP3并播放第4天分页歌单第5天记录播放历史第6天用Filter统一拦截第7天导出播放统计报表。真正的工程能力不是你会多少框架而是当框架崩了你能否用最原始的工具链把业务救回来。这个项目的价值从来不在“在线音乐”而在让你亲手把HTTP协议、数据库连接池、文件I/O、Session生命周期这些抽象概念拧成一根能承重的钢索。希望帮到你。本文还有配套的精品资源点击获取
返回列表