简介:这是一套基于JavaWeb技术栈开发的小型音乐网站完整项目,面向Java初学者与Web开发入门者,解决在线音乐播放、下载、分类浏览及排行榜展示等核心功能实现问题。资源包共881个文件,涵盖80个JSP页面、60个Java后端类、44个运行依赖JAR包、23首MP3示例音频、以及大量前端资源(116个JPG、222个PNG、22个JS、18个HTML、16个CSS),结构清晰,模块划分明确,便于理解MVC分层设计与前后端交互逻辑。压缩包大小为189.75MB,已内置可直接部署的WAR包、完整MySQL建表SQL脚本及全部配置文件(XML、properties等),支持Eclipse+Tomcat+MySQL+JDK环境一键运行与二次开发。目前已有912人学习下载,读者可立即获得可运行的全功能Demo、规范的工程目录结构、真实音频资源集成方案及常见Web异常排错参考,是实践Servlet/JSP、数据库连接池、文件上传下载与页面动态渲染的理想学习样本。
1. 项目缘起:一个“麻雀虽小,五脏俱全”的练手项目
最近在带几个刚学完JavaWeb基础的朋友做项目,发现他们普遍有个困惑:学了一大堆Servlet、JSP、JDBC、MVC的理论,但真到动手时,面对一个空白的IDEA项目,却不知道从何下手,感觉知识点都是散的,串不起来。他们需要一个能把这些技术点都“用起来”的、有完整流程的、但又不过于复杂的项目来练手。
于是,我决定带着他们一起,从零开始搭建一个“小型音乐网站”。这个项目的目标非常明确:不求功能多炫酷,但求流程完整、技术栈典型、能跑通从数据库到前端的全链路。它就像一个技术“骨架”,把JavaWeb的核心技术(Servlet、JSP、JDBC、MVC、Filter、Listener等)像拼图一样,严丝合缝地组装起来。做完这个项目,你就能清晰地理解一个Web应用是如何接收请求、处理业务、访问数据、最终渲染页面的,这对于建立完整的开发认知至关重要。
这个项目非常适合以下人群:刚刚学完JavaWeb基础,想找一个完整案例巩固的在校学生;准备面试,需要项目经验增加谈资的求职者;或者任何想从“看教程”过渡到“自己写”的入门开发者。我们使用的技术栈是经典的“JSP + Servlet + JavaBean”模式,数据库选用最普遍的MySQL,开发工具是IntelliJ IDEA,这些都是企业里最常见的组合,学会了可以直接迁移到更复杂的框架(如Spring MVC)学习中。
2. 环境搭建与项目初始化:从“新建项目”到“跑通Hello World”
万事开头难,一个清晰的项目初始环境能避免后续80%的诡异错误。我们使用IntelliJ IDEA Ultimate版(社区版对Web项目支持稍弱)作为开发工具。
2.1 创建Web项目与配置Tomcat
打开IDEA,选择File -> New -> Project。左侧选择Java Enterprise,确保已勾选Web Application,并选择正确的JDK版本(推荐JDK 8或11,兼容性最好)。项目创建后,IDEA会自动生成一个标准的Web项目结构:src目录放Java代码,web目录(或webapp)下会有WEB-INF和index.jsp。
接下来是核心一步:配置本地Tomcat服务器。点击IDEA右上角的Add Configuration...,点击+号,选择Tomcat Server -> Local。在Deployment标签页,点击+号选择Artifact,添加你的项目war包。这里有个关键点:Application context(应用上下文)最好设置为/或者/music,这决定了你访问项目的根路径。例如,设为/music,那么你的首页访问地址就是http://localhost:8080/music/。配置完成后,点击运行,如果能在浏览器看到默认的index.jsp页面,说明环境基本就绪。
注意:很多新手会卡在“404”错误上。请务必检查:1. Tomcat的
Deployment中是否成功添加了你的项目Artifact;2. 项目的web.xml中是否配置了欢迎页面(<welcome-file-list>);3. 访问的URL路径是否正确。
2.2 数据库设计与建表
我们的音乐网站功能很简单:用户能浏览音乐列表、根据歌名或歌手搜索、播放音乐(模拟)。因此,数据库只需要一张核心表。我们在MySQL中创建一个名为music_db的数据库,然后执行以下SQL建表语句:
CREATE TABLE `t_music` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `title` varchar(100) NOT NULL COMMENT '歌曲名称', `singer` varchar(50) NOT NULL COMMENT '歌手', `album` varchar(100) DEFAULT NULL COMMENT '专辑', `release_time` date DEFAULT NULL COMMENT '发行时间', `music_time` varchar(10) DEFAULT NULL COMMENT '时长(如04:30)', `music_url` varchar(255) NOT NULL COMMENT '音乐文件存储路径(相对路径)', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面图片存储路径', `play_count` int(11) DEFAULT '0' COMMENT '播放次数', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='音乐表';这里有几个设计考量:
id使用自增主键,是标准做法。music_url和cover_url存储的是相对服务器路径,例如/music/audio/xxx.mp3和/music/images/xxx.jpg。绝对路径不利于项目迁移。play_count默认为0,用于后续做简单的“热门歌曲”统计。- 字符集使用
utf8mb4,以支持存储Emoji等特殊字符,避免乱码。
建完表后,可以插入几条测试数据:
INSERT INTO `t_music` (`title`, `singer`, `album`, `release_time`, `music_time`, `music_url`, `cover_url`) VALUES ('起风了', '买辣椒也用券', '起风了', '2018-05-30', '04:25', '/music/audio/起风了.mp3', '/music/images/起风了.jpg'), ('孤勇者', '陈奕迅', '《英雄联盟:双城之战》动画剧集主题曲', '2021-11-08', '04:16', '/music/audio/孤勇者.mp3', '/music/images/孤勇者.jpg'), ('晴天', '周杰伦', '叶惠美', '2003-07-31', '04:29', '/music/audio/晴天.mp3', '/music/images/晴天.jpg');2.3 项目依赖管理与目录结构规划
在WEB-INF下创建lib文件夹,用于存放项目所需的JAR包。对于这个项目,我们至少需要:
- MySQL驱动包 (
mysql-connector-java-8.0.xx.jar):用于连接数据库。 - JSTL标签库包 (
jstl.jar和standard.jar):为了在JSP中避免使用<% %>脚本,我们使用JSTL和EL表达式,让页面更清晰。 druid-1.2.xx.jar:这是一个阿里巴巴开源的数据库连接池。强烈建议使用连接池而不是每次都创建新连接,它能显著提升性能和管理连接资源。
将上述JAR包复制到lib目录后,需要让IDEA识别它们。右键点击lib文件夹,选择Add as Library...。至此,一个干净、标准的JavaWeb项目骨架就搭建完成了。我们的目录结构看起来应该是这样的:
music-website-project ├── src │ └── (后续的Servlet、JavaBean等包) ├── web │ ├── WEB-INF │ │ ├── lib │ │ │ ├── mysql-connector-java-8.0.xx.jar │ │ │ ├── druid-1.2.xx.jar │ │ │ └── jstl.jar、standard.jar │ │ └── web.xml │ ├── index.jsp │ ├── audio (文件夹,存放音乐文件) │ ├── images (文件夹,存放封面图片) │ └── (其他的JSP页面) └── 项目配置文件3. 核心工具类与数据层:打造稳健的“数据引擎”
在动手写业务逻辑前,我们先搭建好底层基础设施,主要是数据库连接和通用的数据访问工具。这部分代码写好了,后面写业务会非常顺畅。
3.1 数据库连接池工具类
直接在Servlet里获取Connection再关闭,是极其低效且危险的做法。我们使用Druid连接池。首先,在src下创建一个工具类DBUtil.java:
package com.music.util; import com.alibaba.druid.pool.DruidDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { // 使用静态内部类实现单例模式的DataSource private static class Holder { private static final DruidDataSource INSTANCE = new DruidDataSource(); static { // 基础配置 INSTANCE.setUrl("jdbc:mysql://localhost:3306/music_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"); INSTANCE.setUsername("root"); // 你的数据库用户名 INSTANCE.setPassword("your_password"); // 你的数据库密码 INSTANCE.setDriverClassName("com.mysql.cj.jdbc.Driver"); // 连接池配置(根据实际需求调整) INSTANCE.setInitialSize(5); // 初始连接数 INSTANCE.setMinIdle(5); // 最小空闲连接 INSTANCE.setMaxActive(20); // 最大活跃连接 INSTANCE.setMaxWait(60000); // 获取连接超时时间(毫秒) // 其他可选配置,如检测连接有效性 INSTANCE.setValidationQuery("SELECT 1"); } } public static DataSource getDataSource() { return Holder.INSTANCE; } public static Connection getConnection() throws SQLException { return getDataSource().getConnection(); } }为什么这么写?这里使用了“静态内部类”的单例模式来创建DruidDataSource。这种方式能保证线程安全,且实现了懒加载(只有第一次调用getDataSource()时才会初始化)。配置参数中,serverTimezone=Asia/Shanghai是为了避免时区错误。validationQuery用于检测连接是否有效,防止程序拿到已断开的连接。
3.2 实体类与基础DAO设计
根据数据库表,创建对应的Java实体类Music.java,放在com.music.entity包下。这就是JavaBean,其属性与表字段一一对应,并包含getter/setter方法。
package com.music.entity; import java.util.Date; public class Music { private Integer id; private String title; private String singer; private String album; private Date releaseTime; // 注意:这里用Date类型,对应数据库的date private String musicTime; private String musicUrl; private String coverUrl; private Integer playCount; // 省略getter和setter方法... }接着,我们创建一个基础的DAO(Data Access Object)接口和实现,用于封装所有对t_music表的操作。先定义接口MusicDao.java:
package com.music.dao; import com.music.entity.Music; import java.util.List; public interface MusicDao { // 查询所有音乐 List<Music> findAll(); // 根据ID查询音乐 Music findById(int id); // 根据歌名或歌手模糊查询 List<Music> findByKeyword(String keyword); // 增加播放次数 boolean increasePlayCount(int id); }然后是实现类MusicDaoImpl.java。这里会大量用到DBUtil.getConnection()和PreparedStatement来防止SQL注入。
package com.music.dao.impl; import com.music.dao.MusicDao; import com.music.entity.Music; import com.music.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class MusicDaoImpl implements MusicDao { @Override public List<Music> findAll() { List<Music> list = new ArrayList<>(); String sql = "SELECT * FROM t_music ORDER BY id DESC"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql); ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { list.add(convertResultSetToMusic(rs)); } } catch (SQLException e) { e.printStackTrace(); } return list; } @Override public List<Music> findByKeyword(String keyword) { List<Music> list = new ArrayList<>(); // 使用LIKE进行模糊查询,%放在参数两侧 String sql = "SELECT * FROM t_music WHERE title LIKE ? OR singer LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, "%" + keyword + "%"); pstmt.setString(2, "%" + keyword + "%"); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { list.add(convertResultSetToMusic(rs)); } } } catch (SQLException e) { e.printStackTrace(); } return list; } // 将ResultSet的一行数据转换为Music对象 private Music convertResultSetToMusic(ResultSet rs) throws SQLException { Music music = new Music(); music.setId(rs.getInt("id")); music.setTitle(rs.getString("title")); music.setSinger(rs.getString("singer")); music.setAlbum(rs.getString("album")); music.setReleaseTime(rs.getDate("release_time")); // 注意是getDate music.setMusicTime(rs.getString("music_time")); music.setMusicUrl(rs.getString("music_url")); music.setCoverUrl(rs.getString("cover_url")); music.setPlayCount(rs.getInt("play_count")); return music; } // 其他方法实现类似,此处省略... }关键点解析:
- 使用Try-with-resources:在
try()括号中声明Connection,PreparedStatement,ResultSet,无论是否发生异常,它们都会在try块结束后自动关闭,避免资源泄漏。这是Java 7后的最佳实践。 - 使用PreparedStatement:所有涉及变量的SQL都必须用它,它能预编译SQL并安全地设置参数,从根本上杜绝SQL注入。
- 结果集到对象的转换:单独抽出一个
convertResultSetToMusic方法,避免了在每个查询方法里重复写一堆set代码,更清晰。
4. 控制器与业务逻辑:Servlet如何扮演“交通指挥”
在MVC模式中,Servlet充当控制器(Controller)。它接收来自浏览器的请求(Request),调用相应的业务层(Service)或数据层(DAO)处理,然后将结果数据放入请求域或会话域,最后转发(Forward)或重定向(Redirect)到JSP页面进行展示。
4.1 首页音乐列表展示
首先,我们创建一个MusicListServlet,用于处理首页请求,展示所有音乐列表。
package com.music.controller; import com.music.dao.MusicDao; import com.music.dao.impl.MusicDaoImpl; import com.music.entity.Music; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @WebServlet("/music/list") public class MusicListServlet extends HttpServlet { private MusicDao musicDao = new MusicDaoImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 调用DAO,获取数据 List<Music> musicList = musicDao.findAll(); // 2. 将数据存入请求域,供JSP页面获取 req.setAttribute("musicList", musicList); // 3. 请求转发到展示页面 req.getRequestDispatcher("/music_list.jsp").forward(req, resp); } }代码解读:
@WebServlet("/music/list"):这是Servlet 3.0的注解配置,它告诉容器,当访问/music/list这个路径时,由这个Servlet来处理。这替代了传统的在web.xml中配置<servlet>和<servlet-mapping>的方式,更简洁。doGet方法:处理HTTP GET请求。我们的首页访问就是一个GET请求。req.setAttribute("musicList", musicList):这是核心!我们将查询到的musicList对象以键值对的形式放入了HttpServletRequest的请求域中。这个数据只在本次请求范围内有效。req.getRequestDispatcher("/music_list.jsp").forward(req, resp):请求转发。服务器内部将请求和响应对象传递给/music_list.jsp页面,由该页面负责渲染输出。浏览器的地址栏不会改变。
接下来,创建对应的JSP页面music_list.jsp,放在web目录下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>音乐列表</title> <style> /* 简单的CSS样式,让页面好看点 */ table { border-collapse: collapse; width: 80%; margin: 20px auto; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } img { width: 60px; height: 60px; object-fit: cover; } </style> </head> <body> <h2 style="text-align: center;">音乐库</h2> <table> <tr> <th>封面</th> <th>歌名</th> <th>歌手</th> <th>专辑</th> <th>时长</th> <th>播放</th> </tr> <%-- 使用JSTL的c:forEach遍历musicList --%> <c:forEach var="music" items="${musicList}"> <tr> <td><img src="${pageContext.request.contextPath}${music.coverUrl}" alt="${music.title}封面" onerror="this.src='default.jpg'"></td> <td>${music.title}</td> <td>${music.singer}</td> <td>${music.album}</td> <td>${music.musicTime}</td> <td> <%-- 播放按钮,点击后跳转到播放Servlet,并传递音乐ID --%> <a href="${pageContext.request.contextPath}/music/play?id=${music.id}">播放</a> </td> </tr> </c:forEach> </table> </body> </html>JSP页面关键点:
<%@ taglib prefix="c" uri="..." %>:引入JSTL核心标签库。有了它,我们才能使用<c:forEach>这样的标签。${musicList}:这是EL表达式,它会自动从页面上下文(PageContext)、请求域(Request)、会话域(Session)、应用域(Application)中寻找名为musicList的属性。因为我们之前在Servlet里用req.setAttribute存入了,所以这里能直接取到。${pageContext.request.contextPath}:这个表达式会动态获取当前Web应用的上下文路径。比如你的应用叫music,它就会输出/music。这是解决路径问题的黄金法则。用它拼接资源路径(如图片、CSS、JS、链接),无论项目部署在什么路径下,都能正确访问。onerror="this.src='default.jpg'":一个实用的小技巧。如果图片路径错误加载失败,会自动替换为一张默认图片,避免页面出现破损图标。
现在,启动Tomcat,访问http://localhost:8080/music/music/list,你应该能看到一个包含测试音乐数据的表格页面。恭喜你,第一个完整的“请求-处理-响应”链路跑通了!
4.2 搜索功能实现
搜索功能是另一个典型的Servlet应用。我们创建一个MusicSearchServlet。
@WebServlet("/music/search") public class MusicSearchServlet extends HttpServlet { private MusicDao musicDao = new MusicDaoImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 从请求中获取用户输入的搜索关键词 String keyword = req.getParameter("keyword"); // 2. 参数校验(简单处理空值) if (keyword == null || keyword.trim().isEmpty()) { // 如果关键词为空,可以重定向回首页,或者展示空结果 resp.sendRedirect(req.getContextPath() + "/music/list"); return; } // 3. 调用DAO进行模糊查询 List<Music> resultList = musicDao.findByKeyword(keyword.trim()); // 4. 将结果和关键词存入请求域 req.setAttribute("musicList", resultList); req.setAttribute("keyword", keyword.trim()); // 5. 转发到同一个展示页面(复用music_list.jsp) req.getRequestDispatcher("/music_list.jsp").forward(req, resp); } }在music_list.jsp页面顶部,我们加上一个搜索框:
<div style="text-align: center; margin: 20px;"> <form action="${pageContext.request.contextPath}/music/search" method="get"> <input type="text" name="keyword" placeholder="输入歌名或歌手..." value="${param.keyword}"> <input type="submit" value="搜索"> </form> </div>这里有个细节:value="${param.keyword}"。param是EL表达式的内置对象,代表请求参数。这样设置后,在搜索结果显示页面,搜索框里会保留用户刚才输入的关键词,体验更好。
4.3 音乐播放与播放次数统计
播放功能稍微复杂一点,它涉及两个动作:一是跳转到播放页面(或直接播放),二是记录播放次数。我们设计两个Servlet协作。
首先,MusicPlayServlet负责处理播放请求,并增加播放次数。
@WebServlet("/music/play") public class MusicPlayServlet extends HttpServlet { private MusicDao musicDao = new MusicDaoImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String idStr = req.getParameter("id"); if (idStr == null || !idStr.matches("\\d+")) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "无效的音乐ID"); return; } int musicId = Integer.parseInt(idStr); // 1. 增加播放次数 boolean success = musicDao.increasePlayCount(musicId); // 2. 根据ID查询音乐详细信息 Music music = musicDao.findById(musicId); if (music == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "音乐不存在"); return; } // 3. 将音乐信息存入请求域,并转发到播放页面 req.setAttribute("music", music); req.getRequestDispatcher("/play.jsp").forward(req, resp); } }在MusicDaoImpl中实现increasePlayCount方法:
@Override public boolean increasePlayCount(int id) { String sql = "UPDATE t_music SET play_count = play_count + 1 WHERE id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, id); int rows = pstmt.executeUpdate(); return rows > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }然后,创建一个简单的播放页面play.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>播放 - ${music.title}</title> </head> <body> <h2>正在播放: ${music.title} - ${music.singer}</h2> <div> <img src="${pageContext.request.contextPath}${music.coverUrl}" alt="封面" width="200"> </div> <div> <%-- 使用HTML5的audio标签播放音乐 --%> <audio controls autoplay> <source src="${pageContext.request.contextPath}${music.musicUrl}" type="audio/mpeg"> 您的浏览器不支持 audio 元素。 </audio> </div> <p>专辑:${music.album} | 发行时间:${music.releaseTime} | 播放次数:${music.playCount}</p> <a href="${pageContext.request.contextPath}/music/list">返回列表</a> </body> </html>技术要点:
- 幂等性考虑:
/music/play这个接口如果被刷新,会再次增加播放次数。在实际项目中,这通常需要结合会话(Session)或更复杂的机制(如用户ID+音乐ID去重)来防止刷量。这里为了简化,我们暂时忽略。 - HTML5 Audio标签:这是最简单的前端播放方案。
controls属性提供播放控件,autoplay让音乐自动播放。source标签指定音频源。你需要确保music_url字段的路径正确,并且音频文件(如MP3)确实放在项目的web/audio/目录下。
5. 项目优化与进阶思考:从“能跑”到“好用”
一个基础功能跑通的项目,就像毛坯房。接下来,我们进行一些必要的“装修”,让它更健壮、更易用。
5.1 统一字符编码过滤器
中文乱码是JavaWeb开发中最常见的问题之一。GET和POST请求的乱码处理方式不同,我们写一个CharacterEncodingFilter统一处理。
package com.music.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter("/*") // 过滤所有请求 public class CharacterEncodingFilter implements Filter { private String encoding = "UTF-8"; @Override public void init(FilterConfig filterConfig) throws ServletException { // 可以从web.xml读取配置的编码,这里简化处理 String configEncoding = filterConfig.getInitParameter("encoding"); if (configEncoding != null) { this.encoding = configEncoding; } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 1. 设置请求和响应的编码 request.setCharacterEncoding(encoding); response.setCharacterEncoding(encoding); response.setContentType("text/html;charset=" + encoding); // 2. 放行请求,传递给下一个过滤器或Servlet chain.doFilter(request, response); } @Override public void destroy() { // 清理资源 } }关键点:
@WebFilter("/*"):注解配置,表示这个过滤器会拦截所有请求(/*)。chain.doFilter(request, response):这行代码至关重要,它表示将处理后的请求和响应传递给过滤器链中的下一个组件(可能是另一个过滤器,也可能是最终的Servlet)。如果忘记写这行,请求就会在这个过滤器被截断,无法到达后续处理环节。- 这个过滤器同时处理了请求参数(
request.setCharacterEncoding)和响应输出(response.setCharacterEncoding和setContentType)的编码,一劳永逸。
5.2 使用JSTL与EL表达式彻底告别JSP脚本
在早期的JSP中,我们经常看到<% ... %>这种Java脚本片段。这种做法将业务逻辑和展示逻辑混在一起,难以维护,是典型的“Model 1”架构的弊端。现代JavaWeb开发强烈建议使用JSTL+EL来替代。
我们已经在前面的JSP中使用了<c:forEach>和${}。JSTL还有很多其他有用的标签,比如条件判断<c:if>、<c:choose>,格式化日期和数字的<fmt:formatDate>等。例如,在music_list.jsp中,我们可以优化日期显示和空结果提示:
<%-- 引入JSTL格式化标签库 --%> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%-- 在表格循环体内,格式化日期 --%> <td><fmt:formatDate value="${music.releaseTime}" pattern="yyyy-MM-dd"/></td> <%-- 在表格前,判断列表是否为空 --%> <c:if test="${empty musicList}"> <p style="text-align: center; color: gray;">没有找到相关的音乐。</p> </c:if>5.3 静态资源处理与路径问题终极解决方案
我们的音乐文件(MP3)和图片文件(JPG)都是静态资源。在JavaWeb项目中,静态资源通常直接放在web目录或其子目录下(如web/audio/,web/images/)。Tomcat容器内置了一个叫做DefaultServlet的Servlet,它会自动处理对这些静态资源的请求。
但是,路径问题经常困扰新手。记住一个原则:所有在JSP/HTML中引用的资源路径(src, href, action),最好都使用${pageContext.request.contextPath}作为前缀。
例如:
- 错误的写法:
<img src="/images/xxx.jpg">。如果应用部署在/music下,它会去请求http://localhost:8080/images/xxx.jpg,这显然是错的。 - 正确的写法:
<img src="${pageContext.request.contextPath}/images/xxx.jpg">。它会动态生成/music/images/xxx.jpg,永远正确。
对于CSS和JS文件,也建议放在web/css/和web/js/下,并用同样的方式引用。
5.4 项目部署与数据库连接池配置优化
开发完成后,我们需要将项目打包成WAR文件,部署到独立的Tomcat服务器上。在IDEA中,你可以通过Build -> Build Artifacts...来生成WAR包。
部署时一个常见的坑是数据库连接池配置。我们在DBUtil.java中硬编码了数据库连接信息(URL、用户名、密码)。这在生产环境是不安全的,也不利于不同环境(开发、测试、生产)切换。标准的做法是将这些配置外置到WEB-INF下的一个属性文件(如druid.properties)中,或者使用JNDI(Java Naming and Directory Interface)让Tomcat来管理数据源。
这里给出使用属性文件的改进方案:
- 在
src目录(或WEB-INF/classes)下创建druid.properties:druid.url=jdbc:mysql://localhost:3306/music_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai druid.username=root druid.password=your_password druid.initialSize=5 druid.minIdle=5 druid.maxActive=20 - 修改
DBUtil.java的静态初始化块,从属性文件读取配置:static { try (InputStream is = DBUtil.class.getClassLoader().getResourceAsStream("druid.properties")) { Properties props = new Properties(); props.load(is); INSTANCE.setUrl(props.getProperty("druid.url")); INSTANCE.setUsername(props.getProperty("druid.username")); // ... 设置其他属性 } catch (IOException e) { throw new RuntimeException("加载Druid配置失败", e); } }
这样,在部署到不同环境时,只需替换druid.properties文件即可,无需重新编译代码。
6. 常见问题排查与调试技巧
即使按照步骤操作,你也可能会遇到一些问题。这里列出几个高频问题及其排查思路。
问题1:访问Servlet报404错误。
- 检查1:Servlet路径映射是否正确。确认
@WebServlet注解里的路径,或者web.xml里的<url-pattern>。访问的URL是否与之匹配?注意大小写。 - 检查2:项目是否成功部署到Tomcat。查看IDEA的Tomcat控制台启动日志,确认你的项目Context Path是什么。访问
http://localhost:8080/你的ContextPath/Servlet路径。 - 检查3:编译后的类文件是否存在。检查Tomcat的工作目录(通常是
[Tomcat安装目录]/webapps/[你的项目]/WEB-INF/classes)下,你的Servlet类文件(.class)是否存在。如果不存在,可能是IDEA没有自动编译部署,尝试Build -> Rebuild Project。
问题2:页面显示中文乱码。
- 确保过滤器生效:检查
CharacterEncodingFilter是否被正确配置和加载。可以在doFilter方法开始加一句System.out.println("过滤器执行");来调试。 - 检查数据库连接字符串:JDBC URL中必须包含
?useUnicode=true&characterEncoding=UTF-8。 - 检查数据库、表、字段的字符集:确保它们都是
utf8mb4。 - 检查JSP文件本身的编码:在IDEA中,右键点击JSP文件,选择
File Encoding,确保是UTF-8,并且“Transparent native-to-ascii conversion”最好勾选上。
问题3:静态资源(图片、音乐)无法加载。
- 检查文件路径:使用浏览器开发者工具(F12),查看“网络(Network)”标签页,看看加载资源时返回的是404还是其他错误。核对请求的URL和文件实际存放的路径。
- 检查Tomcat配置:有时Tomcat的
server.xml中配置了额外的静态资源路径,可能会产生冲突。但我们的项目结构是标准的,一般不会。 - 权限问题(Linux/Mac):确保Tomcat进程有权限读取你项目
web目录下的文件。
问题4:增加播放次数后,页面显示的数字没变。
- 这是一个典型的“请求转发 vs 重定向”理解问题。在我们的
MusicPlayServlet中,增加播放次数后,我们使用了req.getRequestDispatcher("/play.jsp").forward(req, resp);即请求转发。这意味着播放页面 (play.jsp) 和Servlet在同一次请求中。play.jsp页面里${music.playCount}显示的是我们从数据库重新查询出来的Music对象里的playCount,这个值已经是增加后的了。所以逻辑是正确的。 - 如果你发现数字没变,可能是以下原因:
- 数据库更新失败:检查
increasePlayCount方法中的SQL执行是否成功,查看控制台是否有异常。 - 浏览器缓存:刷新页面试试。
- 事务问题(本例不涉及):如果后续引入Service层和事务,需要确保更新和查询在同一个事务内,才能读到最新的数据。
- 数据库更新失败:检查
这个小型音乐网站项目,虽然功能简单,但它完整地串联了JavaWeb开发的核心流程和技术点。从环境搭建、数据库设计、数据访问层(DAO)、控制器(Servlet)、到视图层(JSP),再到过滤器、路径处理等细节,每一步都是企业级开发的缩影。通过亲手实现它,你不仅能巩固技术,更能建立起对Web应用架构的直观理解。接下来,你可以尝试在此基础上扩展更多功能,比如用户登录注册、音乐收藏、分页查询、使用Ajax实现无刷新播放等,让这个“骨架”逐渐丰满起来。
本文还有配套的精品资源,点击获取