资讯动态

JSP+Servlet+MySQL手写相册管理系统:从部署到避坑的完整实践

发布时间:2026/10/15 8:21:23 来源:尧图企业网站定制
简介基于JSP的相册管理系统是一个使用Java服务器页面技术开发的在线照片管理项目面向Java Web初学者和课程设计、毕业设计人群主要解决用户上传照片、管理相册、在线浏览与评论分享等需求整体采用JSP、Servlet、JavaBean配合MySQL数据库的分层结构。压缩包共一百一十五个文件大小约三点五三兆其中有二十个JSP页面用于展示界面二十二个Java源文件及对应Class文件负责控制器、业务逻辑和数据库操作另含SQL脚本、依赖Jar包、示例图片、配置文件等目录按功能模块划分便于逐个阅读。目前已有四百三十余人学习使用。开发者通过源码可以系统学习Web应用从环境配置到部署的完整流程掌握数据库连接管理、用户登录验证、图片上传等关键实现还能看到防御SQL注入、分页查询、图片压缩等优化手段既适合作为实训参考也方便毕业设计二次开发。1. 相册管理系统的真实用途给私有图片一个不需要图床的存放方案一套能直接在 Tomcat 里跑起来的相册管理系统可能是解决私有图片展示问题的最短路径。某次帮朋友维护社团活动相册需求不外乎按活动建相册、批量传照片、临时给某张图加说明还要保证照片只有内部人能看到。试过网盘和图床权限和展示都不理想最后落到这套基于 JSP 的相册管理系统上。它把用户、相册、照片三类数据放进 MySQL用 Servlet 控制页面跳转前台用 JSP 渲染后台管上传和分类没有任何重框架依赖。适合课程设计参考、毕设演示也适合个人或小团队快速搭一个私有相册空间。2. 技术选型与整体架构Servlet 控制层 JSP 视图层 MySQL 数据层2.1 技术选型为什么这套系统坚持不用 Spring这个项目的业务边界非常清楚登录注册、相册增删改查、照片上传与展示最多再加一个评论。用 Servlet 3.0 JSP JDBC 已经足够支撑根本不需要引入 Spring 的 IoC 容器来做对象装配。很多同学拿到源码第一反应是“怎么不用 Spring Boot”但对这种三张表的小系统手写 Sql 反而比套框架更直观部署时一个 War 包丢进 Tomcat 的 webapps 就能跑。需要区分一个常见误区JSP 和 JSF 不是一回事。JSF 是组件化框架有完整的 UIComponent 生命周期、转换器和验证器适合复杂表单交互的后台但它的配置和学习成本偏高对相册这种页面偏展示型的业务来说属于杀鸡用牛刀。这套系统走的是 JSP JSTL EL 表达式路线页面里循环输出照片列表非常顺手。我一般在“框架选择犹豫期”的判断标准是如果业务里没有复杂的状态流转和事务边界Servlet JSP 就是成本最低的方案。数据库侧选择 MySQL 5.7JDBC 直连没有引入 MyBatis。三张表的主外键关系用肉眼就能看清手写 PreparedStatement 反而让排查问题更容易——SQL 就摆在 Service 和 Dao 里不需要再去找 Mapper 文件的映射关系。前端用 Bootstrap jQuery后台管理页面的表单和按钮直接套组件图片列表用卡片网格展示移动端也能凑合看属于典型的“够用就好”搭配。2.2 工程目录拆解src 与 WebContent 各管什么拿到项目先别急着跑把目录结构认清后面改代码才能找准位置。这是一个标准的 Eclipse Dynamic Web Project 结构没有用 Maven依赖包全部放在 WebContent/WEB-INF/lib 下。album ├── src │ └── com.demo.album │ ├── bean # User / Album / Photo 实体类 │ ├── dao # UserDao / AlbumDao / PhotoDao 接口 实现 │ ├── servlet # 登录、注册、相册、上传、退出等控制器 │ ├── util # JdbcUtils 数据库连接工具 │ └── filter # 登录校验过滤器 ├── WebContent │ ├── css / js / images # 静态资源 │ ├── upload # 照片上传后的存放目录 │ ├── WEB-INF │ │ ├── lib # mysql-connector / jstl 等 jar 包 │ │ └── web.xml # Servlet 3.0 可用注解此处主要配欢迎页和过滤器 │ ├── index.jsp # 登录页 │ ├── register.jsp # 注册页 │ ├── list.jsp # 我的相册列表 │ ├── photo.jsp # 某个相册下的照片墙 │ └── admin # 后台管理页面用户管理 / 相册统计 └── init.sql # 数据库建表脚本注意upload目录的两种用法开发时它存的是 Tomcat 部署目录下的真实文件路径但真实项目中推荐把上传目录配到项目外的固定磁盘路径避免每次 redeploy 把照片清掉。这个坑在后面的避坑章节会详细讲。2.3 数据库设计三张表如何把相册关系串起来相册系统的核心关系就一句话一个用户拥有多个相册一个相册包含多张照片。对应的建表 SQL 在init.sql里已经写好核心部分如下CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_album ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, album_name VARCHAR(100) NOT NULL, description VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_album_user FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_photo ( id INT PRIMARY KEY AUTO_INCREMENT, album_id INT NOT NULL, file_name VARCHAR(200) NOT NULL, -- 实际存储文件名如 20230312_xxx.jpg original_name VARCHAR(200), -- 用户上传时的原始文件名 upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_photo_album FOREIGN KEY (album_id) REFERENCES t_album(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有一个重要的设计决策t_photo只存文件名不存二进制内容。数据库里放 LONGBLOB 字段虽然也能存图但会让表体积膨胀、备份变慢而且数据显示时要多一道“取二进制转图片流”的环节。常见做法是把照片写到服务器文件系统数据库只保存拼接相对路径用的文件名渲染时用contextPath /upload/ fileName拼出完整 URL。字符集统一用 utf8mb4 而不是 utf8原因很简单utf8mb4 是 utf8 的超集能存 emoji 和生僻字。相册描述里很可能出现特殊符号用 utf8 在插入时容易报“Incorrect string value”错误这是血泪经验。3. 核心模块代码拆解从登录到照片上传的完整链路3.1 JdbcUtils连接数据库的工具类只做一件事整个项目所有 Dao 都依赖这个工具类。它负责加载驱动、提供连接核心代码很短package com.demo.album.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcUtils { // 注意 useSSLfalse 和 serverTimezone 是 MySQL 8 的必备参数 private static final String URL jdbc:mysql://localhost:3306/album_db ?useUnicodetruecharacterEncodingutf-8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } }类加载时执行静态代码块注册驱动getConnection()每次调用都返回新连接。参数里几个关键点characterEncodingutf-8保证中文参数从 JDBC 到 MySQL 不乱码serverTimezoneAsia/Shanghai是 MySQL 8 的时区校验要求不加会在启动时报The server time zone value异常。如果是 MySQL 5.7驱动类名用这个没问题换 MySQL 8 的话要把com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver。3.2 用户登录链路Dao 查库 Servlet 控制跳转登录是理解这个项目请求流向的最好入口。用户提交表单后请求先进 LoginServletServlet 调 UserDao 查库查成功就把用户信息塞进 Session然后重定向到相册列表页。// UserDao.java 中的查询方法 public User login(String username, String password) { String sql SELECT id, username, nickname FROM t_user WHERE username? AND password?; try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setNickname(rs.getString(nickname)); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }// LoginServlet.java 核心逻辑 WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); String username request.getParameter(username); String password request.getParameter(password); UserDao userDao new UserDao(); User user userDao.login(username, password); if (user ! null) { request.getSession().setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /album/list); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/index.jsp).forward(request, response); } } }这段代码回答了三个常见问题。第一为什么用PreparedStatement而不是 Statement 拼字符串因为参数化的?能直接规避 SQL 注入用户输入的单引号不会破坏 SQL 结构。第二为什么成功用sendRedirect而失败用forward成功跳转要用重定向防止刷新页面时重复提交表单失败提示则应该 forward把 errorMsg 带回登录页渲染。第三密码这里做的是明文比对课程设计够用二次开发时建议改成 MD5 加盐存储后面会讲到。3.3 照片上传Servlet 3.0 的 Part 接口处理 Multipart 请求照片上传是整个系统里技术含量最高的一块。Servlet 3.0 之后不需要再引 commons-fileupload 包用MultipartConfig注解就能解析 multipart/form-data 请求。WebServlet(/upload) MultipartConfig(maxFileSize 5 * 1024 * 1024, // 单张最大 5MB maxRequestSize 50 * 1024 * 1024) // 一次请求最大 50MB public class PhotoUploadServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); int albumId Integer.parseInt(request.getParameter(albumId)); Part part request.getPart(photo); // 获取原始文件名并抽取扩展名 String originalName part.getSubmittedFileName(); String ext originalName.substring(originalName.lastIndexOf(.)); // 用 UUID 重新生成文件名避免重名覆盖 String fileName UUID.randomUUID().toString().replace(-, ) ext; String dir request.getServletContext().getRealPath(/upload); part.write(dir File.separator fileName); // 把 albumId 和 fileName 写入 t_photo 表 PhotoDao photoDao new PhotoDao(); photoDao.addPhoto(albumId, fileName, originalName); response.sendRedirect(request.getContextPath() /album/photo?albumId albumId); } }几个容易忽略的参数说明maxFileSize只限制单个文件maxRequestSize限制整个请求体。如果只配前者用户一次传 10 张 5MB 的图不会报错但会撑爆内存。part.getSubmittedFileName()在 Tomcat 8 以上才能直接拿到上传文件名Tomcat 7 需要自己解析 header这是个版本坑。用 UUID 重命名的原因是用户上传的文件名经常是中文名、带空格、甚至包含特殊字符直接落盘容易出编码或路径问题UUID 彻底规避冲突。3.4 照片展示链路Servlet 取数 JSP 循环输出照片展示是一个标准的“查询转发渲染”链路。AlbumPhotoServlet 根据 URL 参数albumId查出该相册下所有照片放进 request 域后 forward 到 photo.jsp。c:forEach items${photoList} varphoto div classcol-md-3 div classcard img src${pageContext.request.contextPath}/upload/${photo.fileName} classcard-img-top alt${photo.originalName} div classcard-body p classcard-text${photo.originalName}/p fmt:formatDate value${photo.uploadTime} patternyyyy-MM-dd/ /div /div /div /c:forEach这里src属性的拼法值得注意${pageContext.request.contextPath}会输出当前项目的上下文路径保证不管项目叫什么名字、部署在哪层路径下图片 URL 都能正确解析。如果硬写成/upload/xxx.jpg项目部署名一改就全部 404。数据流顺序是PhotoServlet 调 PhotoDao 拿 ListsetAttribute 存进 requestforward 到 JSPJSP 用 JSTL 遍历输出。整个链路没有用到任何前端框架纯 JSP Bootstrap 卡片布局。4. 本地运行每一步从导入项目到浏览器看到第一张照片4.1 环境版本搭配JDK 1.8 Tomcat 8.5 MySQL 5.7这个项目对运行环境的要求非常宽容但版本搭配有讲究。建议直接按下面的组合走可以减少 80% 的玄学问题。组件推荐版本说明JDK1.8项目基于 JDK 8 编写lambda 和 try-with-resources 都用到了Tomcat8.5Servlet 3.1 规范支持注解开发和 Part 接口MySQL5.7 或 8.05.7 免时区配置8.0 需按 3.1 节的参数改驱动IDEEclipse / IDEAEclipse 直接导入 Dynamic Web ProjectIDEA 选 Web 骨架别一上来就开 Tomcat 10它的 Jakarta EE 命名空间javax 改 jakarta会导致项目里的javax.servlet.*全部编译报错。JDK 也不用追新JDK 17 跑老项目虽然能兼容但某些 IDE 配置反而多出模块化相关的麻烦。4.2 初始化数据库一条命令导入建表脚本MySQL 里先创建一个空数据库然后导入 init.sql。命令行操作很简单mysql -uroot -p -e CREATE DATABASE album_db DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p album_db init.sql导入之后打开src/com.demo.album/util/JdbcUtils.java把USERNAME和PASSWORD改成你本机的数据库账号。这一步经常有人漏导致项目启动后页面能开但登录时报“Access denied for user”。如果连接的是 MySQL 8.0记得同时把驱动类名改成com.mysql.cj.jdbc.Driver这是最容易翻车的一处。4.3 导入项目与部署Eclipse 和 IDEA 两种走法Eclipse 的导入路径是File - Import - General - Existing Projects into Workspace选中项目根目录后 Eclipse 会识别出.project和.classpath自动完成构建。然后在下方的 Servers 视图里New - Server - Tomcat 8.5右键项目Add and Remove把 album 添加进去启动即可。IDEA 走的是另一套File - New - Project from Existing Sources选择项目根目录IDEA 识别出 Web 结构后会生成 Artifact。由于项目是 Eclipse 结构需要在Project Structure - Artifacts里确认输出的 War Exploded 目录包含 WebContent 下的所有内容。然后配置 TomcatRun - Edit Configurations新增 Tomcat ServerDeployment 标签页把 album:war exploded 加上Application context 填/album。两个 IDE 的部署差异点在于Eclipse 默认把项目发布到wtpwebapps/albumIDEA 发布到target/album但浏览器访问路径统一是http://localhost:8080/album/所以前后端代码里写死的路径一定不能带具体磁盘路径。4.4 端到端验证链路跟着走一遍就说明跑通了打开首页http://localhost:8080/album/能看到登录页。第一次使用先点注册入口用任意邮箱格式的用户名注册一个新账号。注册成功会被重定向回登录页用刚注册的账号登录进入“我的相册”空列表页。点击“新建相册”填个名称和描述提交后相册卡片出现在列表里。点进相册进入照片墙页面选一张本地图片上传上传完成后页面刷新卡片网格里出现缩略图。鼠标移到图片上查看大图或删除按钮删除后文件从磁盘移除、数据库记录同步删除。这整个链路走通就可以确认项目本身没有问题可以开始改了。5. 避坑指南中文乱码、图片 404、会话丢失三大高频问题排查5.1 中文乱码设置了 utf-8 依然乱的原因是一处漏改现象注册时输入中文昵称登录后页面显示乱码或者相册描述里的中文变成问号。原因乱码的来源有四个位置只改一处没意义。第一JSP 页面顶部的pageEncoding和contentType要同时写utf-8第二所有 Servlet 的doPost里第一行要写request.setCharacterEncoding(utf-8)而且必须在调用getParameter之前第三数据库连接的 URL 要带characterEncodingutf-8第四MySQL 的表字符集要是utf8mb4。任何一环丢失中文就会在中途变成?。解决全局搜索setCharacterEncoding确认每个 Servlet 都写了检查 JdbcUtils 的 URLSHOW CREATE TABLE t_album确认字符集。我习惯的做法是加一个 CharacterEncodingFilter统一处理所有请求编码一劳永逸。5.2 上传图片后页面 404数据库存的是绝对路径导致部署环境一变就断现象本地跑得好好的照片换台电脑部署或改了一次项目名图片全部裂开。原因上传时如果数据库存的是D:/workspace/album/upload/xxx.jpg这种绝对磁盘路径页面 img 标签的 src 直接引用它浏览器会因为跨域和路径差异加载失败。正确做法是只存文件名视图层动态拼接contextPath /upload/ fileName。解决检查t_photo.file_name字段里存的值。如果已经是绝对路径把存储逻辑改成只保存 UUID 文件名JSP 的 img 标签改用${pageContext.request.contextPath}/upload/${photo.fileName}。5.3 Tomcat 重启后照片消失上传目录被 redeploy 清了现象上传了几十张照片一切正常重启 Tomcat 或重新部署后图片没了但数据库记录还在页面显示裂图。原因代码里用request.getServletContext().getRealPath(/upload)获取上传目录这个目录实际位于 Tomcat 的部署路径下。Eclipse 和 IDEA 的重部署redeploy会先删除旧目录再解压新的 War 包upload 里没来得及备份的照片全被清掉。这是开发环境最常见的翻车点。解决把上传目录配置到项目外的固定位置比如D:/album_upload代码里用配置文件读取这个路径同时给 Tomcat 配置虚拟映射在conf/server.xml的 Host 节点加Context path/album/upload docBaseD:/album_upload /让/upload/xxx.jpg映射到外部目录。这样重部署不影响照片。5.4 新建相册列表不显示分页 SQL 的参数越界现象登录后点“我的相册”能查到几条老的记录但新建的相册怎么刷新都不出现。原因列表接口做了分页LIMIT offset, pageSize的 offset 计算依赖当前页号和每页条数。如果相册数量不超过 pageSize只显示第一页新插入的数据排在后面看不到。还有一种情况是点击“下一页”后页号超出了总页数后端返回空列表但没有重置页号。解决排序条件加上ORDER BY create_time DESC让最新的相册排在最前面后端在查询前判断请求的页号是否大于总页数是则强制重置为最后一页。代码里 PageBean 的 getTotalPage 计算逻辑用(totalCount pageSize - 1) / pageSize避免整除丢页。6. 二次开发思路把相册升级成带标签、评论和权限的小系统先说最值得做的一个改造给照片打标签把单一分类变成多维度组织。数据库加一张t_tag和关联表t_photo_tagt_photo_tag存 photo_id 和 tag_id照片上传成功后把选中的标签关联写入。列表页加一行标签筛选条点击标签时发 Ajax 请求到新增的 TagFilterServlet按标签查询照片。这个改造涉及两张新表和一个新 Servlet但已经有现成的 Dao 模式可以抄工作量不大收益却很直观——相册系统从“按本浏览”进化成“按主题浏览”。第二个改造建议是权限控制。当前t_user只有普通用户后台管理页形同虚设。给表加一个role字段默认USER把管理员账号手动改成ADMIN在 LoginFilter 里除了判断loginUser是否为空再加一层角色判断ADMIN才能访问/admin/*下的接口。注意过滤器要放行登录页、注册页和静态资源否则未登录用户连样式都加载不出来这个细节当年卡了我两个小时。第三个改造是评论功能。t_photo加评论数统计新增t_comment表存 comment_id、photo_id、user_id、content、create_time。评论提交用一个 CommentServlet 接收 Ajax POST成功后把新评论追加到页面对应的列表容器里。数据库外键关系清晰前端用 jQuery 拼 DOM 就行不引入复杂组件库。改完之后的验证方法建议按这个顺序新建一个测试账号上传一张带标签的照片用另一个账号登录看评论是否正常显示归属人然后故意用未登录状态访问后台管理 URL确认被过滤器拦回登录页。验证链路的每一步都有明确预期结果比单纯点一遍功能更容易发现问题。我从那次改完标签系统之后养成一个习惯每次动手改这个项目之前先强制走一遍“注册 → 建相册 → 传图 → 看图”的最小链路确认环境和基线代码没问题再开 IDE。数据库结构的改动也坚持先在 SQL 脚本里改好再同步更新 Dao 层代码而不是直接在库里手工加字段。这样的顺序看起来慢但能避免很多“代码没错、数据不对”的假象问题。这套相册系统本身不复杂但作为 JSP Servlet MySQL 的完整闭环把一个小项目能踩的典型坑都踩了一遍二次开发时把这些经验用上就能少走不少弯路。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