简介基于JSP与MySQL的相册管理系统源码包适合Java Web初学者、课程设计或毕业设计参考主要用于实现用户照片的在线上传、分类管理、查看与分享。整套资源共115个文件包括20个JSP页面负责前端展示22个Java源文件及对应class文件处理登录验证、相册分类、评论互动等业务逻辑26张JPG图片可作为演示数据另含一份SQL脚本用于建库建表、4个JAR包提供运行依赖压缩包整体约3.53MB文件结构清楚便于按模块阅读。已有433人学习/下载。通过阅读源码可以完整学习JSPServletJavaBean的协作模式、JDBC连接MySQL的数据库操作流程以及Tomcat部署项目的具体配置方法同时描述中提到的SQL注入防范、图片压缩、分页查询等优化思路也可以作为项目改进和答辩时的扩展方向。对想快速搭建在线相册或理解传统Java Web开发流程的读者来说这是一份兼顾代码与文档说明的实用资料。1. 拆解“基于JSP的相册管理系统”课程设计里的全家桶项目如果你在找的是一份能同时覆盖登录、Session状态、文件上传、MySQL增删改查和前端分页的JSP项目这个相册管理系统基本就是为此存在的。它没有花哨的分布式架构也没有前端框架就是最经典的 JSP Servlet MySQL 三层结构——浏览器发请求Servlet接参数JDBC操作表JSP渲染结果。正因为它技术点足够全才被大量高校课程设计反复选用也正因为全复现时踩的坑才多上传目录权限、编码参数、路径映射这些细节缺一个就跑不通。你拿到手能做什么完整的登录注册、相册分类、照片上传、照片列表与搜索以及一套可以直接替换到自己课设里的页面模板。适合正在做JSP课程设计的人也适合想在一周内把Servlet全流程理顺的开发者。2. 项目骨架与数据库设计从web.xml到四张核心表这一章先把项目的物理结构讲清楚。相册系统不是单表打天下至少要有用户、相册、照片、评论四张表互相支撑配套的表结构直接决定了后面上传、搜索和列表页能不能写得省力。2.1 运行环境匹配JDK、Tomcat与Servlet版本对应关系这个项目用的是传统JSP技术栈所以运行环境很重要。常见的搭配是 JDK 8 Tomcat 8.5 MySQL 5.7这套组合最稳网上能找到的JSP资料大都是基于这个版本组合验证的。如果换到 JDK 11 Tomcat 9也不是不行但要特别注意 Servlet 版本的兼容性——Tomcat 8.5 默认支持 Servlet 3.1Tomcat 9 默认支持 Servlet 4.0项目里如果使用了WebServlet注解或MultipartConfig注解在 Tomcat 9 下需要确认 servlet-api.jar 的版本没有冲突。组件推荐版本说明JDK1.8稳定且兼容老代码JSP标准语法无压力Tomcat8.5默认支持Servlet 3.1自带POST编码处理MySQL5.7InnoDB存储引擎utf8mb4字符集MySQL驱动5.1.49兼容JDK 8不需要额外配置caching_sha2_password如果项目是用 Eclipse 导出的.war包里面通常自带WEB-INF/lib/该目录应该包含jsp-api.jar、servlet-api.jar、mysql-connector-java.jar、jstl.jar和standard.jar。我一般会提醒一句不要在编译时把servlet-api.jar打进最终war包否则和Tomcat自带的servlet实现冲突会出现启动时报ClassCastException的玄学问题。2.2 数据库建模四张表的字段约束与关联关系这个项目的核心表结构并不复杂但字段命名和类型尤其重要。建表语句里最值得关注的是album表和photo表的一对多关系以及user_id作为外键的索引设计。下面是一份可用的建表 SQL-- 用户表 CREATE TABLE sys_user ( id INT(11) NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密码, nickname VARCHAR(50) DEFAULT COMMENT 昵称, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 相册表 CREATE TABLE album ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL COMMENT 所属用户ID, album_name VARCHAR(80) NOT NULL COMMENT 相册名称, description VARCHAR(255) DEFAULT COMMENT 相册描述, cover_photo_id INT(11) DEFAULT NULL COMMENT 封面照片ID可为空, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 照片表 CREATE TABLE photo ( id INT(11) NOT NULL AUTO_INCREMENT, album_id INT(11) NOT NULL COMMENT 所属相册ID, photo_name VARCHAR(120) NOT NULL COMMENT 照片原始文件名, photo_url VARCHAR(255) NOT NULL COMMENT 照片存储的相对路径, file_size INT(11) DEFAULT 0 COMMENT 文件大小单位KB, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_album_id (album_id), KEY idx_upload_time (upload_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表逻辑里有一个常见的取舍album表里的cover_photo_id字段是冗余设计。你既可以在创建相册时指定封面也可以从照片表里ORDER BY upload_time DESC取最新一张作为封面。后者省字段但每次加载相册列表都要多一次子查询这个项目采用冗余字段反而简化了列表页的SQL。sys_user表的password字段长度设为 64是因为通常用 MD5 加密存储32位十六进制就够了64位是为长度更长的摘要预留的。A同学在复现时如果发现注册后密码在数据库里是明文说明登录模块里没有调加密工具类这是该项目和常见安全要求的差异点之一。2.3 字符编码与路径配置web.xml 中的编码过滤器与上传物理目录JSP 项目乱码是重灾区根因往往不是代码写错而是 web.xml 里的过滤器没配对。项目里所有页面统一使用 UTF-8前后端都要覆盖到。常规做法是在 web.xml 里注册 Spring 提供的CharacterEncodingFilter如果是手写项目不带 Spring就自己写一个简单的 Servlet Filterfilter filter-nameencodingFilter/filter-name filter-classcom.example.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingpublic class EncodingFilter implements Filter { private String encoding UTF-8; public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; request.setCharacterEncoding(encoding); resp.setCharacterEncoding(encoding); resp.setContentType(text/html;charset encoding); chain.doFilter(req, resp); } }这段代码的过滤范围是/*意味着静态资源也会走一遍编码设置实际影响很小。注意setCharacterEncoding必须放在getParameter()之前调用才有效所以过滤器顺序很重要。另一个关键配置项是上传文件的物理目录常见做法是在webapps/项目名/data/upload下建立上传文件夹。这个目录在 Eclipse 里通过request.getServletContext().getRealPath(/data/upload)能拿到真实物理路径在打包war部署后也依然有效。但部分生产环境的webapp目录是只读的我一般会配置一个绝对路径地址比如D:/upload然后在JSP页面用request.getContextPath()拼接相对URL来访问这样文件的写入和读取路径分离灵活度更高。3. 核心功能实现从登录校验到照片批量入库的完整链路这一章直接动手写业务。相册系统的核心链路是 用户登录 → 创建相册 → 往相册里传照片 → 前台展示。每一步都有对应的DAO方法、Service逻辑和页面表单我会把代码放到能直接跑的位置。3.1 登录模块MD5密码校验与Session状态保持登录模块的逻辑是接收用户名和密码表单 → 调用DAO层的findByUsernameAndPassword方法 → 成功则把用户对象放进Session → 跳转到首页。密码建议统一做加密处理不在数据库里存明文。项目里通常提供一个MD5Util工具类在注册和登录时对密码做相同处理这样比对才有意义。public class UserDAO { public User login(String username, String password) { // 对密码做MD5加密后再与库中值比对 String encryptedPwd MD5Util.md5(password); String sql SELECT id, username, nickname FROM sys_user WHERE username? AND password?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, encryptedPwd); 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)); return user; } } catch (Exception e) { e.printStackTrace(); } return null; } }这个DAO用了PreparedStatement而不是拼接SQL是防止SQL注入的关键做法。参数占位符是?可以由驱动层预编译避免用户输入的单引号截断SQL结构。MD5Util.md5(password)是同步工具类同一明文多次加密结果一致这就是它能用于登录校验的原因。注意 Session 里只存id、username、nickname不存密码既减小Session体积也避免密码字段在前端页面被EL表达式意外输出。退出登录时的操作是调用session.invalidate()或session.removeAttribute(user)同时清理掉cookie这一步写进logout.jsp的隐藏表单提交里更安全。3.2 相册创建与照片批量入库事务边界与自增ID处理创建相册的流程是先插入主表album拿到数据库生成的自增ID再决定是否插入封面。代码里最常见的坑是在同一循环里反复复用同一个实体对象结果List里存的全是最后一条。正确的写法是每次循环都new一个新对象public boolean createAlbum(Album album) { String sql INSERT INTO album(user_id, album_name, description) VALUES(?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { conn.setAutoCommit(false); // 开启事务防止插入一半失败 ps.setInt(1, album.getUserId()); ps.setString(2, album.getAlbumName()); ps.setString(3, album.getDescription()); ps.executeUpdate(); ResultSet keys ps.getGeneratedKeys(); if (keys.next()) { album.setId(keys.getInt(1)); // 拿到自增ID供后续照片关联 } conn.commit(); return true; } catch (Exception e) { throw new RuntimeException(事务回滚, e); } }批量上传照片时通常的做法是把待上传文件的信息先组装成ListPhoto再用一条INSERT INTO photo(album_id, photo_name, photo_url) VALUES(?,?,?)的循环逐条执行。如果照片数量超过几十张可以用addBatch()批量执行更快for (Photo photo : photoList) { ps.setInt(1, photo.getAlbumId()); ps.setString(2, photo.getPhotoName()); ps.setString(3, photo.getPhotoUrl()); ps.addBatch(); } int[] batchResult ps.executeBatch(); // 返回每条插入影响的行数addBatch的收益在50条以上才明显少于20条直接循环执行更好。特别需要注意的是conn.setAutoCommit(false)之后必须手动commit()否则executeBatch返回后数据可能还锁在事务里。我在实际项目里遇到过执行成功但页面刷新不出来的情况十有八九是忘了提交事务。3.3 JSP页面数据渲染EL表达式加JSTL避免Java代码堆积前端页面如果大量使用% %scriptlet页面会变得极难维护。相册列表页建议全部用 JSTL 的c:forEach循环渲染。调用后台时Servlet 里request.setAttribute(albumList, albumList)然后转发到albumList.jsp% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classtable table-hover c:forEach items${albumList} varalbum tr td a href${pageContext.request.contextPath}/photo/list?albumId${album.id} c:out value${album.albumName}/ /a /td tdfmt:formatDate value${album.createTime} patternyyyy-MM-dd//td /tr /c:forEach /table${pageContext.request.contextPath}会自动拼接当前部署路径避免硬编码成/projectName——一旦 war 包改名部署硬编码路径就会失效。c:out的作用是转义HTML特殊字符比如相册名里如果出现script它会原样显示而不是执行。fmt:formatDate是 JSTL 的格式化标签它要求createTime是java.util.Date类型如果你的实体类里匹配的是java.sql.Timestamp转换时不需要额外处理因为 Timestamp 是 Date 的子类。4. 照片上传与文件存储三个绕不开的坑JSP 项目里文件上传是最容易翻车的一环。这一部分的原因不是逻辑复杂而是文件系统操作和HTTP协议本身的边界问题。如果你准备把相册系统改写为商品管理、证书上传之类的系统这四个问题会原样重现。4.1 上传组件的选择Servlet 3.0原生Part接口 vs 传统SmartUpload老项目里经常见到SmartUpload或commons-fileupload组件但如果你用的是 Tomcat 7 和 Servlet 3.0我建议直接用原生Part接口少引入一个 jar 包也少一层兼容问题。控制器中只需在方法签名上加MultipartConfig注解然后从request.getPart(file)里读取文件。WebServlet(/upload) MultipartConfig(maxFileSize 5 * 1024 * 1024, // 单文件最大5MB maxRequestSize 20 * 1024 * 1024) // 整个请求最大20MB public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String albumId req.getParameter(albumId); Part part req.getPart(photo); String fileName getFileName(part); // 从Content-Disposition中提取原始文件名 String newName System.currentTimeMillis() _ UUID.randomUUID().toString().substring(0,8) fileName.substring(fileName.lastIndexOf(.)); String uploadPath req.getServletContext().getRealPath(/data/upload); part.write(uploadPath File.separator newName); // 把photo信息插入数据库photo_url存相对地址 data/upload/ newName } }maxFileSize参数控制单个文件上限maxRequestSize控制整个表单请求的上限。这个配置里两者差4倍是为了给表单里的其他文本字段预留空间二十张照片同时上传的场景也够用。如果项目里的照片以高清大图为主可以把maxFileSize调到10MB但数据库里的file_size字段要按KB为单位换算后入库否则列表页显示的大小会离谱。getFileName方法需要从part.getHeader(Content-Disposition)里截取filename的值因为不同浏览器的格式略有差异建议统一做一次URLDecoder.decode否则中文文件名在后续写入数据库时会乱码。4.2 文件重命名与乱码时间戳加随机数是最稳妥的命名方式照片文件的物理存储名和原始文件名之间没有必然关系项目里通常不直接用中文原始名落盘。原因有两条中文文件名在URL访问时会产生转义问题不同系统对特殊字符的容忍度不一样同一个目录下如果多人上传同名文件会发生覆盖。我一般用时间戳 随机数 扩展名生成文件名这样同一个用户在同一秒内上传多张照片也不会冲突。String originalName part.getSubmittedFileName(); // 兼容IE浏览器和现代浏览器有的版本从header取更多信息 if (originalName null || originalName.isEmpty()) { originalName new File(part.getHeader(Content-Disposition) .split(;)[2].split()[1].replace(\, )).getName(); } String ext originalName.substring(originalName.lastIndexOf(.)).toLowerCase(); String newName System.currentTimeMillis() _ String.format(%04d, (int)(Math.random()*10000)) ext; part.write(uploadDir File.separator newName);String.format(%04d, ...)让随机数固定为四位文件在文件夹里按创建时间排序时视觉上更整齐。这里两个不同的调用层面有不同编码处理getSubmittedFileName()是 Servlet 3.1 新增方法Tomcat 8.5 已可靠返回 UTF-8 解码后的文件名如果项目运行在 Tomcat 8.0 以下需要手工从 header 解析并做ISO-8859-1到UTF-8的转换这也是乱码的根源之一。4.3 物理路径与虚拟路径绝对路径用于写文件相对路径用于读文件部署JSP项目时经常有人把照片路径设成D:\upload存数据库然后前端img srcD:\upload\a.jpg结果自然裂图。WEB 应用不能直接用客户端访问服务器任意磁盘路径的绝对路径正确做法是数据库存相对路径页面上用request.getContextPath()拼接为绝对URL。还有一个BASE64预览方案读取文件时通过Base64.getEncoder().encodeToString(bytes)把图片字节串成文本直接嵌进img srcdata:image/jpeg;base64,...。这个方案在小图缩略场景下很好用省去一次HTTP请求但单张超过 200KB 的图会导致页面源文件膨胀不适合列表页大面积使用。方案数据库存储值适用场景坑点相对路径data/upload/xxx.jpg列表、详情通用必须用getContextPath()拼接绝对路径D:/upload/xxx.jpg仅服务端读写前端不能直接引用BASE64data:image/jpeg;base64,xxx缩略图大图使页面膨胀一个实用习惯是把上传目录拆分成按日期分层的子文件夹比如data/upload/202505/01/xxx.jpg这样单个文件夹内文件数量可控文件系统遍历快很多也方便按月份清理过期照片。5. 排查与避坑相册系统最常见的十二个报错到这里你会开始和 Tomcat、MySQL 打交道这一节记录的是复现过程中最高频的问题。每一条都是「现象 → 原因 → 解决」的结构建议直接对照排查。5.1 现象点击上传后提示FileNotFoundException或Permission denied原因Tomcat 的 webapps 目录下有权限限制data/upload目录不存在或者没有写权限。Eclipse 部署项目时可能不会自动创建空目录而是等你用资源管理器手动补建部分 Linux 服务器上 webapps 目录由 root 用户创建而 Tomcat 以普通用户运行自然无法写入。解决先确认路径真实存在登录服务器或本地执行mkdir -p webapp路径/data/upload再检查目录所有者是否与 Tomcat 进程一致chown -R tomcat:tomcat data/放行。我一般建议不在webapp目录里建上传目录而是从server.xml配置docBase指向独立目录这样就算项目重新部署也不会清空照片文件。5.2 现象POST请求的中文参数插入数据库后全是???原因数据库连接串没有指定字符集MySQL 客户端连接的默认字符集与表结构的utf8mb4不一致。这个和 JSP Filter 是两类问题Filter 解决的是 HTTP 请求体解码连接串解决的是 JDBC 传输编码两者独立。许多人只加了 Filter忽略了 JDBC URL。解决在jdbc.properties中给 URL 追加?useUnicodetruecharacterEncodingutf8。注意useSSLfalse建议一并加上避免 MySQL 5.7 的 SSL 握手警告刷屏。jdbc.urljdbc:mysql://localhost:3306/album_db?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.passwordyour_password5.3 现象搜索相册时输入中文关键词返回空白英文正常原因虽然表结构是utf8mb4但搜索时WHERE album_name LIKE % keyword %里的 keyword 是从 request 参数直接绑进来的request.getParameter()在没有Filter的情况下默认按ISO-8859-1解码和库里的 UTF-8 编码对不上导致条件永远匹配失败。搜索功能属于 GET 请求Tomcat 8 已默认按 URIEncodingUTF-8 解码但前提是前端页面把表单设置成methodget且页面本身以 UTF-8 输出。解决在搜索入口页面强制meta charsetUTF-8如果还是不行在Servlet入口对参数做一次转码new String(keyword.getBytes(ISO-8859-1), UTF-8)这个写法只对编码错乱场景有效不属于清洁代码。5.4 现象前端列表页图片裂图右键看地址是绝对路径原因代码中使用了photo.photoUrl直接做img src而 photoUrl 字段存的是D:/upload/xxx.jpg。浏览器无法直接访问本地磁盘路径因为客户端没有服务器文件系统的访问权限这就是学理上的“物理路径泄漏资源不可达”。即使你本机打开能显示换一台机器访问必然裂图。解决数据库字段保持相对路径data/upload/xxx.jpg页面渲染时写${pageContext.request.contextPath}/${photo.photoUrl}。如果旧数据已经存了绝对路径写个UPDATE photo SET photo_url REPLACE(photo_url, D:/upload/, data/upload/)一次性清洗。5.5 现象批量上传10张照片数据库里只有最后一张原因这是一个典型误用循环内反复photo.setPhotoUrl()操作同一个photo对象然后list.add(photo)Java 引用传递导致 List 里所有引用指向同一个对象最终该对象的字段值变成最后一次循环的值。解决每一轮循环里都new Photo()再设置属性并加入List。下面这段代码看似重复实际上是防止这类问题的标准写法for (Part part : parts) { Photo p new Photo(); // 每次创建新对象避免引用共享 p.setAlbumId(albumId); p.setPhotoName(getFileName(part)); p.setPhotoUrl(data/upload/ newName); p.setFileSize((int)(part.getSize()/1024)); photoList.add(p); }6. 从能用到好用分页加载与照片搜索的进阶调优项目跑通之后你大概率会面临两个性能问题相册多了之后列表页一次加载几十张照片卡顿、搜索相册名时响应慢。前者的典型解法是分页后者则是靠数据库索引和查询函数。分页的做法是限制每页显示数量SQL 里用LIMIT ?, ?配合PreparedStatement绑定参数。第一页LIMIT 0, 12第二页LIMIT 12, 12。注意这里的页码计算要放在 Java 层完成不要在 SQL 里直接算因为用户输入的pageNo可能为非数字Integer.parseInt前需要一次校验。照片表photo的联合查询可以写成SELECT id, photo_name, photo_url FROM photo WHERE album_id ? ORDER BY upload_time DESC LIMIT ?, ?ORDER BY upload_time DESC要能走索引必须依赖前面建的idx_upload_time。如果没有索引数据量过500条后排序会开始明显变慢。搜索相册名时惯用写法是WHERE album_name LIKE %关键词%这种前缀模糊查询用不上索引数据量上来就是全表扫描。更好的方案是LOCATE(?, album_name) 0成绩不如LIKE稳定却在部分场景下能被优化为更稳的执行计划。如果相册名搜索的高频场景是首部匹配可以在建表时给album_name加前缀索引KEY idx_album_name(album_name(20))但该索引对%关键词%无效更适合关键词%形式。列表页的照片可以加一个前端懒加载策略页面只渲染首屏12张滚到底部时通过 AJAX 请求下一页数据。后端接口可以复用同一个PhotoServlet判断request.getHeader(X-Requested-With)是否为XMLHttpRequest如果是则只返回 JSON 字符串否则返回完整 JSP 页面。这种渐进式增强方案不需要引入前端框架在JSP内置环境里就能实现。整个系统的调优边界大致如此数据量在万级以下时索引和分页足够如果照片超过10万张就要考虑将物理文件迁移到对象存储数据库只保存URL——那是另外一个量级的话题了。这个项目基础版本不背这个包袱。最后说一个我的习惯从那以后我每次部署这类JSP项目第一时间不是看业务代码而是先确认JDBC连接串里的characterEncoding参数、web.xml 里的 Filter 顺序、上传目录是否存在且有写权限这三项都不会出现在需求文档里却是运行期的生死线。这套排查顺序帮我少踩了不少坑也把这个项目从“能启动”推到了“能稳定上传百张照片不丢数据”的程度。希望帮到你。本文还有配套的精品资源点击获取