简介:这是一套基于JSP技术实现的轻量级Web相册管理系统源码,面向Java Web初学者与课程设计实践者,解决照片上传、分类管理、在线浏览与用户交互等典型Web应用需求。资源包共115个文件,含20个JSP页面(负责前端展示与简单逻辑)、22个Java源文件与对应class字节码(涵盖PhotoServlet、AdminServlet等核心业务控制器及PhotoBiz、LeiBieBiz等业务组件)、26张示例图片与5个GIF动图(用于界面占位与效果示意),以及SQL建表脚本、web.xml配置、JDBC连接工厂等关键支撑文件,整体压缩包仅3.53MB,结构清晰、模块职责分明。已有434人学习下载,适合通过完整MVC分层实践掌握JSP+Servlet+MySQL整合开发流程,深入理解数据库连接池、用户权限控制、相册分类与评论功能的代码实现,并可直接部署于Tomcat环境进行本地调试与二次开发。
1. 一个十年前的老项目,现在跑通它反而成了最硬的实战考题
上周帮某高校实验室迁移一批教学 Demo,翻出这个「基于 JSP 的相册管理系统」源码包——压缩包里连web.xml都带着 DTD 声明,<%@ page contentType="text/html;charset=GBK"%>还在 header 里倔强地活着。本以为是过时的玩具,结果部署到 Tomcat 9 时卡在登录跳转死循环,查日志发现是RequestDispatcher.forward()被Filter拦了三次;改完又崩在 MySQL 8 的mysql-connector-java驱动兼容性上,报错java.sql.SQLException: Unknown system variable 'query_cache_size'。折腾六小时才让首页相册缩略图正常加载。这项目不是古董,它是 Java Web 基础链路的「全栈压力测试仪」:从 JSP 编译、Servlet 生命周期、JDBC 连接池、MySQL 字符集、再到 Filter/Listener 协作机制,一环断,全盘停。适合刚学完《Java Web 编程技术》但没真正串起请求链路的开发者,也适合想快速验证老系统迁移风险的运维同学——它不炫技,但每一步都踩在真实生产环境的兼容性刀尖上。
2. 技术栈解剖:为什么用 JSP 而不是 JSF?这个选择决定了整个项目的呼吸节奏
提示:标题里写的「jsf 相册」是典型关键词误标。实际代码中完全没出现
javax.faces.*包,所有页面都是.jsp后缀,控制器用的是HttpServlet子类,web.xml中<servlet-class>指向com.album.servlet.LoginServlet。JSF 是组件化框架,而本项目是经典 MVC 三层裸写:JSP 做 View(含大量<%= %>脚本片段),Servlet 做 Controller(处理表单、转发),DAO 层直连 MySQL(无 Hibernate/MyBatis)。这种结构对新手更透明,但对现代 IDE 支持差——IntelliJ 会把 JSP 里的 Java 代码标成红色警告,因为默认不启用 JSP 编译器。
2.1 目录结构即架构:WebContent/下藏着所有运行时真相
项目解压后根目录结构如下(已过滤.idea、target等无关文件):
album-system/ ├── src/ # Java 源码 │ └── com/album/ │ ├── bean/ # Javabean:Album、Photo、User │ ├── dao/ # 数据访问层:AlbumDAO.java、PhotoDAO.java │ ├── servlet/ # 控制器:LoginServlet.java、UploadServlet.java │ └── util/ # 工具类:DBUtil.java(含 getConnection()) ├── WebContent/ # Web 根目录(Tomcat 部署时的 webapp) │ ├── css/ # style.css(仅 3 个 class:.album-list, .photo-item, .upload-form) │ ├── images/ # 用户上传的相册图片(部署后为空) │ ├── js/ # upload.js(含 FormData 提交逻辑) │ ├── WEB-INF/ │ │ ├── web.xml # 关键!定义 Servlet 映射、Filter、Listener │ │ └── lib/ # mysql-connector-java-5.1.38.jar(注意版本!) │ ├── index.jsp # 首页:展示相册列表(调用 AlbumDAO.getAllAlbums()) │ ├── login.jsp # 登录页(form action="/login" → LoginServlet) │ ├── album-detail.jsp # 相册详情页(URL 参数 ?id=123) │ └── upload.jsp # 上传页(含 <input type="file">) └── README.txt # 仅一行:"数据库脚本见 sql/album.sql"这个结构暴露了两个关键事实:第一,没有构建工具——src/下的 Java 类直接被 Tomcat 的 JSP 编译器和WEB-INF/classes/加载器处理,不存在 Maven 的compile阶段;第二,静态资源与动态资源物理隔离——css/和js/在WebContent/下,而 Java 类在WEB-INF/classes/,符合 Servlet 规范,但导致路径引用极易出错(比如upload.jsp里写<script src="js/upload.js">是对的,但若写成<script src="../js/upload.js">就 404)。
2.2 核心依赖版本:MySQL 5.7 是它的舒适区,跨到 8.x 就要动手术
项目WEB-INF/lib/下只有一份 JDBC 驱动:mysql-connector-java-5.1.38.jar。这是决定能否连上数据库的生死线。我们来对比三个常见场景:
| 场景 | MySQL 版本 | 驱动版本 | 结果 | 原因 |
|---|---|---|---|---|
| 教学机房环境 | 5.7.21 | 5.1.38 | ✅ 正常连接 | com.mysql.jdbc.Driver类存在,协议兼容 |
| 本地 Docker | 8.0.33 | 5.1.38 | ❌ 启动报Unknown system variable 'query_cache_size' | MySQL 8 移除了 query_cache 相关变量,旧驱动仍尝试读取 |
| 本地 Docker | 8.0.33 | 8.0.33 | ✅ 但需改DBUtil.java | 驱动类名变为com.mysql.cj.jdbc.Driver,URL 必须加?serverTimezone=UTC |
DBUtil.java中的关键代码段如下(原始版):
// src/com/album/util/DBUtil.java(原始) public static Connection getConnection() { Connection conn = null; try { Class.forName("com.mysql.jdbc.Driver"); // ← 这行在 MySQL 8 下会 ClassNotFoundException conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/albumdb?useUnicode=true&characterEncoding=UTF-8", "root", "123456" ); } catch (Exception e) { e.printStackTrace(); } return conn; }必须修改两处:
Class.forName("com.mysql.cj.jdbc.Driver")—— 注意cj(Connector/J)后缀;- URL 补全时区参数:
"jdbc:mysql://localhost:3306/albumdb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"。
漏掉任一,getConnection()返回null,后续 DAO 全部NullPointerException。
2.3 web.xml:那个被现代框架遗忘的“总调度台”
WebContent/WEB-INF/web.xml是本项目真正的中枢神经。它不只注册 Servlet,还通过Filter控制字符编码、用Listener监听应用启动加载数据库连接池(虽然本项目没做池化,但预留了ContextLoaderListener的壳)。截取关键片段:
<!-- web.xml 片段 --> <filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.album.filter.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 注意:这里匹配所有请求 --> </filter-mapping> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.album.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> <!-- 浏览器访问 /login → 走这个 Servlet --> </servlet-mapping> <listener> <listener-class>com.album.listener.AppContextListener</listener-class> </listener>这个配置意味着:每个 HTTP 请求(包括/css/style.css)都会先经过EncodingFilter。如果EncodingFilter.doFilter()里写了request.setCharacterEncoding("UTF-8"),那对静态资源是无效且多余的,但不会报错;可一旦doFilter()里有response.getWriter().write("hello")这种操作,就会触发IllegalStateException: getWriter() has already been called—— 因为 Tomcat 已经为 CSS 文件打开了输出流。这是新手最容易翻车的点:以为 Filter 只处理 JSP/Servlet,其实它吃下整个/*。
3. 运行实操:从零部署到首页相册列表渲染的七步闭环
部署不是复制粘贴,而是理解每个环节如何咬合。以下步骤在 Windows 10 + IntelliJ IDEA 2023.2 + Tomcat 9.0.83 + MySQL 8.0.33 环境下实测通过(Linux/macOS 命令微调即可)。
3.1 第一步:初始化数据库,字符集必须显式指定
不要依赖 MySQL 默认配置。执行sql/album.sql前,先确认当前库字符集:
-- 进入 MySQL 命令行 CREATE DATABASE albumdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE albumdb; -- 再执行建表语句(album.sql 内容)注意:
utf8mb4而非utf8。MySQL 的utf8实际只支持 3 字节 UTF-8 字符(不支持 emoji),而utf8mb4才是完整 UTF-8。album.sql中建表语句若写CHARSET=utf8,必须手动改为CHARSET=utf8mb4,否则中文插入会变???。
3.2 第二步:替换 JDBC 驱动并修正 DBUtil
- 删除
WEB-INF/lib/mysql-connector-java-5.1.38.jar; - 下载
mysql-connector-java-8.0.33.jar,放入WEB-INF/lib/; - 修改
src/com/album/util/DBUtil.java,将Class.forName(...)和 URL 按前文要求更新; - 关键动作:在 IntelliJ 中右键
WEB-INF/lib/→Add as Library...,确保编译期能 resolvecom.mysql.cj.jdbc.Driver。
3.3 第三步:配置 Tomcat 的 JVM 参数,绕过 JSP 编译玄学
Tomcat 9 默认使用org.apache.jasper.compiler.JspCompiler,但对老旧 JSP 中的<%! int count = 0; %>这类声明式脚本支持不稳定。在 IntelliJ 的 Tomcat 配置页 →Configuration→VM options中添加:
-Dorg.apache.jasper.compiler.Parser.STRICT_QUOTE_ESCAPING=false -Dorg.apache.jasper.compiler.Generator.SAVE_PRECOMPILED=false这两项关闭严格引号转义和预编译缓存,能避免index.jsp中<img src="<%= photo.getPath() %>">报org.apache.jasper.JasperException: /index.jsp (line 42) quote symbol expected。
3.4 第四步:解决 JSP 页面的 GBK 与 UTF-8 冲突
原始index.jsp头部是:
<%@ page language="java" contentType="text/html; charset=GBK" pageEncoding="GBK"%>但数据库和 HTML 页面实际用 UTF-8。必须统一为 UTF-8:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <!-- 双重保险 -->否则浏览器解析 HTML 时用 UTF-8,但 JSP 引擎用 GBK 解码<%= album.getName() %>,中文显示为乱码。
3.5 第五步:检查 web.xml 中的 URL 映射是否覆盖全部入口
web.xml中LoginServlet映射为/login,但login.jsp表单写的是:
<form action="login" method="post"> <!-- 注意:没有前置斜杠 -->这会导致:当用户从http://localhost:8080/album-system/login.jsp提交时,浏览器实际请求http://localhost:8080/album-system/login(相对路径),而 Tomcat 期望的是http://localhost:8080/login(绝对路径)。解决方案只有两个:
- 改 JSP:
<form action="/login" method="post">(加/); - 或改 web.xml:
<url-pattern>/album-system/login</url-pattern>(不推荐,耦合部署路径)。
选前者,这是标准实践。
3.6 第六步:启动 Tomcat 并验证日志中的三个关键信号
启动后,紧盯catalina.out(或 IntelliJ Console):
- 出现
INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [...]→ 应用开始部署; - 出现
SEVERE [main] org.apache.catalina.core.StandardContext.startInternal One or more Filters failed to start.→ Filter 初始化失败,回查EncodingFilter.init(); - 出现
INFO [main] org.apache.catalina.core.ApplicationContext.log ContextListener: contextInitialized()→AppContextListener启动成功,说明web.xml解析无误。
没有第 2 条,且第 1、3 条都有,基本可认为部署成功。
3.7 第七步:手工触发一次相册创建,确认数据链路贯通
打开浏览器访问http://localhost:8080/album-system/login.jsp,用默认账号admin/123456登录。登录成功后跳转到index.jsp,此时应看到空相册列表。点击「新建相册」按钮(实际是链接<a href="create-album.jsp">),填写名称「测试相册」提交。此时观察:
- 数据库
album表应新增一条记录,name字段为「测试相册」; index.jsp刷新后,列表中出现「测试相册」;- 点击该相册,进入
album-detail.jsp,URL 为?id=1,页面显示「暂无照片」。
这七步走完,证明从浏览器输入、Filter 拦截、Servlet 处理、DAO 写库、JSP 渲染的全链路已打通。任何一步卡住,都不是配置问题,而是对 Servlet 生命周期的理解断点了。
4. 避坑指南:那些让我重启三次 Tomcat 的血泪经验
4.1 现象:登录成功后跳转到空白页,浏览器地址栏显示http://localhost:8080/album-system/,但页面什么也不显示
原因:LoginServlet中response.sendRedirect("index.jsp")是相对重定向,而当前请求 URL 是http://localhost:8080/album-system/login,重定向后变成http://localhost:8080/album-system/index.jsp。但web.xml中未配置index.jsp的 welcome-file,Tomcat 默认只认index.html/index.htm/index.jsp作为根路径欢迎页,而album-system/下的index.jsp需要显式映射。
解决:在web.xml<web-app>根节点内添加:
<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>4.2 现象:上传照片后,images/目录下生成了文件,但album-detail.jsp中<img src="images/xxx.jpg">404
原因:images/是WebContent/下的静态资源目录,按 Servlet 规范,它应该能被直接访问。但 IntelliJ 默认部署时,WebContent/下的images/不会被自动复制到out/artifacts/输出目录。
解决:在 IntelliJ 中,File → Project Structure → Artifacts,展开album-system:war exploded,在Output Layout标签页右侧点击+ → Directory Content,选择项目根目录下的WebContent/images/,确定。这样打包时images/才会进war。
4.3 现象:AlbumDAO.getAllAlbums()返回空集合,但数据库明明有数据
原因:DBUtil.getConnection()返回null,但 DAO 方法里没判空,直接调conn.prepareStatement(...)→NullPointerException。而try-catch把异常吞掉了,只打印堆栈,前端无感知。
解决:在AlbumDAO.java的每个方法开头加防御性检查:
public List<Album> getAllAlbums() { Connection conn = DBUtil.getConnection(); if (conn == null) { // ← 关键防御 System.err.println("DB connection failed in AlbumDAO.getAllAlbums()"); return new ArrayList<>(); } // ... 后续逻辑 }4.4 现象:upload.jsp中 JavaScript 提交FormData,但UploadServlet的request.getParameter("albumId")总是null
原因:FormData提交的是multipart/form-data编码,getParameter()只能读application/x-www-form-urlencoded数据。albumId是普通字段,但request对象未被MultipartRequest包装。
解决:在UploadServlet的doPost()中,用 Apache Commons FileUpload 解析:
// 需引入 commons-fileupload-1.5.jar 和 commons-io-2.11.0.jar DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); List<FileItem> items = upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { String fieldName = item.getFieldName(); // "albumId" String value = item.getString("UTF-8"); // ← 必须指定编码 if ("albumId".equals(fieldName)) { albumId = Integer.parseInt(value); } } }4.5 现象:修改index.jsp后刷新页面,内容不变
原因:Tomcat 的 JSP 编译缓存未清除。JSP 第一次访问时被编译为org.apache.jsp.index_jsp.java,放在work/Catalina/localhost/album-system/org/apache/jsp/下,即使改了.jsp,只要.java和.class还在,Tomcat 就不重新编译。
解决:停止 Tomcat → 删除work/Catalina/localhost/album-system/整个目录 → 重启。或者,在conf/context.xml中<Context>节点加reloadable="true"(仅开发环境用,性能损耗大)。
5. 二次开发实战:给老系统加「软删除」功能,三步落地不伤筋骨
现在需求来了:运营同学说「删错相册太频繁,希望加回收站功能」。这不是推倒重来,而是在现有 DAO 层打补丁。核心思路是——不动表结构,只加逻辑标记;不动 Servlet 流程,只改 DAO 方法;不碰 JSP 渲染,只增一个「恢复」按钮。全程不改web.xml,不加新 Servlet,不引入新框架。
5.1 第一步:数据库层面,用is_deleted字段实现软删除
album表原结构(来自album.sql):
CREATE TABLE album ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );执行 SQL 新增字段:
ALTER TABLE album ADD COLUMN is_deleted TINYINT(1) DEFAULT 0 COMMENT '0-未删除, 1-已删除';注意:
TINYINT(1)在 MySQL 中是整数类型,不是布尔型。Java 中用int接收,值为0或1。别用BOOLEAN,那是别名,底层还是TINYINT。
5.2 第二步:DAO 层改造,分离「物理查询」与「逻辑查询」
AlbumDAO.java原getAllAlbums()方法返回所有相册,现在要改成「只查未删除的」。但历史代码如LoginServlet可能依赖「查所有」,不能直接改。所以采用方法重载 + 命名区分:
// src/com/album/dao/AlbumDAO.java // 原方法保留,供内部管理用(比如回收站页面需要查全部) public List<Album> getAllAlbums() { String sql = "SELECT * FROM album ORDER BY create_time DESC"; // ... 执行查询 } // 新增方法:业务主流程用这个 public List<Album> getActiveAlbums() { String sql = "SELECT * FROM album WHERE is_deleted = 0 ORDER BY create_time DESC"; // ... 执行查询(复用原模板代码,只改 SQL) } // 新增软删除方法 public boolean softDeleteAlbum(int albumId) { String sql = "UPDATE album SET is_deleted = 1 WHERE id = ?"; // ... 执行 update,返回影响行数 > 0 则成功 } // 新增恢复方法 public boolean restoreAlbum(int albumId) { String sql = "UPDATE album SET is_deleted = 0 WHERE id = ?"; // ... 执行 update }关键细节:getActiveAlbums()替换所有业务调用点。全局搜索AlbumDAO.getAllAlbums(),找到index.jsp对应的 Java 脚本段:
<% AlbumDAO dao = new AlbumDAO(); List<Album> albums = dao.getAllAlbums(); // ← 改这里 %>改为:
<% AlbumDAO dao = new AlbumDAO(); List<Album> albums = dao.getActiveAlbums(); // ← 仅改这一行 %>5.3 第三步:JSP 层增加「删除/恢复」双态按钮,用 CSS 控制显隐
在index.jsp相册列表项中,原删除按钮是:
<a href="delete-album.jsp?id=<%= album.getId() %>">删除</a>现在改为条件渲染:
<% if (album.getIsDeleted() == 0) { // 未删除状态 %> <a href="delete-album.jsp?id=<%= album.getId() %>" onclick="return confirm('确定删除?')">删除</a> | <% } else { // 已删除状态 %> <a href="restore-album.jsp?id=<%= album.getId() %>" onclick="return confirm('确定恢复?')">恢复</a> | <% } %> <a href="album-detail.jsp?id=<%= album.getId() %>">查看</a>同时,为delete-album.jsp和restore-album.jsp新建两个极简 Servlet:
DeleteAlbumServlet.java:调AlbumDAO.softDeleteAlbum(id),然后response.sendRedirect("index.jsp");RestoreAlbumServlet.java:调AlbumDAO.restoreAlbum(id),同样重定向。
不用改web.xml,因为这两个 Servlet 的 URL 映射可以复用现有模式:在web.xml中新增:
<servlet> <servlet-name>DeleteAlbumServlet</servlet-name> <servlet-class>com.album.servlet.DeleteAlbumServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DeleteAlbumServlet</servlet-name> <url-pattern>/delete-album</url-pattern> </servlet-mapping> <!-- restore 同理 -->5.4 验证:三步闭环,确保改动无副作用
- 功能验证:登录 → 创建相册 A → 点「删除」→ A 从首页消失 → 访问
http://localhost:8080/album-system/index.jsp?showAll=1(可选,加个参数查全部)→ A 显示且带「恢复」按钮 → 点「恢复」→ A 回到首页; - SQL 验证:
SELECT * FROM album WHERE id = A_id,确认is_deleted在 0 和 1 之间切换; - 兼容性验证:原有「上传照片」「修改相册描述」等功能是否照常工作——因为 DAO 的
updateAlbum()方法没动,它只更新name和description,不碰is_deleted字段。
从那以后我每次给老系统加功能,都强制走一遍「数据库改字段 → DAO 加方法 → JSP 改调用」的三步闭环,绝不跨层操作。改 DAO 时必写单元测试(哪怕只是main方法),改 JSP 时必开浏览器开发者工具看 Network 请求是否 200。这套节奏让我在三个月内交付了 7 个类似的老系统改造需求,没出过一次线上事故。希望帮到你。
本文还有配套的精品资源,点击获取