首页/新闻资讯/正文详情

JSP相册系统实战:上传、缩略图、分页与越权防护

发布时间:2026/11/4 22:03:44 来源:云帆数科 栏目:资讯中心
JSP相册系统实战:上传、缩略图、分页与越权防护
简介一套基于JSP的相册管理系统源码面向Java Web初学者、课程设计或毕业设计参考者围绕用户登录、照片上传、相册分类、评论互动与后台维护等常见需求呈现JSP页面、Servlet与MySQL数据库协作的完整实现思路。压缩包共115个文件、约3.53MB以20个JSP页面、22个Java源文件及22个编译类为核心配有26张jpg展示图、SQL建库脚本、web.xml、properties配置和运行所需jar依赖便于快速了解项目结构与部署材料。已有434人学习。源码可对照PhotoServlet、AdminServlet、PhotoBiz、ConnectionFactory等类梳理请求处理、业务分层、数据库连接及相册数据维护流程系统采用分层设计实体类、业务处理类、控制器与连接工厂分工明确可帮助理解从页面提交到数据持久化的调用链。结合SQL脚本和web.xml配置还能熟悉Tomcat环境下的项目导入、页面跳转、基础安全与排错思路适合作为JSP与MySQL综合练习及答辩准备的参考样例。1. 拆开这套 JSP 相册管理系统一套老技术栈的完整图档流程JSP 相册管理系统这几个字多数人是在课程设计题目里第一次见到的能登录、能建相册、能批量上传、能在页面上翻图后端 Servlet JSP数据库换成 MySQL一套下来把「用户—相册—照片」这条最基础的三层关系跑全了。它不追高并发也不做 CDN 分发权限、上传、缩略图、分页、回收站却一个不少。真正让它值得拿来改的地方不是代码多优雅而是骨架完整、改动阻力小——换套皮肤就是个人作品集站加两个字段就是某个行业的图档系统。适合两类人一是要交课程设计需要一份能跑通、能讲清楚流程的完整工程二是想把 JavaWeb 这套老技术栈重新过一遍、拿真实场景练手的后端。下面按我自己的部署顺序拆从环境对版本写到上传、缩略图、分页最后落在几个必须提前知道的坑上。2. 环境与库表JDK、Tomcat、MySQL 三件套对齐五张核心表立住这套系统最容易崩的地方不在业务代码而在环境。我见过太多次同一个 war 包在 A 同学的机器上跑得好好的换到 B 同学机器上直接 404 或者ClassNotFoundException原因基本都是 JDK、Servlet 容器、MySQL 驱动这三者之间的版本关系没理顺。把这一层对齐后面所有关于相册、上传、分页的讨论才有意义。2.1 三件套的版本对齐与自检先说清楚它们之间的关系。JSP 页面最终是被容器编译成 Servlet 执行的而 Servlet 规范在 Java EE 时代用javax.servlet.*命名空间Jakarta EE 之后统一改成了jakarta.servlet.*。这个改动看着只是包名实际是整套工程能不能启动的分水岭你的代码里写着import javax.servlet.http.HttpServlet;部署到只认jakarta的容器上启动阶段就会报类找不到页面连白屏都算不上直接 500。所以我一般会先把三个事实确认下来再动代码组件常见可用版本关键约束JDK8 或 11与容器主版本匹配避免用 17 跑老代码Servlet 容器Tomcat 9 及以下javax / Tomcat 10 及以上jakarta决定了 import 用哪套包名MySQL5.7 或 8.08.0 必须用com.mysql.cj.jdbc.Driver驱动mysql-connector-j8.x 驱动兼容 5.7 服务端反之不一定命令行自检比在 IDE 里瞎猜快得多# 1. 看 JDK 版本注意 majors 是否和容器匹配 java -version 21 # 2. 看容器版本决定 import 用 javax 还是 jakarta $CATALINA_HOME/bin/version.sh 2/dev/null || echo 未配置 CATALINA_HOME # 3. 看 MySQL 服务端版本与字符集 mysql -uroot -p -e SELECT VERSION(); SHOW VARIABLES LIKE character_set_server;这三条命令的用意分别是java -version确认 JVM 主版本避免高版本 JDK 对新容器或旧框架的模块化限制容器版本命令确认你手里的是 javax 分支还是 jakarta 分支这一条决定第 2.3 节里那段 DB 工具类能不能原样编译MySQL 那条则提前把字符集问题暴露出来——character_set_server如果是latin1后面建表就算写了utf8mb4插入中文照样乱码这个坑在第 4 章还会再出现一次。驱动 jar 别用 IDE 自动补全随便拿一个就塞进去。MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver连接串里要带serverTimezone否则驱动在第一次取连接时就会因为时区解析失败抛异常。我习惯把驱动直接放到WEB-INF/lib而不是依赖容器的lib目录这样换机器部署时行为一致。2.2 五张核心表的字段设计相册系统的表设计有个经典误区把图片二进制直接存进 MySQL 的BLOB字段。小规模演示能跑一旦照片上千张备份、迁移、查询全部会拖垮——数据库体积暴涨导出 SQL 几百兆翻页稍微深一点就开始超时。正确做法是文件落磁盘、库里只存相对路径这一点在 3.2 节会展开。我一般保留五张表就够支撑一个完整可答辩的系统用户、相册、照片、标签关联、操作日志。相册对照片是一对多照片对标签是多对多所以中间要有一张关联表。建表语句如下-- 1. 用户表只留最必要字段密码存哈希 CREATE TABLE t_user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL COMMENT 登录名, password CHAR(64) NOT NULL COMMENT SHA-256 十六进制, nickname VARCHAR(32) DEFAULT NULL, avatar_path VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户; -- 2. 相册表cover_photo_id 冗余一列列表页少一次关联查询 CREATE TABLE t_album ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, title VARCHAR(64) NOT NULL, description VARCHAR(255) DEFAULT NULL, cover_photo_id INT UNSIGNED DEFAULT NULL, is_public TINYINT(1) NOT NULL DEFAULT 1 COMMENT 1 公开 0 私有, sort_num INT NOT NULL DEFAULT 0, is_deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT 软删除, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_sort (user_id, is_deleted, sort_num) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT相册; -- 3. 照片表只存路径不存二进制 CREATE TABLE t_photo ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, album_id INT UNSIGNED NOT NULL, orig_name VARCHAR(128) NOT NULL COMMENT 原始文件名用于下载, rel_path VARCHAR(255) NOT NULL COMMENT 相对上传根目录的路径, thumb_path VARCHAR(255) DEFAULT NULL COMMENT 缩略图相对路径, byte_size INT UNSIGNED NOT NULL DEFAULT 0, width SMALLINT UNSIGNED DEFAULT NULL, height SMALLINT UNSIGNED DEFAULT NULL, taken_at DATETIME DEFAULT NULL, is_deleted TINYINT(1) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_album (album_id, is_deleted, id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT照片; -- 4. 标签表与关联表 CREATE TABLE t_tag ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(32) NOT NULL, UNIQUE KEY uk_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT标签; CREATE TABLE t_photo_tag ( photo_id INT UNSIGNED NOT NULL, tag_id INT UNSIGNED NOT NULL, PRIMARY KEY (photo_id, tag_id), KEY idx_tag (tag_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT照片-标签; -- 5. 操作日志排查“谁把相册删了”这类问题全靠它 CREATE TABLE t_op_log ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, action VARCHAR(32) NOT NULL COMMENT UPLOAD/DELETE/LOGIN 等, target_id INT UNSIGNED DEFAULT NULL, detail VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT操作日志;几处设计取舍值得单独说明。t_album.cover_photo_id是刻意做的冗余相册列表页要显示封面如果不冗余这一列每页 12 个相册就得为每个相册查一次最新照片典型 N1 查询。冗余之后封面上传和删除时同步更新这一个字段即可代价可控。t_photo.rel_path用相对路径而不是绝对路径是为了让整份工程能从一台机器搬到另一台机器而不用改数据——绝对路径写死/Users/xxx/...的项目我见过不止一个换个人部署直接全站图片 404。索引方面idx_album (album_id, is_deleted, id)是给分页查询用的is_deleted放在中间是为了让“查某个相册下未删除的照片、按 id 排序”这一条语句走同一个索引。字符集统一utf8mb4而不是utf8因为utf8在 MySQL 里其实是三字节版本存不了 emoji 和部分生僻字用户给相册起个带表情的名字就会截断报错。2.3 连接池与资源目录约定连接池用 Druid 是这套技术栈里最省事的选择配置文件放src/main/resources/db.properties与代码分离改数据库地址不用重新编译。# db.properties —— 注意 8.0 驱动的时区与 SSL 参数 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/album_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue usernamealbum_user passwordchange_me initialSize5 minIdle5 maxActive30 maxWait3000 validationQuerySELECT 1 testWhileIdletrue// DBUtil.java —— 静态初始化一次数据源全局复用 public final class DBUtil { private static final DataSource DS; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties p new Properties(); p.load(in); DS DruidDataSourceFactory.createDataSource(p); } catch (Exception e) { // 初始化失败必须让进程尽早暴露而不是等到第一次请求 throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DS.getConnection(); } /** 统一关闭顺序 rs - stmt - conn任何一个为 null 都跳过 */ public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) { } } } } private DBUtil() { } }这段代码的关键点在两个地方。maxActive30是个经验值本地演示环境设 30 已经足够设太大反而会在压力测试时把 MySQL 的max_connections打满validationQuery配合testWhileIdle是为了防止连接被 MySQL 的wait_timeout单方面断开后应用还拿着一根死连接去查询——这个现象在页面上表现为“放一晚上第二天第一次点开相册必定报错刷新一下又好了”第 4 章会再提。close方法用可变参数做统一关流是为了避免 DAO 里层层 try-finally 嵌套写漏一个。目录约定同样要提前定死我一般这样分webapp/ ├── uploads/ # 原图按 yyyy/MM 分子目录避免单目录几万文件 ├── thumbs/ # 缩略图路径与原图一一对应 ├── WEB-INF/ │ ├── lib/ │ └── web.xml └── static/ # css/js/占位图按年月分目录不是洁癖。单目录文件数过万之后ls、备份、增量同步都会变慢某些文件系统上目录遍历的开销会明显拖慢上传响应。这个约定一旦定下t_photo.rel_path里存的就是2024/06/xxxx.jpg这种形式读取时前端拼上uploads/前缀即可绝对路径永远不进数据库。3. 上传、缩略图与分页相册系统的三条主干链路环境立住以后真正撑起这个相册系统的只有三条链路文件怎么进来、进来之后怎么变成能快速展示的小图、列表页怎么在几千张照片里翻页。这三条任何一条设计糙了系统都能跑但用起来会很难受。3.1 上传链路Part 接口与 Commons FileUpload 怎么选Servlet 3.0 之后容器内置了 multipart 解析用Part接口就能拿到上传流不必再额外引入 Commons FileUpload。对于这套系统我倾向直接用Part少一个依赖就少一处版本冲突。WebServlet(/api/photo/upload) MultipartConfig( fileSizeThreshold 512 * 1024, // 超过 512KB 写临时文件 maxFileSize 10 * 1024 * 1024, // 单张 10MB maxRequestSize 60 * 1024 * 1024 // 单次请求最多 60MB ) public class PhotoUploadServlet extends HttpServlet { private static final SetString ALLOW_EXT Set.of(jpg, jpeg, png, gif, webp); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); int albumId Integer.parseInt(req.getParameter(albumId)); int userId (int) req.getSession().getAttribute(uid); for (Part part : req.getParts()) { String submitted part.getSubmittedFileName(); if (submitted null) continue; // 跳过普通表单域 if (part.getSize() 0) continue; String ext submitted.substring(submitted.lastIndexOf(.) 1) .toLowerCase(); if (!ALLOW_EXT.contains(ext)) { resp.sendError(400, 不支持的格式: ext); return; } // 1. 生成不冲突的存储名日期目录 UUID String rel FileNames.build(ext); // 如 2024/06/uuid.jpg Path target Paths.get(Storage.ROOT, rel); Files.createDirectories(target.getParent()); Files.copy(part.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING); // 2. 生成缩略图见 3.2 String thumbRel Thumbnails.build(target, rel); // 3. 落库只存相对路径 PhotoDao.insert(albumId, userId, submitted, rel, thumbRel, part.getSize(), target); } resp.setStatus(201); } }逻辑上分四步过滤、命名、落盘、入库。MultipartConfig的三个参数要理解清楚否则会踩“文件小的时候没事、一到大图就 500”的坑fileSizeThreshold决定多大以内放内存、超过就写临时目录设太小会频繁写盘设太大会吃内存maxFileSize是单文件上限maxRequestSize才是整个请求的上限批量传 20 张时如果不调大第二个参数用户会在第 10 张的时候收到一个容器抛出的IllegalStateException而这个异常默认不会给出友好提示。命名用日期目录 UUID 原扩展名而不是直接用原始文件名。原因有两层一是原始文件名可能重复同一张IMG_0001.jpg被两个用户上传就会互相覆盖二是原始文件名里可能带中文、空格、#、?这些字符直接拼进 URL 后浏览器解析的行为不稳定前端图片有时候能显示有时候 404。注意part.getSubmittedFileName()在不同浏览器下返回的可能是完整路径而不是纯文件名某些老版本会带上C:\fakepath\所以取扩展名之前我一般先substring(lastIndexOf(/)1)再做一次lastIndexOf(\\)处理。如果项目已经引入了 Commons FileUpload那思路一致只是入口换成ServletFileUpload.parseRequestFileItem提供getName()和getSize()。两种方式不要混用混用会导致容器已经解析过的流在 FileUpload 里读到空——这个现象特别迷惑日志显示请求有文件落盘却是 0 字节。3.2 缩略图与图片落库别把二进制塞进 MySQL原图动辄几 MB列表页一次展示十几张直接加载原图在普通宽带下要等好几秒。缩略图的存在不是为了好看是为了让列表页的首屏时间从秒级降到百毫秒级。我一般用 Thumbnailator一行就能完成等比缩放和质量压缩public final class Thumbnails { private static final int THUMB_SIZE 320; /** 生成缩略图并返回相对路径 */ public static String build(Path orig, String relPath) throws IOException { String thumbRel relPath.replaceFirst(^, ).replace(/, /t_); Path dest Paths.get(Storage.THUMB_ROOT, thumbRel); Files.createDirectories(dest.getParent()); ThumbnailsShim.of(orig.toFile()) .size(THUMB_SIZE, THUMB_SIZE) // 限定在 320x320 的盒子里 .keepAspectRatio(true) // 保持比例不拉伸 .outputQuality(0.85f) // 0.85 是清晰度和体积的折中 .outputFormat(jpg) // 统一转 jpg减少格式分支 .toFile(dest.toFile()); return thumbRel; } }参数里size加keepAspectRatio的组合意味着“最长边不超过 320另一边按比例缩”而不是强行压成正方形。真要做正方形封面应该先居中裁切再缩否则头像和人物照片会被拉变形这个细节在一堆照片里特别明显。outputQuality(0.85f)是个经验值0.9 以上体积涨得快但肉眼几乎看不出差别0.7 开始大图缩略会有可见的块状噪点。outputFormat(jpg)统一转格式是为了让前端不用处理 png 透明通道在深色背景上的黑边问题。有一点容易被忽略生成的缩略图不应该每次访问都重新算。有些实现图省事图片 URL 指向一个thumb.jsp?srcxxx请求时现算现返回单机测两张图没问题一旦相册里几百张每次刷新列表都要跑几百次 ImageIOCPU 直接拉满。正确做法就是上面这样上传时算一次、落盘、把路径存进t_photo.thumb_path之后只做静态文件读取。至于入库再强调一遍t_photo表里存的是rel_path和thumb_path两个字符串不是LONGBLOB。我见过把图片存进 BLOB 的工程备份出来一个 2GB 的 sql 文件恢复到新环境要二十分钟而且每次SELECT列表只要不小心写了SELECT *就会把几 MB 的图片数据从磁盘拉进内存。3.3 分页与瀑布流limit offset 与 count 的配合相册照片列表的分页本质是两条 SQL一条算总数一条取当前页。看着简单翻车点集中在“总数和列表对不上”以及“翻到后面越来越慢”。-- 1. 总数条件必须和列表查询完全一致 SELECT COUNT(*) FROM t_photo WHERE album_id ? AND is_deleted 0; -- 2. 当前页排序字段要能保证稳定顺序 SELECT id, orig_name, thumb_path, width, height FROM t_photo WHERE album_id ? AND is_deleted 0 ORDER BY id DESC LIMIT ?, ?;第一条和第二条的WHERE必须一字不差这是产生“分页总数”和“实际列表条数”不一致的最常见原因。我见过列表加了标签过滤条件、count 语句忘了加结果第一页显示 20 张、分页器写着 200 页翻到第 3 页就全是空白。修起来只是补一个AND但排查的时候如果不把两条 SQL 并排打印出来很容易怀疑到前端分页组件上去。ORDER BY id DESC而不是ORDER BY created_at DESC是因为created_at精度只到秒批量上传时同一秒内多张照片的排序不确定翻页会出现“第 1 页最后一张和第 2 页第一张重复”的经典现象。用自增主键排序天然唯一且稳定。深翻页的优化要看规模LIMIT 10000, 20这种写法MySQL 会先扫描前 10020 行再丢弃前面部分offset 越大越慢。如果相册动辄上万张可以改成键集分页——把上一页最后一行的id带回来-- 键集分页只扫索引offset 再深也不掉速 SELECT id, orig_name, thumb_path FROM t_photo WHERE album_id ? AND is_deleted 0 AND id ? ORDER BY id DESC LIMIT 20;代价是不能直接跳页只能上一页/下一页。相册场景下这个取舍通常是划算的因为用户翻到第 300 页的概率极低。JSP 里渲染时用 JSTL 遍历即可别在页面上写 Java 脚本片段页面里混业务逻辑以后想换视图层会非常痛苦c:forEach items${page.list} varp a classcell href${ctx}/photo?id${p.id} img src${ctx}/thumbs/${p.thumbPath} alt${p.origName} loadinglazy /a /c:forEachloadinglazy是浏览器原生的懒加载长列表里能再省一截首屏流量代价几乎为零。3.4 视图层选 JSP 还是 JSF相册列表页的两种写法关键词里带着 jsf这里就顺带说清楚。相册系统这类“列表 表单 详情”的结构用 JSP 和用 JSF 都能做但写法和维护成本差别不小。维度JSPJSF编程模型模板 标签逻辑写在 Servlet组件树 托管 Bean列表渲染c:forEach手写循环h:dataTable绑定集合状态管理靠 session / request 手工传递框架托管Bean 作用域声明学习成本低逻辑路径直观高需要理解生命周期六阶段适合场景结构简单、要快速交差表单多、组件复用要求高列表页用 JSF 的写法大概是这样!-- album-list.xhtml数据绑定到托管 Bean分页参数也在 Bean 里 -- h:dataTable value#{albumBean.pageList} vara rows12 h:column h:graphicImage value/thumbs/#{a.thumbPath} / /h:column h:column h:outputText value#{a.title} / /h:column h:column h:commandLink value进入 action#{albumBean.open(a.id)} / /h:column /h:dataTableJSF 的好处是分页、校验、组件复用都由框架接管坏处是请求生命周期比 JSP 复杂得多一个“上传后列表没刷新”的问题可能要在恢复视图、应用请求值、处理校验、更新模型、调用应用、渲染响应这六个阶段里逐段排查。如果这份工程的目标是快速跑通、方便别人接手改皮肤JSP 更合适如果后续要接大量表单校验和组件化的后台管理界面JSF 的表现力更强。两者都要连 MySQL数据访问层可以完全复用切换视图层不影响 DAO。4. 上线前的常见问题排查五条我踩过的坑这套系统在本地跑通和在别人机器上跑通中间隔着的不是代码是环境细节。下面五条是我在不同机器上反复遇到的每条都按“看到什么—为什么—怎么改”来写照着对号入座基本能省掉大半天。4.1 上传后文件名变问号、相册标题变乱码现象浏览器里上传一张名为「春游.jpg」的照片列表里显示成???.jpg新建相册时输入中文标题保存后数据库里是一串问号。原因整条链路上有三处编码设置缺一处就断。请求体编码request.setCharacterEncoding、响应编码response.setContentType、数据库连接串的characterEncoding。很多人只改了 JSP 顶部的pageEncoding那只能保证页面本身不乱码管不到请求参数。解决三处一起补。Servlet 里在读取任何参数之前调用req.setCharacterEncoding(UTF-8)连接串带上useUnicodetruecharacterEncodingutf8mb4建库建表统一utf8mb4。如果用的是过滤器统一处理编码注意过滤器的doFilter里要在chain.doFilter之前设置放在之后是无效的。4.2 启动就报 ClassNotFoundException: javax.servlet.http.HttpServlet现象war 包丢进去容器能起来一访问首页就是 500日志顶部写着找不到javax.servlet.http.HttpServlet。原因容器主版本和代码的 Servlet 命名空间不一致。较新的 Tomcat 分支改用jakarta.servlet.*而工程里是老代码import 写的是javax。解决两条路选一条。要么降容器版本到 javax 分支改动量为零要么把工程里所有javax.servlet替换成jakarta.servlet同时把web.xml的 schema 版本号一起升级。选后者的话记得第三方库也要换成 jakarta 兼容版本只改自己的代码会在引用过滤器、监听器时再次报错。4.3 缩略图在本地能显示部署后全是 404现象照片详情页的原图能打开列表页的缩略图全是裂的F12 里显示请求/thumbs/2024/06/xxx.jpg返回 404。原因上传目录被写到了工程目录之外。Storage.ROOT如果取的是System.getProperty(user.dir)在 IDE 里启动时是工程根目录打包部署后变成容器的bin目录文件就存到了一个前端根本访问不到的路径。解决上传根目录改成显式配置放在webapp下的固定子目录或者配成容器的一个外部目录并通过虚拟路径映射到/uploads。改完之后要检查两件事目录的写权限以及 Nginx 之类的反代有没有把/uploads和/thumbs也代理过去——只代理了/api的话静态图片请求根本到不了应用。4.4 放一晚上第二天第一次点开必报错现象白天用得好好的第二天早上第一个请求必定抛Communications link failure刷新一下又正常了。原因MySQL 的wait_timeout默认八小时空闲超时后服务端主动断开连接但连接池里那根连接的引用还在应用拿到手一用就报错。解决连接池开testWhileIdletrue并配validationQuerySELECT 1让池子在借出前做一次轻量探活同时把minEvictableIdleTimeMillis调到小于wait_timeout。还要确认 DAO 里没有漏关连接——连接池满的表现是请求一直挂着不返回日志里没有任何异常这个和第 4.1 的乱码一样属于“看起来像业务 bug、其实是资源问题”的典型。4.5 分页器写着 200 页翻到第 3 页就空白现象相册列表显示“共 2400 张200 页”实际翻到第三页就是一张空白但数据库里明明有那么多数据。原因count 语句和列表语句的过滤条件不一致最常见的是列表带了album_id和is_deleted0count 只写了album_id另一种是列表做了权限过滤只看自己可见的count 没做。解决把两条 SQL 抽到同一个 DAO 方法里并排写条件用同一个StringBuilder拼接从结构上杜绝两边不一致。顺手加一条自检在调试模式下把两条 SQL 都打出来肉眼对一遍比猜快得多。5. 收紧权限与加水印给相册系统补两处实操基础版跑通之后最容易出问题的是权限。默认实现通常是“登录了就能访问/photo?id123”而 id 是自增的换个数字就能看到别人的照片。补这个洞不需要引入权限框架一个过滤器加一次归属校验就够// AlbumGuard.java —— 在进入 Servlet 之前校验资源归属 public class AlbumGuard implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest r (HttpServletRequest) req; HttpServletResponse w (HttpServletResponse) resp; Integer uid (Integer) r.getSession().getAttribute(uid); if (uid null) { w.sendRedirect(r.getContextPath() /login.jsp); return; } String idStr r.getParameter(id); if (idStr ! null idStr.matches(\\d)) { int owner PhotoDao.findOwnerId(Integer.parseInt(idStr)); // owner 为 0 表示记录不存在同样拒绝避免探测 if (owner ! uid) { w.sendError(403); return; } } chain.doFilter(req, resp); } }校验的关键不是“有没有登录”而是“这条记录属不属于你”。owner ! uid那一行同时挡住了“不存在”和“不属于你”两种情况返回同样的 403不给探测者区分的机会。过滤器注册到web.xml里时url-pattern要覆盖/photo、/album/detail这些带 id 参数的入口而不是只拦/admin/*。第二处是水印。相册被分享出去以后原图很容易被二次传播加一层半透明文字水印能省掉不少麻烦。做法是在 3.2 生成缩略图那一步之后追加一次绘制位置放在右下角、透明度 0.25 左右字号按图宽的 3% 计算这样大图小图看起来比例一致BufferedImage src ImageIO.read(orig.toFile()); Graphics2D g src.createGraphics(); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.25f)); g.setColor(Color.WHITE); g.setFont(new Font(SansSerif, Font.PLAIN, Math.max(12, src.getWidth() / 33))); FontMetrics fm g.getFontMetrics(); int x src.getWidth() - fm.stringWidth(mark) - 16; int y src.getHeight() - fm.getDescent() - 16; g.drawString(mark, x, y); g.dispose();水印只加在分享出去的副本上原图保持干净——这一点我吃过亏。早期版本为了省事直接把水印画在原图上用户想要原片的时候已经没有干净版本了只能重新上传。后来改成“原图只读、派生图带水印”t_photo里另外存一个share_path分享链接指向派生图才算把这个后悔药留住了。验证这套改动是否到位我一般跑一张小清单用两个账号互相访问对方的照片 id确认都是 403上传一张 4000×3000 的大图确认缩略图最长边是 320 且没有变形把t_photo里的rel_path手动改错一个字符确认页面是优雅降级显示占位图而不是抛异常。三件事做完心里就有底了。从那以后我每次改完上传或权限相关的代码都会强制用另一个账号走一遍越权访问而不是只在自己登录状态下点两下——自己看自己永远看不出问题。希望帮到你。本文还有配套的精品资源点击获取

