JSP相册系统实战:三小时搭建用户登录+图片上传+缩略图生成

发布时间:2026/11/6 5:27:16
JSP相册系统实战:三小时搭建用户登录+图片上传+缩略图生成 简介这是一套基于JSP技术实现的轻量级相册管理系统源码面向Java Web初学者与课程设计实践者解决照片上传、分类管理、评论互动及多用户相册展示等典型Web应用需求。资源包共115个文件含20个JSP页面负责前端交互与视图渲染、22个Java源文件与对应class字节码涵盖PhotoServlet、AdminServlet等核心业务逻辑、1个SQL建表脚本用于MySQL初始化、1个CSS样式文件及若干配置类与工具类整体压缩包仅3.53MB结构清晰、依赖精简便于本地快速部署与代码跟踪。已有434人学习下载读者可完整获得从环境搭建JDKTomcatMySQL到功能实现登录鉴权、相册增删改查、图片元数据存储的全流程参考尤其适合理解JSP与Servlet协同机制、JDBC数据库操作及基础Web安全防护实践。1. 一个老派但依然能跑通的JSP相册系统它不炫技但能让你三小时搭起带用户登录、图片上传、分类浏览、缩略图生成的完整Web相册你可能在简历里写过“熟悉Java Web开发”但真要从零搭个能演示的相册系统却卡在Tomcat配不熟、MySQL连不上、JSP表单提交后跳转404——这太常见了。这个基于JSP的相册管理系统就是为这种“想快速验证能力、又不想被Spring Boot自动配置绕晕”的场景而生的。它没用任何现代框架不是JSF标题里的“jsf”是误写实际是JSPServletJDBC所有逻辑都摊开在.jsp和.java文件里用户注册登录走Session控制图片存服务器磁盘元数据存MySQL缩略图用Java2D动态生成分类靠album表关联。适合刚学完Servlet生命周期、想亲手串起“请求→处理→数据库→页面渲染”全链路的开发者也适合需要交付一个轻量级内部相册、不打算长期维护的运维或测试同学。它不追求高并发但每一步都可调试、可打断点、可改一行代码立刻看到效果——这才是新手真正需要的“可控感”。2. 环境准备与项目结构解析看清目录树你就已经避开了50%的部署失败这个项目不是Maven工程没有pom.xml它是一个标准的Java Web传统目录结构直接扔进Tomcat就能跑。理解它的物理布局比背API更重要。2.1 运行环境硬性要求别让JDK版本成为第一个拦路虎项目编译目标是Java 7class file version 51.0这意味着必须用JDK 7 或 JDK 8。JDK 11 会报Unsupported major.minor version 51.0错误因为字节码不兼容。Tomcat 版本建议7.0.x 或 8.0.x。Tomcat 9 默认禁用某些旧版JSP特性如jsp:useBean的scopesession在EL表达式中失效会导致登录态丢失。MySQL 推荐5.7。项目SQL脚本里用了datetime类型默认值CURRENT_TIMESTAMPMySQL 5.6以下不支持该语法。提示如果你本地只有JDK 17别急着卸载——用SDKMAN!或jEnv管理多版本JDK比改系统PATH安全得多。我一般会在项目根目录下放一个jdk.version文本文件写明“require jdk8”避免团队新人踩坑。2.2 项目目录结构每个文件夹都承担明确职责没有黑匣子photo_album/ ├── WebContent/ # 标准WebRootTomcat部署时的根路径 │ ├── index.jsp # 首页未登录显示登录框已登录显示相册列表 │ ├── login.jsp # 登录表单页含用户名/密码输入、错误提示 │ ├── register.jsp # 注册表单页含两次密码校验JS │ ├── upload.jsp # 图片上传页含分类下拉框、描述输入框 │ ├── album.jsp # 单个相册详情页显示该相册所有图片缩略图 │ ├── view.jsp # 大图查看页点击缩略图跳转带上一张/下一张导航 │ ├── css/ # 静态资源 │ │ └── style.css │ ├── images/ # 用户上传的原始图 缩略图按年月日分目录存储 │ └── WEB-INF/ │ ├── web.xml # 核心配置servlet映射、welcome-file-list、session-config │ ├── lib/ # 依赖jar包仅3个mysql-connector-java-5.1.26.jar、jstl-1.2.jar、standard-1.1.2.jar │ └── classes/ # 编译后的.class文件src目录下的.java编译结果 ├── src/ # Java源码非Maven结构需手动编译 │ ├── dao/ # 数据访问层UserDao.java、PhotoDao.java、AlbumDao.java │ ├── entity/ # 实体类User.java、Photo.java、Album.java含getter/setter │ ├── servlet/ # 控制器LoginServlet.java、RegisterServlet.java、UploadServlet.java等 │ └── util/ # 工具类DBUtil.java封装Connection获取、ImageUtil.java缩略图生成核心 └── sql/ # 数据库脚本 └── photo_album.sql # 建库建表初始化管理员账号admin/123456注意WebContent/WEB-INF/classes/是编译输出目录但项目未提供.class文件——你需要用javac手动编译src/下的所有.java。这是老项目的特点没有构建工具一切靠人肉。2.3 web.xml关键配置解读为什么你的Servlet总404这是整个项目的路由中枢必须看懂这三段!-- 1. 定义Servlet -- servlet servlet-nameLoginServlet/servlet-name servlet-classservlet.LoginServlet/servlet-class /servlet !-- 2. 映射URL路径 -- servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern !-- 注意不是/login.do也不是/loginServlet -- /servlet-mapping !-- 3. 设置首页和Session超时 -- welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list session-config session-timeout30/session-timeout !-- 单位分钟 -- /session-config关键点url-pattern决定了浏览器访问地址。比如http://localhost:8080/photo_album/login才会触发LoginServlet。所有Servlet类必须放在src/servlet/下且包名严格为servlet否则servlet-class找不到类。welcome-file-list确保输入http://localhost:8080/photo_album/自动加载index.jsp而不是显示Tomcat默认目录页。2.4 数据库设计精要三张表撑起全部功能字段命名直白无抽象执行sql/photo_album.sql后你会得到三张表表名主要字段关键约束用途说明userid(PK),username,password,email,reg_timeusername唯一索引存储用户账号密码明文存储⚠️仅用于学习生产环境必须加盐哈希albumid(PK),user_id(FK),name,description,create_timeuser_id外键关联user.id每个用户可建多个相册user_id标识归属photoid(PK),album_id(FK),original_name,save_path,thumbnail_path,description,upload_timealbum_id外键关联album.id一张照片属于一个相册save_path存相对路径如/images/2023/10/01/abc.jpg注意photo表中的save_path和thumbnail_path是相对路径代码里拼接request.getServletContext().getRealPath(/)得到绝对磁盘路径。这是传统JSP项目处理文件存储的典型做法好处是路径可控坏处是迁移服务器时需同步复制images/目录。3. 核心模块拆解从登录到上传逐行看懂Servlet如何驱动整个流程现在我们聚焦三个最常出问题、也最能体现JSPServlet协作逻辑的核心模块登录认证、图片上传、缩略图生成。它们不是黑盒而是由几段清晰的代码串联而成。3.1 登录流程Session管理才是状态保持的关键login.jsp提交表单到/login由LoginServlet处理// servlet/LoginServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); UserDao userDao new UserDao(); User user userDao.findByUsername(username); if (user ! null user.getPassword().equals(password)) { // ✅ 认证成功将用户对象存入Session HttpSession session request.getSession(); session.setAttribute(currentUser, user); // 关键后续页面靠这个判断登录态 response.sendRedirect(index.jsp); // 重定向到首页 } else { // ❌ 认证失败把错误信息存入request转发回login.jsp显示 request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } }逻辑说明request.getParameter()获取表单字段简单直接无需JSON解析。UserDao.findByUsername()执行SQLSELECT * FROM user WHERE username ?返回User对象。request.getSession()创建或获取当前会话setAttribute(currentUser, user)是登录态的唯一凭证。response.sendRedirect()是客户端重定向URL会变request.getRequestDispatcher().forward()是服务端转发URL不变且能传递request属性如错误信息。参数说明request.getSession()若当前无Session会新建一个并返回若已有则返回现有Session。session.setAttribute()的keycurrentUser必须和index.jsp中EL表达式${sessionScope.currentUser}完全一致否则首页无法识别登录状态。3.2 图片上传FileItem解析与路径生成的细节决定成败upload.jsp使用form enctypemultipart/form-data后端用Apache Commons FileUpload解析// servlet/UploadServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 创建工厂和解析器 DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); try { // 2. 解析请求获取所有表单项包括普通字段和文件字段 ListFileItem items upload.parseRequest(request); MapString, String params new HashMap(); FileItem fileItem null; for (FileItem item : items) { if (item.isFormField()) { // 普通字段albumId, description params.put(item.getFieldName(), item.getString(UTF-8)); } else { // 文件字段photo fileItem item; } } // 3. 生成保存路径/images/2023/10/01/uuid.jpg String originalName fileItem.getName(); String ext originalName.substring(originalName.lastIndexOf(.)); String saveFileName UUID.randomUUID().toString() ext; String datePath new SimpleDateFormat(yyyy/MM/dd).format(new Date()); String savePath /images/ datePath / saveFileName; // 4. 写入磁盘 String realPath request.getServletContext().getRealPath(/) savePath; File dir new File(realPath.substring(0, realPath.lastIndexOf(/))); if (!dir.exists()) dir.mkdirs(); // 确保日期目录存在 fileItem.write(new File(realPath)); // 5. 保存到数据库Photo实体 路径 Photo photo new Photo(); photo.setAlbumId(Integer.parseInt(params.get(albumId))); photo.setOriginalName(originalName); photo.setSavePath(savePath); photo.setDescription(params.get(description)); PhotoDao photoDao new PhotoDao(); photoDao.save(photo); response.sendRedirect(album.jsp?albumId params.get(albumId)); } catch (Exception e) { e.printStackTrace(); request.setAttribute(errorMsg, 上传失败 e.getMessage()); request.getRequestDispatcher(upload.jsp).forward(request, response); } }关键点fileItem.getName()返回的是客户端文件名如my_photo.jpg不是服务器路径。realPath是服务器磁盘绝对路径getRealPath(/)返回WebContent/所在目录所以/images/...拼接后指向WebContent/images/...。dir.mkdirs()必须调用否则/images/2023/10/01/目录不存在时fileItem.write()会抛FileNotFoundException。3.3 缩略图生成ImageUtil.java里的Java2D魔法缩略图不是前端CSS缩放而是服务端用Java2D实时生成新文件// util/ImageUtil.java public static void generateThumbnail(String sourcePath, String thumbnailPath, int width, int height) throws IOException { // 1. 读取原图 File sourceFile new File(sourcePath); BufferedImage originalImage ImageIO.read(sourceFile); // 2. 创建缩略图BufferedImage BufferedImage thumbnailImage new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g2d thumbnailImage.createGraphics(); // 3. 绘制缩放后的图像保持宽高比居中裁剪 double ratio Math.min((double) width / originalImage.getWidth(), (double) height / originalImage.getHeight()); int scaledWidth (int) (originalImage.getWidth() * ratio); int scaledHeight (int) (originalImage.getHeight() * ratio); int x (width - scaledWidth) / 2; int y (height - scaledHeight) / 2; g2d.setColor(Color.WHITE); g2d.fillRect(0, 0, width, height); // 背景填白 g2d.drawImage(originalImage, x, y, scaledWidth, scaledHeight, null); g2d.dispose(); // 4. 写出JPEG格式缩略图 File thumbnailFile new File(thumbnailPath); ImageIO.write(thumbnailImage, jpg, thumbnailFile); }逻辑说明ImageIO.read()支持JPG/PNG/GIF但写入时ImageIO.write(..., jpg, ...)强制输出为JPEG即使原图是PNG。ratio计算保证缩放后图像完全填满指定宽高多余部分被裁剪x/y偏移实现居中。g2d.setColor(Color.WHITE).fillRect()确保背景为白色避免PNG透明背景在缩略图中显示为黑色。参数说明sourcePath是磁盘绝对路径如/opt/tomcat/webapps/photo_album/images/2023/10/01/abc.jpgthumbnailPath同理width/height通常设为150x150。这个方法在UploadServlet保存原图后立即调用生成的缩略图路径存入photo.thumbnail_path字段。4. 避坑指南那些让我重启三次Tomcat才定位到的血泪经验部署这类老项目80%的问题不是代码bug而是环境错配或路径误解。以下是我在某高校实验室帮学生调试时高频出现的5个真实问题按“现象→原因→解决”给出可立即执行的方案。4.1 现象登录成功后跳转index.jsp但页面顶部仍显示“请登录”Session似乎没生效原因web.xml中session-config缺失或Tomcat的context.xml里Context标签设置了cookiesfalse导致Session ID无法通过Cookie传递。解决检查web.xml是否包含session-config节点哪怕只有一行session-timeout30/session-timeout查看Tomcat安装目录下conf/context.xml确认Context标签内没有cookiesfalse属性在index.jsp顶部加一行调试代码% session.getAttribute(currentUser) null ? null : not null %确认Session是否真的为空。4.2 现象上传图片后images/目录下文件存在但网页显示404缩略图路径在数据库里是/images/2023/10/01/xxx.jpg浏览器访问http://localhost:8080/photo_album/images/2023/10/01/xxx.jpg报错原因Tomcat默认禁止直接访问WEB-INF以外的静态资源但images/在WebContent/下本应可访问——问题出在路径拼接错误。savePath存的是/images/...但request.getServletContext().getRealPath(/)返回的是/opt/tomcat/webapps/photo_album/拼接后变成/opt/tomcat/webapps/photo_album//images/...双斜杠而File类对双斜杠容忍但Tomcat静态资源处理器不认。解决修改UploadServlet.java中路径拼接逻辑// 错误写法导致双斜杠 String savePath /images/ datePath / saveFileName; // 正确写法去掉开头的/ String savePath images/ datePath / saveFileName; // 注意这里没有开头的/这样getRealPath(savePath)返回的路径就是干净的/opt/tomcat/.../images/...。4.3 现象MySQL连接失败报com.mysql.jdbc.Drivernot found原因WEB-INF/lib/下jar包名为mysql-connector-java-5.1.26.jar但项目web.xml或代码里仍用旧版驱动类名com.mysql.jdbc.Driver而新驱动5.1.30推荐用com.mysql.cj.jdbc.Driver。解决确认lib/下jar包版本用jar -tf mysql-connector-java-*.jar | head -5看如果是5.1.26保持DBUtil.java中Class.forName(com.mysql.jdbc.Driver)不变如果误换了新版jar必须同步修改DBUtil.java// 替换这一行 Class.forName(com.mysql.jdbc.Driver); // 为这一行 Class.forName(com.mysql.cj.jdbc.Driver); // 并在jdbc.url末尾加上时区参数?serverTimezoneGMT%2B84.4 现象register.jsp提交后RegisterServlet报NullPointerException堆栈指向userDao.save(user)原因UserDao.java中private Connection conn;未初始化getConnection()方法里写了conn DriverManager.getConnection(...)但save()方法里直接用了conn.prepareStatement(...)而conn为null。解决检查UserDao.java的save()方法确保每次操作前都调用getConnection()public void save(User user) throws SQLException { Connection conn getConnection(); // ✅ 必须先获取连接 String sql INSERT INTO user(username,password,email) VALUES(?,?,?); PreparedStatement ps conn.prepareStatement(sql); // ... 设置参数 ps.executeUpdate(); closeConnection(conn); // ✅ 记得关闭 }4.5 现象缩略图生成后打开是纯白图片或只有左上角一小块内容原因ImageUtil.java中g2d.drawImage()的坐标计算错误x或y为负数导致图像绘制区域超出BufferedImage边界。解决在generateThumbnail()方法末尾加日志System.out.println(x x , y y , scaledWidth scaledWidth , scaledHeight scaledHeight);观察日志如果x或y为负说明ratio计算有误。修正ratio为double ratio Math.max((double) width / originalImage.getWidth(), (double) height / originalImage.getHeight()); // 改为保持宽高比的“等比缩放”不裁剪而是留白然后删除x/y偏移逻辑直接g2d.drawImage(originalImage, 0, 0, width, height, null)。5. 二次开发实战给相册加搜索、权限分级、响应式适配的三步改造法现在你已经能跑通整个系统下一步往往是“老板说要加个搜索框”“客户要求不同用户只能看自己的相册”。这些需求看似简单但老项目改造容易牵一发而动全身。我以三个真实场景为例给出可立即落地的改造方案不碰架构只改最小必要代码。5.1 场景一在首页增加关键词搜索按相册名/图片描述需求用户在index.jsp顶部输入关键词点击搜索列出所有匹配的相册和图片。改造步骤在index.jsp添加搜索表单放在body顶部form actionsearch.jsp methodget input typetext namekeyword placeholder搜索相册名或图片描述... required button typesubmit搜索/button /form创建search.jsp复用album.jsp的展示逻辑但数据源改为搜索结果% page importjava.util.* % % page importentity.* % % page importdao.* % % String keyword request.getParameter(keyword); ListAlbum albums new ArrayList(); ListPhoto photos new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { AlbumDao albumDao new AlbumDao(); PhotoDao photoDao new PhotoDao(); albums albumDao.searchByKeyword(keyword); // 新增方法 photos photoDao.searchByKeyword(keyword); // 新增方法 } request.setAttribute(albums, albums); request.setAttribute(photos, photos); % jsp:include pageheader.jsp/ !-- 复用album.jsp的相册列表HTML -- c:forEach items${albums} varalbum div classalbum-item h3${album.name}/h3 p${album.description}/p a hrefalbum.jsp?albumId${album.id}查看相册/a /div /c:forEach !-- 复用upload.jsp的图片列表HTML -- c:forEach items${photos} varphoto div classphoto-item img src${photo.thumbnailPath} alt${photo.description} p${photo.description}/p /div /c:forEach在AlbumDao.java和PhotoDao.java中新增searchByKeyword()方法// AlbumDao.java public ListAlbum searchByKeyword(String keyword) throws SQLException { String sql SELECT * FROM album WHERE name LIKE ? OR description LIKE ?; PreparedStatement ps getConnection().prepareStatement(sql); ps.setString(1, % keyword %); ps.setString(2, % keyword %); ResultSet rs ps.executeQuery(); ListAlbum list new ArrayList(); while (rs.next()) { Album album new Album(); album.setId(rs.getInt(id)); album.setName(rs.getString(name)); album.setDescription(rs.getString(description)); list.add(album); } closeConnection(); return list; }关键点LIKE ?参数化查询防止SQL注入%keyword%实现模糊匹配。不用全文索引小数据量足够快。5.2 场景二限制用户只能查看/上传自己创建的相册权限隔离需求用户A登录后看不到用户B的相册也不能往B的相册里传图。改造步骤修改AlbumDao.java的findAllByUserId(int userId)方法替换掉原来的findAll()// 原来的 findAll() 方法废弃新增 public ListAlbum findAllByUserId(int userId) throws SQLException { String sql SELECT * FROM album WHERE user_id ?; PreparedStatement ps getConnection().prepareStatement(sql); ps.setInt(1, userId); ResultSet rs ps.executeQuery(); // ... 同上解析 }在index.jsp中从Session获取当前用户ID传给DAO% User currentUser (User) session.getAttribute(currentUser); if (currentUser ! null) { AlbumDao albumDao new AlbumDao(); ListAlbum albums albumDao.findAllByUserId(currentUser.getId()); // ✅ 传入当前用户ID request.setAttribute(albums, albums); } %修改UploadServlet.java校验相册归属// 在保存图片前加入校验 AlbumDao albumDao new AlbumDao(); Album album albumDao.findById(Integer.parseInt(params.get(albumId))); if (album null || album.getUserId() ! currentUser.getId()) { request.setAttribute(errorMsg, 您无权向此相册上传图片); request.getRequestDispatcher(upload.jsp).forward(request, response); return; // 终止执行 }注意Album实体类需新增getUserId()方法对应数据库user_id字段。这是最轻量的权限控制无需RBAC模型。5.3 场景三让相册页面在手机上也能正常浏览响应式适配需求album.jsp在手机浏览器打开时缩略图自动变为单列大图view.jsp支持手势缩放。改造步骤在css/style.css末尾追加媒体查询/* 手机端缩略图单列显示 */ media screen and (max-width: 768px) { .photo-grid { display: block; } .photo-item { width: 100%; margin-bottom: 15px; } .photo-item img { width: 100%; height: auto; } } /* 移动端大图启用viewport缩放 */ media screen and (max-width: 768px) { .view-container { padding: 10px; } .view-image { max-width: 100vw; height: auto; } }在album.jsp和view.jsp的head中添加viewport metameta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableyes为view.jsp添加基础手势支持无需jQueryscript document.addEventListener(DOMContentLoaded, function() { const img document.querySelector(.view-image); let scale 1; let originX 0, originY 0; img.addEventListener(wheel, function(e) { e.preventDefault(); const delta e.deltaY 0 ? 0.9 : 1.1; scale * delta; scale Math.min(Math.max(0.5, scale), 3); // 限制缩放范围 img.style.transform scale(${scale}) translate(${originX}px, ${originY}px); }); }); /script这段JS实现了鼠标滚轮缩放对触屏设备也有效Chrome on Android支持wheel事件。不需要引入任何第三方库老项目改造友好。从那以后我每次接手一个JSP老项目都强制走一遍“三查”查web.xml的servlet-mapping是否匹配URL、查getRealPath()拼接路径是否有双斜杠、查session.getAttribute()的key是否和EL表达式完全一致。这三步能覆盖80%的“明明代码没错却跑不通”问题。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询