news 2026/10/29 2:56:35

基于JSP的相册管理系统:Java Web全栈开发与文件上传避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于JSP的相册管理系统:Java Web全栈开发与文件上传避坑指南

简介:基于JSP与MySQL的相册管理系统完整项目源码,面向Java Web初学者、课程设计及毕业设计场景,演示在线照片上传、管理、查看与分享的基本实现。压缩包含115个文件,大小约3.53MB,结构清晰:20个JSP页面负责界面展示,22个Java源文件与22个class编译文件构成Servlet及业务逻辑,另有数据库SQL脚本、依赖库jar包、properties配置及jpg/gif图片素材,可直接导入IDE与Tomcat运行。系统涵盖用户登录、照片上传、相册分类、评论及管理员管理等核心功能,可以通过Servlet控制流转、JSP视图渲染、JavaBean封装数据、JDBC连接MySQL的完整过程,学习动态页面开发与数据库交互的方法。目前已有433人学习下载,附带数据库建表脚本和环境配置提示,便于快速部署、调试与二次扩展,对理解JSP Web项目结构有直接参考价值。

1. JSP 相册管理系统是什么:一个看起来老、但能把 Java Web 全链路走通的项目

拿到「基于 JSP 的相册管理系统」这个题目,第一反应很可能是一句吐槽:JSP 不是快被淘汰了么。但真把这个题目做完,你会发现它几乎是整套 Java Web 技术栈最完整的一类练手项目——注册登录、会话拦截、文件上传、路径映射、数据库关联查询,全都在一张照片的保存和展示里串起来了。它解决的是怎么用最少的框架依赖,把 Servlet 的请求处理、JSP 的页面渲染和 JDBC 的数据读写打通的问题。适合两类人:一类是要交课设或毕设、需要快速跑通并讲清楚原理的学生;另一类是刚入职、想补 Java Web 底层知识的开发者。相册的增删改查不复杂,难的是图片这个非结构化数据怎么落盘、怎么回显、怎么不把服务器搞崩。

2. 技术选型和数据模型:为什么还选 JSP,三张表怎么设计

2.1 都什么年代了,为什么课设/毕设还选 JSP + Servlet + JDBC

「基于 JSP 的 XX 管理系统」在毕业设计选题里常年占着一大块席位,相册系统又是其中功能颗粒度最合适的一种。它的合理性不在技术新不新,而在教学曲线:JSP 是 Java Web 里唯一能让你同时看到「前端页面怎么写、后端逻辑怎么接、数据怎么落库」的东西。你写一个login.jsp,里面能嵌 Java 代码,也能写 HTML,这种直观感是 Spring Boot 给不了的。

选型和后端框架互斥吗?不互斥。常见做法是 JSP 只当视图层,Servlet 当控制器,JDBC 直接操作数据库,这就是最标准的三层结构。等你把这套东西跑通,再去看 Spring MVC 的DispatcherServlet、ModelAndView,会发现全是熟脸——JSP 里request.setAttribute()再在页面上EL取出来,就是Model和View的原型。反过来,如果一上来就用 Spring Boot + MyBatis,很多东西被注解和框架封装得太严实,报错了像个黑匣子,很难排查。

所以这个题目真正的价值,是把 Servlet 生命周期、会话跟踪、文件上传这几块硬骨头啃下来。等答辩的时候老师问「你上传的图片存哪了」「Session 怎么失效的」「数据库连接怎么管理的」,你能答得出每一层,分数自然不一样。如果你后续要换成基于 JSP 的毕业论文过程管理系统这类题目,核心代码基本平移,变的只有业务字段。

2.2 数据库设计:user、album、photo 三张表的字段取舍

相册系统的数据模型比电商简单,但三张表的关联关系值得认真设计。用户和相册是一对多,相册和照片是一对多,照片不直接挂在用户下,而是挂在相册下,这样后续做「按相册浏览照片」的查询就顺了。我一般用jsp_album作为库名,三张表的结构如下:

CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password` CHAR(32) NOT NULL COMMENT 'MD5 密文', `nickname` VARCHAR(50) DEFAULT NULL, `avatar_url` VARCHAR(255) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `album` ( `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL, `name` VARCHAR(100) NOT NULL, `description` VARCHAR(255) DEFAULT NULL, `cover_url` VARCHAR(255) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `photo` ( `id` INT NOT NULL AUTO_INCREMENT, `album_id` INT NOT NULL, `photo_name` VARCHAR(255) NOT NULL COMMENT '原文件名', `photo_url` VARCHAR(255) NOT NULL COMMENT '存储相对路径', `file_size` INT DEFAULT 0 COMMENT '字节数', `upload_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_album_id` (`album_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个字段取舍的细节:password设计成CHAR(32),对应 MD5 摘要长度,这是课设里最常见的做法;但注意 MD5 本身已经不安全了,如果答辩老师问,你要能答出「正式项目会用 BCrypt,这里是为了演示登录流程」。photo_url存的是相对路径而不是完整 URL,比如uploads/20240512/uuid.jpg,好处是项目迁移时不用改数据库。file_size建议存单位字节,做分页和流量统计时有用,不要存成字符串。建了索引不建外键,这是我的习惯:逻辑关联靠程序控制,避免删除相册时外键约束报错打断流程,后面迁移到 MyBatis 也少一层坑。

2.3 项目分层与目录约定:别把 JDBC 写进 JSP

网上很多旧教程喜欢在 JSP 里直接写Class.forName然后select,页面长一大串,跑是能跑,答辩时基本会被老师连环追问到哑口无言。我做这类项目坚持一个约定:JSP 页面里除了 JSTL/EL 和少量scriptlet做遍历,不允许出现 JDBC 代码。整个工程按功能分包:

src/com/example/album/ ├── entity/ User.java Album.java Photo.java ├── dao/ UserDao.java AlbumDao.java PhotoDao.java ├── service/ UserService.java AlbumService.java PhotoService.java ├── servlet/ LoginServlet.java RegisterServlet.java │ AlbumServlet.java UploadPhotoServlet.java ├── filter/ AuthFilter.java └── util/ DbUtil.java

WebContent 下放 JSP 页面和静态资源。这里有个新手常犯的错:把 Java 类放进WebContent/WEB-INF/classes之外的地方,或者建了 Maven 项目却忘了 resources 目录。如果是 Eclipse 的 Dynamic Web Project,classes 默认输出到WEB-INF/classes;如果是 IDEA + Maven,打包成 war 后结构由pom.xml控制。不管哪种,源文件放在src下,别放WebContent里。

分层之后还有个好处:换数据库、换前端框架都只是替换某层的事。我见过一个同学做类似的毕业设计,到答辩前三天才被要求「不能只用 JSP,要加一点框架思想」,他因为分了层,把 DAO 换成 MyBatis 的 Mapper,Servlet 换成 Spring MVC Controller,两天就改完了;没分层的同学改到熬夜。

3. 搭骨架:从空项目到注册登录跑通

3.1 环境和项目形态:JDK 1.8 + Tomcat 8.5,注解代替 XML

先把最基础的环境说清楚。JDK 用 1.8 或 11 都行,Tomcat 我用 8.5/9 系列,这两个版本对 Servlet 3.0/4.0 支持完整,且 JSP 语法兼容最好。创建项目的方式有两种:Eclipse 直接建 Dynamic Web Project,或者 IDEA 建 Maven 项目然后打成 war 包。如果你要交课设,我建议用 Maven 方式,因为老师很可能要求「用 IDEA 开发的」,Maven 结构在后续改造成 Spring Boot 时也更好平移。

在这套组合下,Servlet 3.0 以上支持注解配置,我一般不在web.xml里写一长串<servlet>映射,而是直接在 Servlet 类上写@WebServlet("/login")。web.xml只保留三样东西:欢迎页、Session 超时时间、以及一个字符编码过滤器。这样配置文件短、报错少、答辩也好解释。web.xml骨架如下:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <display-name>jsp-album</display-name> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> <session-config> <session-timeout>30</session-timeout> </session-config> </web-app>

这里的session-timeout单位是分钟,默认 30。如果你希望用户一直保持登录,就调大,或者做成「记住我」的 Cookie 方案,后面会提到。欢迎页指向login.jsp而不是index.jsp,保证未登录用户先进登录页。

3.2 JDBC 工具类与 MySQL 8 连接参数

数据库连接我坚持单写一个DbUtil,所有 DAO 都从它拿连接。MySQL 8.x 和 5.7 驱动类名不一样,这是第一个容易翻车的地方:MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver,而且连接串必须带时区参数,否则启动时报The server time zone value异常。工具类:

package com.example.album.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbUtil { private static final String URL = "jdbc:mysql://localhost:3306/jsp_album" + "?useUnicode=true&characterEncoding=UTF-8" + "&serverTimezone=Asia/Shanghai&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception ignored) {} } } } }

参数说明:useUnicode=true&characterEncoding=UTF-8解决中文写入乱码,serverTimezone=Asia/Shanghai是 MySQL 8 的必填项,useSSL=false避免本地开发时 SSL 握手告警刷屏。static块里加载驱动,保证类加载一次只注册一次。关于连接池:课设阶段我建议直接用DriverManager,不要引 C3P0/Druid。原理都没讲清就直接上连接池,被问「为什么用连接池」「连接池参数怎么调」很容易卡住。如果功能够了,再提一句「生产环境会用连接池」,显得你懂。

3.3 注册登录与 Session 会话:密码怎么存,拦截器怎么写

注册登录是管理系统的门面,也是 Session 机制第一次出场的地方。注册时密码不能用明文,常见做法是 MD5 加盐:

// RegisterServlet.java 关键逻辑 String username = request.getParameter("username"); String rawPassword = request.getParameter("password"); String salt = "album_salt_2024"; // 固定盐,演示用 String hashed = DigestUtils.md5Hex(rawPassword + salt); User user = new User(); user.setUsername(username); user.setPassword(hashed); user.setNickname(username); boolean ok = userService.register(user);

逻辑说明:盐的作用是防止相同密码产生相同摘要,固定盐在演示项目里够用,真实项目应该使用随机盐并单独存储。DigestUtils来自commons-codec,不放心的可以手写MessageDigest转 hex。注册成功后重定向到login.jsp并携带提示参数,不要直接转发——避免刷新页面时表单重复提交。

登录的核心是比对摘要成功后把用户对象放进 Session:

// LoginServlet.java User loginUser = userService.login(username, hashed); if (loginUser != null) { HttpSession session = request.getSession(); session.setAttribute("loginUser", loginUser); response.sendRedirect(request.getContextPath() + "/album/list"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); }

这里刻意用了sendRedirect而不是forward,因为登录成功后跳转是「换了一个页面」,重定向会通知浏览器发起新请求,地址栏变成/album/list,刷新不会重复提交登录。登录后的首页其实就是用户的个人信息展示页面——头像、昵称、相册数量,都可以在这个页面上展示。

有了 Session 里的loginUser,还得防止未登录用户直接访问相册页面。写一个AuthFilter,在请求到达 Servlet 前检查:

package com.example.album.filter; @WebFilter("/*") public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); String ctx = request.getContextPath(); // 放行登录注册页、静态资源 if (uri.endsWith("login.jsp") || uri.endsWith("register.jsp") || uri.equals(ctx + "/login") || uri.equals(ctx + "/register") || uri.contains("/css/") || uri.contains("/js/") || uri.contains("/images/")) { chain.doFilter(request, response); return; } // 未登录一律回登录页 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect(ctx + "/login.jsp"); return; } chain.doFilter(request, response); } }

这段代码的要点在放行列表:静态资源路径不拦截,否则页面的 CSS、JS 加载不出来,样式全丢。另外注意request.getSession(false)的写法——传false表示如果当前没有 Session 就返回null,而不是新建一个。如果直接getSession(),那么拦截器自己就先把 Session 造出来了,这个细节答辨时很加分。

4. 核心业务:相册 CRUD、照片上传与页面回显

4.1 相册的增删改查与级联删除策略

用户登录后进入相册列表页,这一页就是AlbumServlet的list动作:查询当前用户的所有相册,放进request,转发到album_list.jsp。JSP 页面用 JSTL 遍历,注意引入jstl依赖,并把 taglib 声明写在页面顶部。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <c:forEach items="${albumList}" var="album"> <div class="album-card"> <h3>${album.name}</h3> <p>${album.description}</p> <p>创建于 <fmt:formatDate value="${album.createTime}" pattern="yyyy-MM-dd"/></p> <a href="${ctx}/photo/list?albumId=${album.id}">查看照片</a> <a href="${ctx}/album/delete?id=${album.id}" onclick="return confirm('确认删除该相册及其所有照片?')">删除</a> </div> </c:forEach>

删除相册时要不要级联删除照片和磁盘文件?我的策略是程序级联:先查出相册下所有照片的记录,逐个删除磁盘文件,再删数据库记录,最后删相册本身。为什么不用数据库外键ON DELETE CASCADE?因为照片文件在磁盘上,数据库删了记录但文件还在,才是真正的删除不干净。代码上按「文件 → 记录 → 相册」三步来:

// AlbumServlet.delete() List<Photo> photos = photoDao.findByAlbumId(albumId); for (Photo p : photos) { File f = new File(servletContext.getRealPath(p.getPhotoUrl())); if (f.exists()) f.delete(); } photoDao.deleteByAlbumId(albumId); albumDao.delete(albumId);

这里有个隐藏坑:servletContext.getRealPath()拿到的是部署目录下的物理路径,如果你把文件写到了源码目录,就删不到,这和第 5 章裂图的坑是同一个根子。稳妥的做法是文件存储路径和删除路径都通过同一套配置取,别一处写死一处动态获取。

4.2 照片上传:Multipart 解析、UUID 重命名与存储路径

照片上传是整个系统技术含量最高的地方,也是踩坑最多的环节。先看表单:

<form action="${ctx}/photo/upload" method="post" enctype="multipart/form-data"> <input type="hidden" name="albumId" value="${album.id}" /> <input type="file" name="file" accept="image/*" required /> <button type="submit">上传照片</button> </form>

enctype="multipart/form-data"是关键。表单一旦设置了这个类型,后端就不能用request.getParameter("albumId")拿普通字段了,这是第 5 章会展开说的经典坑。上传的 Servlet 要加@MultipartConfig注解:

@WebServlet("/photo/upload") @MultipartConfig( maxFileSize = 5 * 1024 * 1024, // 单个文件最大 5MB maxRequestSize = 20 * 1024 * 1024, // 整个请求最大 20MB fileSizeThreshold = 1024 * 1024 // 超过 1MB 落临时文件 ) public class UploadPhotoServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 中文参数要在读取 Part 之前设置,否则文件名乱码 request.setCharacterEncoding("UTF-8"); int albumId = Integer.parseInt(request.getParameter("albumId")); Part part = request.getPart("file"); String submittedName = part.getSubmittedFileName(); // 校验文件类型,别只看扩展名 String contentType = part.getContentType(); if (!contentType.startsWith("image/")) { request.setAttribute("errorMsg", "只能上传图片文件"); request.getRequestDispatcher("/photo/list?albumId=" + albumId).forward(request, response); return; } // 生成 UUID 文件名,保留扩展名 String ext = submittedName.substring(submittedName.lastIndexOf(".")); String fileName = UUID.randomUUID().toString().replace("-", "") + ext; // 保存路径:项目外部的 uploads 目录,按日期分子目录 String uploadRoot = "C:/jsp_album_uploads"; File dir = new File(uploadRoot, new SimpleDateFormat("yyyyMMdd").format(new Date())); if (!dir.exists()) dir.mkdirs(); File target = new File(dir, fileName); part.write(target.getAbsolutePath()); // 数据库存相对路径:uploads/20240512/xxxx.jpg String url = "uploads/" + dir.getName() + "/" + fileName; Photo photo = new Photo(); photo.setAlbumId(albumId); photo.setPhotoName(submittedName); photo.setPhotoUrl(url); photo.setFileSize((int) part.getSize()); photoDao.insert(photo); response.sendRedirect(request.getContextPath() + "/photo/list?albumId=" + albumId); } }

几个关键决策说明。第一,文件保存到项目外部的C:/jsp_album_uploads,而不是 WebContent 里的 upload 目录——这个选择能躲掉后面一整个系列的部署路径问题,后面讲虚拟目录映射怎么配。第二,文件名用 UUID 重命名,彻底避开中文文件名乱码和同名覆盖问题,原始文件名存到photo_name字段用于展示。第三,part.write()是 Servlet 3.0 封装好的方法,写完自动把临时文件清理掉,不用自己管InputStream的关闭。类型校验用part.getContentType()判断 MIME 而不是只看文件扩展名——扩展名可以伪装,虽然 MIME 也能伪装,但课设层面这已经足够拦截大部分误操作。

4.3 照片回显与列表定位:路径映射、排序和坐标定位

照片传完得能在页面上显示。如果按上面方案把文件存在项目外部,直接在 JSP 里写<img src="C:/jsp_album_uploads/xxxx.jpg">是访问不到的,浏览器只能通过 HTTP 访问服务器能公开的路径。所以要把外部目录映射成虚拟路径,这一步和 Tomcat 配置有关。在 Tomcat 的conf/server.xml的<Host>节点下加:

<Context path="/uploads" docBase="C:/jsp_album_uploads" crossContext="true" reloadable="false" />

配置之后,http://localhost:8080/uploads/20240512/xxxx.jpg就能直接访问到磁盘上的文件。注意docBase指向的是物理目录,path是访问前缀,重启 Tomcat 生效。如果你不想动server.xml,退一步也可以把文件写到项目内并用getRealPath("uploads")定位,但是一旦用 IDEA 部署到 Tomcat,项目会复制到当前工作目录的副本中,代码里写的路径和你在源码目录看到的文件不是一个地方,这个坑放到第 5 章细讲。

页面展示照片列表,核心就是一个forEach拼img标签:

<div class="photo-grid"> <c:forEach items="${photoList}" var="photo"> <div class="photo-item"> <img src="${ctx}/uploads/${photo.photoUrl}" loading="lazy" alt="${photo.photoName}" /> <p>${photo.photoName}</p> <span>${photo.fileSize / 1024} KB</span> </div> </c:forEach> </div>

这里的photoUrl是uploads/20240512/xxxx.jpg,而虚拟目录映射的path="/uploads",所以拼接时${ctx}/uploads/${photo.photoUrl}其实会变成context/uploads/uploads/20240512/...,出现双/uploads。修正方案有两种:映射path="/"(不推荐,会拦截根路径);或者在数据库存不含uploads/前缀的相对路径,页面统一加前缀。我习惯后者:数据库photo_url只存20240512/xxxx.jpg,页面写成${ctx}/uploads/${photo.photoUrl},这样存储结构和展示结构解耦。

如果你在找「JSP 图片如何对坐标定位」的资料,这里一并说清楚。相册页面里照片的「坐标」一般有两种含义。一种是页面布局坐标,用 CSS Grid 或 Flex 控制每张卡片的位置,这在 JSP 里就是给div加上style="left:xx;top:yy",坐标值来自数据库字段;另一种是照片内容里的位置标记,比如给一张合影标注「左起第三个人是谁」,那就需要在前端用绝对定位在img上覆盖坐标点,坐标值存到数据库的x、y字段。JSP 本身不管坐标,它只负责把数据库里的数字输出到style或自定义属性里,真正的定位逻辑在前端 JavaScript。所以做这类功能,核心是设计好photo_tag表和「前端可交互取坐标」的逻辑。

5. 避坑:图片上传与路径问题的 5 个经典坑

5.1 上传成功但页面裂图:部署目录和源码目录不是一回事

现象:上传照片时提示成功,数据库也有记录,但页面<img>一片空白。去项目 WebContent 目录下看,文件根本不存在。原因:IDEA 或 Eclipse 部署到 Tomcat 时,是把项目复制或链接到 Tomcat 的webapps目录(或工作目录下的副本),代码里getRealPath()拿到的是部署路径,而上传时part.write()如果用了相对路径,文件实际落在部署副本里。你在源码目录里找,当然找不到。解决:不要依赖项目内的相对路径,把上传目录固定到项目外的绝对路径,再用虚拟目录映射。第 4 章给的C:/jsp_album_uploads方案就是为这一步准备的。如果你非要把文件放到项目内,请用System.getProperty("catalina.base")拼出 Tomcat 工作目录,并接受「源码里看不到、但运行中能访问」这件事。

5.2 multipart 表单里request.getParameter("albumId")返回 null

现象:表单加了enctype="multipart/form-data"后,Servlet 里request.getParameter("albumId")取到的是null,甚至直接抛NullPointerException。原因:multipart 格式的表单数据不再是普通的 key-value 键值对,普通getParameter只能解析application/x-www-form-urlencoded格式。解决:一个是最简单——把相册 ID 放到 URL 里传,比如表单action="${ctx}/photo/upload?albumId=${album.id}",这样无论表单什么格式都能从 queryString 里取到;另一个是标准做法——用request.getPart("albumId")配合getSubmittedFileName()判断是普通字段还是文件,再取值。第一种方式代码改动小,推荐课设使用,但要能解释清楚原理。

5.3 中文文件名上传后 404:文件名编码和 URI 编码双重问题

现象:上传「风景照.jpg」,上传成功但页面访问 404。原因有两层。第一层,Tomcat 8.5 默认对 URL 编码是 UTF-8,但如果文件名里有非英文字符,服务器保存时用的是平台默认编码,存到磁盘的文件名乱码;第二层,浏览器访问/uploads/风景照.jpg,URL 会被编码成%E9%A3%8E%E6%99%AF...,Tomcat 对 URI 的编码解析不对。解决:最省心的方案是第 4 章已经做的——服务端用 UUID 重命名,让磁盘文件名永远是纯 ASCII,原始名字只存数据库用于展示。UUID 重命名这个动作,看起来只是为了防重名,实际上它顺手解决了一整类编码问题。如果你非要保留中文文件名,需要同时改server.xml里<Connector URIEncoding="UTF-8">、JSP 页面pageEncoding="UTF-8"、还有文件系统目录名称,配置链很长,不推荐。

5.4 登录后立刻跳回登录页:Session/Cookie/Filter 拦截三连坑

现象:用户输入正确账号密码,跳转到相册列表页瞬间又被踢回登录页,但刷新登录页再登录又成功了。原因一般是 Filter 拦截范围写太宽又没放行对应的资源,或者登录 Servlet 用了forward而没有重定向,Session 里还没设上值就被拦截了。排查思路按三步走:第一步,在LoginServlet里打印session.getId()和loginUser,确认登录成功时 Session 是否真的写进去;第二步,检查AuthFilter放行列表是不是把/photo/list、/album/list都拦截了;第三步,确认浏览器是否禁用了 Cookie——JSP 的 Session 依赖容器生成JSESSIONIDCookie,禁了就相当于每次请求都是新 Session。如果是 iframe 嵌套导致 Cookie 被当作第三方请求阻止,要给响应加SameSite=None; Secure,但本地调试不要用 iframe,直接整页跳转。

5.5 MySQL 中文乱码:建库、连接串、JSP 页面三层必须统一

现象:注册时输入中文昵称,数据库里存进去是??。这个问题我帮人排查过很多次,几乎每次都是三层里的某一层漏了。第一层是建库,CREATE DATABASE jsp_album DEFAULT CHARSET utf8mb4,表也要继承这个字符集,第 2 章的建表 SQL 里已经写明白;第二层是连接串,characterEncoding=UTF-8必须写在 JDBC URL 里,第 3 章的DbUtil也给了;第三层是 JSP 页面和 Servlet 的读入,JSP 顶部写<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>,Servlet 里request.setCharacterEncoding("UTF-8")要在读取第一个参数之前调用。漏掉第三层的典型现象是:数据库里中文正常,页面输入框打中文也没问题,但 Servlet 拿到手就是乱码——原因是request对象在解析参数时已经按默认 ISO-8859-1 解码了,设置编码已经来不及。要么在 Filter 里统一setCharacterEncoding,要么在doPost第一行就设置。

6. 进阶:分页、缩略图和向 Spring Boot 迁移

照片传多了以后,列表页一次加载几十张原图,一是慢,二是流量扛不住。分页是必须做的,缩略图则是让页面真正流畅起来的关键。先说分页,常见做法是用 MySQL 的LIMIT offset, size,但要注意 offset 计算不能出错:页码从 1 开始的话,offset = (currentPage - 1) * pageSize。往前翻、往后翻要保证筛选条件不丢,查询参数里带上albumId,否则翻页会跑偏到别的相册去。

缩略图更值得做。如果你不想在磁盘上额外生成一堆小图,可以直接写一个ThumbServlet实时缩放,页面引用<img src="${ctx}/thumb?path=20240512/xxx.jpg">,Servlet 里用 JDK 自带的ImageIO读入、缩放、输出:

BufferedImage src = ImageIO.read(new File(uploadRoot, path)); BufferedImage thumb = new BufferedImage(300, 200, BufferedImage.TYPE_INT_RGB); Graphics2D g = thumb.createGraphics(); g.drawImage(src.getScaledInstance(300, -1, Image.SCALF_SMOOTH), 0, 0, null); g.dispose(); ImageIO.write(thumb, "jpg", response.getOutputStream());

这个方案省磁盘空间,实现简单,缺点是每次请求都重新缩放一次。照片量上百以后,可以在第一次请求时生成xxx_thumb.jpg落盘,后续直接读文件。课程设计阶段做到第一种就行,但答辩时能说出「缩略图落盘」的改进方向,会让老师觉得你有性能意识。

这个项目做完以后,向 Spring Boot 迁移几乎是顺理成章的——Servlet 对应@Controller,JSP 可以保留但要换成官方推荐的模板引擎,DAO 层的 JDBC 换成 Spring 的JdbcTemplate或 MyBatis,AuthFilter对应 Spring MVC 的HandlerInterceptor,文件上传用 Spring 的MultipartFile封装,核心逻辑整体平移。我做这套系统的最大教训是:永远先设计文件存储和路径映射,再写业务代码。上传、预览、删除、迁移,所有环节都被「文件放在哪」这一个决定牵动。以后再接任何上传类项目,我第一件事就是问清楚文件存哪、访问路径怎么走,这比花时间调框架省力得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 2:56:35

Java生产级微信小程序二维码生成实战

简介&#xff1a;本资源是一份面向Java后端开发者的小程序二维码生成实战方案&#xff0c;聚焦微信生态中裂变分享与渠道推广场景&#xff0c;解决账号专属邀请码动态生成、永久有效且高并发可用的核心需求。资源提供5种基于HTTP调用微信官方getUnlimitedQRCode接口的完整实现方…

作者头像 李华
网站建设 2026/10/19 4:42:10

佳能Linux驱动安装指南:CUPS、PPD与后端全流程避坑

简介&#xff1a;面向 Linux 用户的佳能 CUPS 驱动安装包&#xff0c;基于 cndrvcups-common 3.21 通用驱动组件&#xff0c;解决佳能打印机在多种 Linux 发行版下无法直接识别、打印队列配置困难的问题。内置驱动动态库、色彩校准 ICC 文件、打印机描述与安装配置脚本&#xf…

作者头像 李华
网站建设 2026/10/17 3:19:35

Linux下exfat-fuse详解:U盘挂载、选型与避坑指南

简介&#xff1a;exfat-fuse是Linux下基于FUSE框架的ExFAT文件系统驱动。ExFAT是微软为突破FAT32单文件大小限制而设计的文件系统&#xff0c;广泛用于SD卡、U盘等设备&#xff1b;Linux默认不直接支持&#xff0c;此驱动通过用户态FUSE实现读写&#xff0c;让普通用户无需修改…

作者头像 李华
网站建设 2026/10/15 11:17:50

Boot Camp 5.1.5640驱动详解:解决Intel Mac安装Win11的兼容性问题

简介&#xff1a;这是一份面向苹果电脑用户的Windows 11驱动适配包&#xff0c;基于BootCamp 5.1.5640版本&#xff0c;集中解决苹果硬件在Windows 11系统下的驱动兼容问题。包内共1756个文件&#xff0c;体积约883MB&#xff0c;以dll、inf、cat、exe、msi等类型为主&#xff…

作者头像 李华
网站建设 2026/10/15 22:06:44

计算机网络课程设计全攻略:选题、实现与答辩避坑指南

简介&#xff1a;合工大计算机网络课程设计配套资料包&#xff0c;面向高校计算机网络课程设计选题与项目实践&#xff0c;提供一套可运行的Java Web项目及对应报告文档。压缩包共419个文件&#xff0c;总大小61.1MB&#xff0c;内容结构完整&#xff1a;44个Java源文件与45个c…

作者头像 李华
网站建设 2026/10/15 20:10:11

构建可调试可扩展的CLI技能(Skill)开发环境

1. 项目概述&#xff1a;当“skills”不再只是简历上的一个词&#xff0c;而成为可执行、可组合、可调试的运行时能力单元“skills”这个词最近在开发者社区里频繁刷屏&#xff0c;但它的含义已经彻底变了味——它不再是HR筛选简历时扫一眼的软技能列表&#xff0c;也不是培训课…

作者头像 李华