相关推荐

【元宝搜索开发者征文大赛】不荐货,只做尽调:用元宝搜索给二手数码做一张「买前决策卡」
【元宝搜索开发者征文大赛】不荐货,只做尽调:用元宝搜索给二手数码做一张「买前决策卡」

先交代背景。这是一个本地跑的小工具:你输入想买的二手数码、预算、在乎什么不在乎什么,它输出一张「尽调卡」——几个候选机型的价带、通病、验机清单,还有一个带取舍解释的推荐。行情这块的底座全是元宝搜索的实时检索:每条价格… · 2026/11/4 22:02:43

C++实现LeetCode(99.复原二叉搜索树)
C++实现LeetCode(99.复原二叉搜索树)

前言 LeetCode 第 99 题「恢复二叉搜索树」的描述是:一棵二叉搜索树的其中两个节点被错误地交换了,在不改变树结构的前提下恢复这棵树。题目通常要求给出原地算法,进阶要求使用 O(1) 额外空间。 解题的钥匙是中序遍历。二叉搜索树的中序序列严… · 2026/11/4 2:45:15

线上翻译工具与 AI 双屏翻译机的场景化选型指南
线上翻译工具与 AI 双屏翻译机的场景化选型指南

在涉外商务接待中,语言障碍往往是阻碍合作达成的第一道隐形墙。很多企业在初期尝试解决这一问题时,倾向于使用手机上的实时翻译软件,认为只要能把意思传达过去即可。然而,随着接待场景从简单的会议室寒暄深入到样品间选品、车间产… · 2026/11/3 21:05:40

