尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

基于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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个字段取舍的细节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.javaWebContent 下放 JSP 页面和静态资源。这里有个新手常犯的错把 Java 类放进WebContent/WEB-INF/classes之外的地方或者建了 Maven 项目却忘了 resources 目录。如果是 Eclipse 的 Dynamic Web Projectclasses 默认输出到WEB-INF/classes如果是 IDEA Maven打包成 war 后结构由pom.xml控制。不管哪种源文件放在src下别放WebContent里。分层之后还有个好处换数据库、换前端框架都只是替换某层的事。我见过一个同学做类似的毕业设计到答辩前三天才被要求「不能只用 JSP要加一点框架思想」他因为分了层把 DAO 换成 MyBatis 的 MapperServlet 换成 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 version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-namejsp-album/display-name welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list session-config session-timeout30/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 ?useUnicodetruecharacterEncodingUTF-8 serverTimezoneAsia/ShanghaiuseSSLfalse; 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) {} } } } }参数说明useUnicodetruecharacterEncodingUTF-8解决中文写入乱码serverTimezoneAsia/Shanghai是 MySQL 8 的必填项useSSLfalse避免本地开发时 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 prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % c:forEach items${albumList} varalbum div classalbum-card h3${album.name}/h3 p${album.description}/p p创建于 fmt:formatDate value${album.createTime} patternyyyy-MM-dd//p a href${ctx}/photo/list?albumId${album.id}查看照片/a a href${ctx}/album/delete?id${album.id} onclickreturn confirm(确认删除该相册及其所有照片)删除/a /div /c:forEach删除相册时要不要级联删除照片和磁盘文件我的策略是程序级联先查出相册下所有照片的记录逐个删除磁盘文件再删数据库记录最后删相册本身。为什么不用数据库外键ON DELETE CASCADE因为照片文件在磁盘上数据库删了记录但文件还在才是真正的删除不干净。代码上按「文件 → 记录 → 相册」三步来// AlbumServlet.delete() ListPhoto 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 methodpost enctypemultipart/form-data input typehidden namealbumId value${album.id} / input typefile namefile acceptimage/* required / button typesubmit上传照片/button /formenctypemultipart/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 srcC:/jsp_album_uploads/xxxx.jpg是访问不到的浏览器只能通过 HTTP 访问服务器能公开的路径。所以要把外部目录映射成虚拟路径这一步和 Tomcat 配置有关。在 Tomcat 的conf/server.xml的Host节点下加Context path/uploads docBaseC:/jsp_album_uploads crossContexttrue reloadablefalse /配置之后http://localhost:8080/uploads/20240512/xxxx.jpg就能直接访问到磁盘上的文件。注意docBase指向的是物理目录path是访问前缀重启 Tomcat 生效。如果你不想动server.xml退一步也可以把文件写到项目内并用getRealPath(uploads)定位但是一旦用 IDEA 部署到 Tomcat项目会复制到当前工作目录的副本中代码里写的路径和你在源码目录看到的文件不是一个地方这个坑放到第 5 章细讲。页面展示照片列表核心就是一个forEach拼img标签div classphoto-grid c:forEach items${photoList} varphoto div classphoto-item img src${ctx}/uploads/${photo.photoUrl} loadinglazy 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加上styleleft: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现象表单加了enctypemultipart/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/风景照.jpgURL 会被编码成%E9%A3%8E%E6%99%AF...Tomcat 对 URI 的编码解析不对。解决最省心的方案是第 4 章已经做的——服务端用 UUID 重命名让磁盘文件名永远是纯 ASCII原始名字只存数据库用于展示。UUID 重命名这个动作看起来只是为了防重名实际上它顺手解决了一整类编码问题。如果你非要保留中文文件名需要同时改server.xml里Connector URIEncodingUTF-8、JSP 页面pageEncodingUTF-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 被当作第三方请求阻止要给响应加SameSiteNone; Secure但本地调试不要用 iframe直接整页跳转。5.5 MySQL 中文乱码建库、连接串、JSP 页面三层必须统一现象注册时输入中文昵称数据库里存进去是??。这个问题我帮人排查过很多次几乎每次都是三层里的某一层漏了。第一层是建库CREATE DATABASE jsp_album DEFAULT CHARSET utf8mb4表也要继承这个字符集第 2 章的建表 SQL 里已经写明白第二层是连接串characterEncodingUTF-8必须写在 JDBC URL 里第 3 章的DbUtil也给了第三层是 JSP 页面和 Servlet 的读入JSP 顶部写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-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?path20240512/xxx.jpgServlet 里用 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 对应ControllerJSP 可以保留但要换成官方推荐的模板引擎DAO 层的 JDBC 换成 Spring 的JdbcTemplate或 MyBatisAuthFilter对应 Spring MVC 的HandlerInterceptor文件上传用 Spring 的MultipartFile封装核心逻辑整体平移。我做这套系统的最大教训是永远先设计文件存储和路径映射再写业务代码。上传、预览、删除、迁移所有环节都被「文件放在哪」这一个决定牵动。以后再接任何上传类项目我第一件事就是问清楚文件存哪、访问路径怎么走这比花时间调框架省力得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表