迪杰斯特拉算法详解:原理、优化与实战应用
迪杰斯特拉算法详解:原理、优化与实战应用

1. 迪杰斯特拉算法核心解析 迪杰斯特拉算法是解决单源最短路径问题的经典算法,由荷兰计算机科学家Edsger W. Dijkstra于1956年提出。这个算法在路由选择、地图导航、网络流量优化等领域有广泛应用,是每个程序员必须掌握的基础算法之一。 算法核心思想是… · 2026/11/4 22:02:59

技术文档写作前提:为何‘ax’无法生成有效博文
技术文档写作前提:为何‘ax’无法生成有效博文

我无法根据当前输入生成符合要求的博文。原因如下:项目标题仅为“ax”,无明确语义指向,既非完整技术名词、工具名、协议名,也非可独立解析的缩写(如未说明是 Agent Substrate、AX电机轴向定义、还是其他领域术语&#… · 2026/11/4 22:02:59

Superpowers开源项目:浏览器端多人实时协作游戏开发实战指南
Superpowers开源项目:浏览器端多人实时协作游戏开发实战指南

1. 从“superpowers”这个热词说起:它到底指什么第一次看到“superpowers”这个词挂在热搜上,我脑子里冒出来的第一个念头是:这又是哪个游戏或者影视作品里的设定?毕竟“超能力”这个词本身就带着强烈的娱乐属性。但翻了一圈讨论之… · 2026/11/4 22:02:59

35ms 抗量子握手:ESP32-S3 跑通 ML-KEM-768
35ms 抗量子握手:ESP32-S3 跑通 ML-KEM-768

量子计算机还没造出来,“先存后解”(harvest now, decrypt later)的攻击已经在发生——现在抓走的加密流量,等量子计算成熟后再解。IoT 设备寿命动辄十年,这笔账必须现在算。 最近 Aethyr Research 开源了一套 ESP32-S… · 2026/11/4 22:02:59

电商售后管理接口开发与优化实战
电商售后管理接口开发与优化实战

1. 电商售后管理的痛点与破局之道 做电商的朋友们都知道,售后环节是整个业务流程中最让人头疼的部分。每天面对大量的退货退款申请,处理不及时不仅影响用户体验,还可能面临平台处罚。我曾经运营过一家月销百万的抖店,高峰期每天要… · 2026/11/4 22:01:56

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38

AI辅助技术栈评估:提升研发决策效率60%
AI辅助技术栈评估:提升研发决策效率60%

1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17

从生成Token到直接决策:Jev如何重塑AI Agent工程实践
从生成Token到直接决策:Jev如何重塑AI Agent工程实践

1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17

32G统一内存跑大模型:内存账、TPS实测与端云分流
32G统一内存跑大模型:内存账、TPS实测与端云分流

想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码