1. 项目概述一个被低估但极具教学价值的全栈实践入口“JspservletSqlServer实现图片上传、详情查看、表格分页”——这行标题乍看像教科书目录实则是一把打开Java Web开发底层逻辑的钥匙。它不炫技不堆砌框架却完整覆盖了Web应用最核心的四大能力闭环用户输入上传、服务处理servlet、数据持久化SqlServer、结果呈现JSP。我带过不少刚从培训班出来的新人一上来就学Spring Boot连HTTP请求怎么被容器解析、session如何在内存中流转都说不清而这个看似“老旧”的组合恰恰逼着你亲手搭起每一块砖Servlet不是黑盒是你要重写doPost方法的类JSP不是模板引擎是你得手写 jsp:useBean 并理解pageContext生命周期的页面SqlServer不是连接字符串里的一个名字是你得手动建表、设主键、加外键约束、写存储过程的真实数据库。它解决的从来不是“要不要用新技术”的问题而是“能不能说清楚每一层发生了什么”的问题。比如图片上传表面是选个文件点提交背后涉及MIME类型校验、二进制流读取缓冲区大小设置、临时文件存储路径权限、数据库BLOB字段与文件系统存储的权衡表格分页看着只是显示第2页数据实则牵扯SQL Server的OFFSET-FETCH语法兼容性、内存中Page对象的封装逻辑、JSP里c:forEach遍历时currentItem与totalPage的边界判断。这些细节在Spring Data JPA一行repository.findAll(Pageable.ofSize(10))里全被抹平了可一旦线上出问题——比如上传大图超时、分页跳转错乱、中文文件名乱码——你翻文档都找不到根因。适合谁三类人必须动手做一遍一是刚学完Java基础、正卡在“学完不知道能做什么”的初学者它提供一条从代码到可运行系统的完整路径二是准备面试的求职者90%的中小厂技术面仍会问“Servlet生命周期”“JSP九大内置对象”“分页SQL怎么写”这不是考背诵是考你有没有真跑通过三是需要快速交付内部管理系统的开发者比如某高校实验室要建一个设备图片档案库没有复杂权限只要稳定、易维护、能跑在Windows Server上——这套组合拳比硬上微服务更务实。我去年帮某公司重构其老旧的资产管理系统第一版就是用这个结构打底上线后运维同事说“比原来那个.NET 2.0版本好查日志多了。”2. 整体架构设计与技术选型逻辑2.1 为什么坚持用原生Servlet而非Spring MVC很多人看到标题第一反应是“都2024年了还写Servlet”。这恰恰是本项目最值得深挖的设计前提。我们不是拒绝框架而是明确场景约束零外部依赖、最小学习成本、最大可控性。Spring MVC的DispatcherServlet背后是HandlerMapping、HandlerAdapter、ViewResolver等十余个组件的协作新手调试时断点打在哪request对象在哪个拦截器里被包装成ModelAndView当上传失败时是CommonsMultipartResolver配置错了还是Tomcat的maxPostSize限制了这些问题的答案藏在Spring源码的第3782行而不是你的业务逻辑里。而原生Servlet逻辑链路清晰到可以用一张纸画完用户点击上传 → 浏览器发POST请求含multipart/form-data头Tomcat接收请求 → 解析为HttpServletRequest对象 → 调用你写的UploadServlet.doPost()你在doPost里调用request.getPart(file) → Tomcat返回Part对象 → 你调用part.write()存到磁盘同时执行SQL INSERT → 把文件名、路径、上传时间存入SqlServer表重定向到list.jsp → JSP里用JSTL遍历查询结果全程没有魔法每个环节你都能加日志、打断点、改参数。比如想验证文件大小限制直接在web.xml里改max-file-size想换存储路径改一行part.write(D:/uploads/filename)就行。这种“所见即所得”的控制感是框架抽象掉的最宝贵的教学资源。提示本项目采用Servlet 3.0规范的MultipartConfig注解而非老式commons-fileupload。因为前者是Java EE标准无需额外jar包Tomcat 7.0原生支持避免了jar包冲突的经典坑——我曾见过团队因commons-fileupload 1.3和1.4混用导致上传时部分文件内容为空。2.2 为什么选SqlServer而非MySQL或H2标题里明确指定SqlServer这不是随意选择而是直面企业级开发的真实约束。国内大量政企、金融、教育类系统仍运行在Windows Server SqlServer组合上尤其当客户要求“必须用国产数据库替代方案”时SqlServer的T-SQL语法、存储过程、全文索引能力是MySQL难以直接对标的技术栈。本项目用到的两个关键SQL特性完美体现其不可替代性分页查询的OFFSET-FETCH语法SELECT * FROM images ORDER BY upload_time DESC OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY这比MySQL的LIMIT 20,10更语义化且SqlServer 2012完全支持。更重要的是它强制要求ORDER BY规避了无序分页导致的数据重复/丢失问题——这是新手用MySQL写分页时踩得最多的坑。文件路径与元数据分离存储我们不在数据库存图片二进制BLOB而是存VARCHAR(255)类型的相对路径如/uploads/20240512_abc.jpg真实文件存文件系统。SqlServer的VARCHAR字段对中文路径支持稳定而早期MySQL的utf8mb4编码配置稍有不慎就会导致路径乱码引发404错误。实测下来SqlServer在Windows环境下的中文路径兼容性比跨平台部署的MySQL更省心。注意SqlServer Express版完全满足本项目需求10GB数据库限制足够存数万张缩略图且安装包仅1.2GB比MySQL Community版的3.5GB更轻量。对于教学演示或内网系统这是更务实的选择。2.3 JSP作为视图层的不可替代性有人质疑“JSP不是被淘汰了吗”——淘汰的是滥用Java代码嵌入HTML的坏习惯不是JSP本身。本项目严格遵循MVC分离原则JSP里只允许JSTL标签和EL表达式所有业务逻辑如分页计算、图片URL拼接都在Servlet或JavaBean里完成。这样做的好处是调试直观页面报错时浏览器直接显示JSP编译错误行号如org.apache.jasper.JasperException: /list.jsp (line 45)而Thymeleaf的错误堆栈常指向TemplateEngine内部学习成本低JSTL的c:forEach、c:if与前端for循环、if语句思维一致新人两天就能上手写列表渲染部署简单JSP文件修改后Tomcat自动热加载无需重启服务比编译型模板如FreeMarker更适合快速迭代。我们用到的核心JSTL标签只有4个c:forEach遍历图片列表、c:choose处理空数据提示、fmt:formatDate格式化上传时间、c:url生成安全URL自动处理contextPath。这比学一套前端框架的路由、状态管理、组件通信门槛低得多。3. 核心功能模块详解与实操要点3.1 图片上传模块从二进制流到数据库记录的完整链路图片上传看似简单实则是本项目最易出错的环节。我整理了从浏览器发起请求到数据库落库的6个关键节点每个节点都有对应代码和避坑说明。第一步HTML表单必须声明enctypeform actionupload methodpost enctypemultipart/form-data input typefile nameimageFile acceptimage/* required input typetext nametitle placeholder图片标题 required button typesubmit上传/button /form关键点enctypemultipart/form-data是硬性要求。若遗漏浏览器会以application/x-www-form-urlencoded方式发送Servlet收到的request.getParameter()将全部为空。acceptimage/*是前端校验但不可靠后端必须二次验证。第二步Servlet接收并校验文件WebServlet(/upload) MultipartConfig( fileSizeThreshold 1024 * 1024, // 1MB缓存阈值 maxFileSize 10 * 1024 * 1024, // 单文件最大10MB maxRequestSize 50 * 1024 * 1024 // 总请求最大50MB ) public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { Part filePart request.getPart(imageFile); String fileName extractFileName(filePart); // 1. 文件名校验防路径遍历攻击 if (fileName.contains(..) || fileName.startsWith(/)) { throw new ServletException(非法文件名); } // 2. 类型校验不能只信contentType要读文件头 String contentType filePart.getContentType(); if (!contentType.startsWith(image/)) { throw new ServletException(仅支持图片文件); } // 3. 大小校验getPart()已触发size检查此处为双重保险 long fileSize filePart.getSize(); if (fileSize 10 * 1024 * 1024) { throw new ServletException(文件超过10MB限制); } // 4. 生成唯一文件名防重名 String uniqueName System.currentTimeMillis() _ fileName; // 5. 写入文件系统 String uploadPath D:/uploads/; File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); filePart.write(uploadPath uniqueName); // 6. 插入数据库 ImageDao.insert(new ImageInfo(uniqueName, request.getParameter(title), new Date())); response.sendRedirect(list); } catch (Exception e) { request.setAttribute(error, e.getMessage()); request.getRequestDispatcher(upload.jsp).forward(request, response); } } }第三步文件名提取的安全写法private String extractFileName(Part part) { String contentDisp part.getHeader(content-disposition); String[] items contentDisp.split(;); for (String s : items) { if (s.trim().startsWith(filename)) { return s.substring(s.indexOf() 2, s.length() - 1); } } return ; }实操心得不要用part.getSubmittedFileName()Servlet 4.0因旧版Tomcat不支持。上述正则解析法兼容所有版本。我曾在线上环境遇到Chrome 110上传时filename字段带双引号导致截取异常此写法能正确处理filenamea.jpg和filenamea.jpg两种格式。第四步数据库表结构设计CREATE TABLE images ( id INT IDENTITY(1,1) PRIMARY KEY, filename VARCHAR(255) NOT NULL, title VARCHAR(100) NOT NULL, upload_time DATETIME2 DEFAULT GETDATE(), file_size BIGINT ); -- 添加索引提升分页查询速度 CREATE INDEX idx_upload_time ON images(upload_time DESC);注意DATETIME2比DATETIME精度更高100纳秒级且范围更大0001-9999年避免2038年问题。file_size字段虽未在上传时写入但预留为后续扩展如按大小筛选图片。3.2 详情查看模块URL路由与资源安全访问详情页不是简单跳转而是建立“ID→文件路径→二进制流”的可信映射。我们采用间接引用模式避免直接暴露文件系统路径。URL设计原则不用/images/abc.jpg暴露物理路径易被爬虫遍历改用/detail?id123通过数据库ID查路径再读取文件DetailServlet核心逻辑WebServlet(/detail) public class DetailServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String idStr request.getParameter(id); if (idStr null || !idStr.matches(\\d)) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } int id Integer.parseInt(idStr); ImageInfo image ImageDao.findById(id); if (image null) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } // 1. 拼接绝对路径注意生产环境应配置为配置项 String imagePath D:/uploads/ image.getFilename(); File imageFile new File(imagePath); // 2. 安全校验确保文件存在且在允许目录下 if (!imageFile.exists() || !imageFile.getAbsolutePath() .startsWith(D:/uploads/)) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } // 3. 设置响应头触发浏览器下载或内联显示 String mimeType getMimeType(image.getFilename()); response.setContentType(mimeType); response.setContentLength((int) imageFile.length()); // 4. 流式输出避免内存溢出 try (FileInputStream fis new FileInputStream(imageFile); OutputStream os response.getOutputStream()) { byte[] buffer new byte[8192]; int len; while ((len fis.read(buffer)) ! -1) { os.write(buffer, 0, len); } } } }JSP详情页展示% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % c:set varimage value${requestScope.image} / html headtitle${image.title}/title/head body h1${image.title}/h1 p上传时间fmt:formatDate value${image.uploadTime} patternyyyy-MM-dd HH:mm:ss//p img srcc:url value/detail?id${image.id} / alt${image.title} stylemax-width:100%;height:auto; a hrefc:url value/list /返回列表/a /body /html关键技巧c:url自动生成带contextPath的URL如/myapp/detail?id123避免硬编码路径。图片src直接指向/detail?id123由Servlet动态输出二进制流浏览器自动识别为图片渲染——这是最简洁的资源访问方案。3.3 表格分页模块从SQL到JSP的端到端实现分页是检验数据层与表现层协同能力的试金石。本项目采用服务器端分页非前端JS分页确保大数据量下性能可控。分页SQL Server写法SqlServer 2012-- 查询总记录数用于计算总页数 SELECT COUNT(*) FROM images WHERE title LIKE ? -- 查询当前页数据 SELECT id, filename, title, upload_time FROM images WHERE title LIKE ? ORDER BY upload_time DESC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY;为什么不用ROW_NUMBER()因为OFFSET-FETCH语法更简洁且SqlServer优化器对其有专门优化。实测10万条数据分页OFFSET 50000 ROWS FETCH NEXT 20 ROWS ONLY耗时稳定在120ms内而ROW_NUMBER()子查询在大数据量时易出现性能抖动。Page对象封装关键public class PageT { private ListT list; // 当前页数据 private int pageNum; // 当前页码从1开始 private int pageSize; // 每页条数 private int total; // 总记录数 private int pages; // 总页数 public Page(ListT list, int pageNum, int pageSize, int total) { this.list list; this.pageNum pageNum; this.pageSize pageSize; this.total total; this.pages (int) Math.ceil((double) total / pageSize); } // getter/setter... public boolean isHasPrevious() { return pageNum 1; } public boolean isHasNext() { return pageNum pages; } public int getPrePage() { return pageNum - 1; } public int getNextPage() { return pageNum 1; } }实操心得pages必须用Math.ceil()计算否则整除会导致最后一页数据不足时页码显示错误。例如total25, pageSize1025/102整数除法但实际需要3页。ListServlet分页逻辑WebServlet(/list) public class ListServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取分页参数默认第1页每页10条 int pageNum getIntParam(request, pageNum, 1); int pageSize getIntParam(request, pageSize, 10); // 2. 查询总记录数 int total ImageDao.count(); // 3. 查询当前页数据 ListImageInfo list ImageDao.findPage(pageNum, pageSize); // 4. 封装Page对象并转发 PageImageInfo page new Page(list, pageNum, pageSize, total); request.setAttribute(page, page); request.getRequestDispatcher(list.jsp).forward(request, response); } private int getIntParam(HttpServletRequest req, String name, int defaultValue) { String val req.getParameter(name); return val ! null val.matches(\\d) ? Integer.parseInt(val) : defaultValue; } }list.jsp分页导航栏JSTL实现% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % c:set varpage value${requestScope.page} / html headtitle图片列表/title/head body h2图片列表共${page.total}条/h2 !-- 数据表格 -- table border1 trth标题/thth上传时间/thth操作/th/tr c:forEach items${page.list} varimg tr td${img.title}/td tdfmt:formatDate value${img.uploadTime} patternyyyy-MM-dd//td tda hrefc:url value/detail?id${img.id} /查看/a/td /tr /c:forEach /table !-- 分页导航 -- div classpagination c:if test${page.hasPrevious} a hrefc:url value/list?pageNum${page.prePage}pageSize${page.pageSize} /上一页/a /c:if c:forEach begin1 end${page.pages} vari c:choose c:when test${i page.pageNum} span stylefont-weight:bold;${i}/span /c:when c:otherwise a hrefc:url value/list?pageNum${i}pageSize${page.pageSize} /${i}/a /c:otherwise /c:choose /c:forEach c:if test${page.hasNext} a hrefc:url value/list?pageNum${page.nextPage}pageSize${page.pageSize} /下一页/a /c:if /div /body /html注意分页链接中的pageSize必须显式传递否则切换页码时会丢失每页条数设置。JSTL的c:forEach begin1 end${page.pages}生成数字页码比手写10个a标签更灵活。4. 全流程实操步骤与环境配置4.1 开发环境搭建从零开始的5步落地本项目对环境要求极低一台普通笔记本即可完成。以下是我在Windows 11环境下实测的完整步骤Mac/Linux仅路径略有差异步骤1安装JDK 8u202最低要求下载地址Oracle官网历史版本需注册账号或Adoptium Temurin 8验证java -version输出1.8.0_202为什么不用JDK 11因为Tomcat 8.5虽支持JDK 11但部分老教材的web.xml配置如servlet-class在JDK 11下会因模块化报错。JDK 8是兼容性最优解。步骤2安装Tomcat 8.5.99最新8.x版下载zip包解压到C:\tomcat\配置环境变量CATALINA_HOMEC:\tomcat启动C:\tomcat\bin\startup.bat→ 访问http://localhost:8080看到猫脸即成功实操心得不要用Windows Service Installer版本它会把Tomcat注册为系统服务导致端口占用难排查。zip版解压即用关闭直接关cmd窗口。步骤3安装SqlServer Express 2019下载地址Microsoft官网免费版安装时勾选“SQL Server和Windows身份验证模式”设置sa密码为Sa123456方便开发安装后启动SQL Server Management Studio (SSMS)连接localhost\SQLEXPRESS用sa登录注意安装过程中若提示“缺少.NET Framework 3.5”需在Windows功能中启用——这是SqlServer的硬性依赖。步骤4创建数据库与表在SSMS中执行-- 创建数据库 CREATE DATABASE image_db COLLATE Chinese_PRC_CI_AS; USE image_db; -- 创建表同3.1节 CREATE TABLE images (...);关键点COLLATE Chinese_PRC_CI_AS确保中文排序和比较正常避免WHERE title LIKE %测试%查不到数据。步骤5配置数据库连接池推荐在C:\tomcat\conf\context.xml中添加Resource namejdbc/imageDB authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.microsoft.sqlserver.jdbc.SQLServerDriver urljdbc:sqlserver://localhost\\SQLEXPRESS;databaseNameimage_db;encryptfalse; usernamesa passwordSa123456 maxActive20 minIdle5 maxWait10000/为什么用JNDI连接池避免每次请求都新建Connection实测并发100用户时连接池响应时间稳定在20ms而直连方式飙升至200ms。encryptfalse是SqlServer Express的必需配置否则连接失败。4.2 项目结构与关键文件清单本项目采用标准Java Web结构无任何构建工具不使用Maven纯手工组织确保新人一眼看懂MyImageApp/ ├── WEB-INF/ │ ├── web.xml # 部署描述符Servlet映射、欢迎页 │ ├── lib/ │ │ └── mssql-jdbc-12.4.2.jre8.jar # SqlServer JDBC驱动必须 │ └── classes/ │ ├── com/example/servlet/ │ │ ├── UploadServlet.class │ │ ├── ListServlet.class │ │ └── DetailServlet.class │ ├── com/example/dao/ │ │ └── ImageDao.class # 数据库操作类 │ └── com/example/model/ │ └── ImageInfo.class # 实体类 ├── uploads/ # 图片存储目录需手动创建 ├── upload.jsp # 上传页面 ├── list.jsp # 列表页面 ├── detail.jsp # 详情页面 └── index.jsp # 欢迎页重定向到/upload.jspweb.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 welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list servlet servlet-nameUploadServlet/servlet-name servlet-classcom.example.servlet.UploadServlet/servlet-class /servlet servlet-mapping servlet-nameUploadServlet/servlet-name url-pattern/upload/url-pattern /servlet-mapping !-- 其他Servlet映射... -- /web-app注意url-pattern必须与WebServlet注解一致否则404。新手常犯错误是注解写了WebServlet(/upload)web.xml里却配成url-pattern/Upload/url-pattern大小写敏感。4.3 JDBC驱动集成与连接测试SqlServer JDBC驱动是本项目最关键的第三方依赖。2024年必须用Microsoft官方mssql-jdbc淘汰老式sqljdbc4.jar。下载与放置访问https://github.com/microsoft/mssql-jdbc/releases下载mssql-jdbc-12.4.2.jre8.jar适配JDK 8复制到WEB-INF/lib/目录下连接测试代码放在ImageDao.java中public static void testConnection() { try { Context ctx new InitialContext(); DataSource ds (DataSource) ctx.lookup(java:comp/env/jdbc/imageDB); Connection conn ds.getConnection(); System.out.println(数据库连接成功当前时间 conn.prepareStatement(SELECT GETDATE()).executeQuery().next()); conn.close(); } catch (Exception e) { e.printStackTrace(); // 打印详细错误定位驱动或配置问题 } }常见问题ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver→ 驱动jar未放入lib目录或jar包损坏Cannot create PoolableConnectionFactory→ URL中的localhost\\SQLEXPRESS写成localhost\SQLEXPRESS单反斜杠被转义Login failed for user sa→ SqlServer未启用混合模式或sa账户被禁用SSMS中右键sa→属性→状态→登录→启用5. 常见问题与排查技巧实录5.1 上传失败的7种典型场景及解决方案在真实教学和项目实施中上传功能出错率高达65%。以下是我在某高校实验室部署时记录的7个高频问题附带日志特征和解决命令问题现象日志关键信息根本原因解决方案上传后页面空白SEVERE: Servlet.service() for servlet [UploadServlet] threw exception java.lang.NullPointerExceptionrequest.getPart(imageFile)返回null检查HTML表单nameimageFile是否与Servlet中getPart()参数名一致确认enctypemultipart/form-data已设置上传大图超时SEVERE: Servlet.service() for servlet [UploadServlet] threw exception org.apache.tomcat.util.http.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. Socket read timeoutTomcat默认maxPostSize2MB超出即中断修改C:\tomcat\conf\web.xml在connector标签内添加maxPostSize5242880050MB中文文件名乱码数据库中filename字段显示?????.jpgSqlServer数据库未设中文排序规则在SSMS中执行ALTER DATABASE image_db COLLATE Chinese_PRC_CI_AS文件写入失败java.io.FileNotFoundException: D:\uploads\test.jpg (拒绝访问)Windows系统对D:\uploads目录无写入权限右键uploads文件夹→属性→安全→编辑→添加Users组并勾选“写入”上传后图片无法查看浏览器显示“无法加载图片”JSP中img src路径错误或DetailServlet未正确设置Content-Type在DetailServlet中添加response.setContentType(image/jpeg);并确认img src指向/detail?id123而非/uploads/xxx.jpg多文件上传只存一个数据库只有一条记录HTML中多个input typefile但Servlet只调用一次getPart()改用request.getParts()遍历所有Part或为每个文件域设置唯一name如file1,file2上传成功但列表不显示list.jsp表格为空ImageDao.findPage()查询SQL未加ORDER BY导致OFFSET-FETCH语法报错检查SQL语句确保SELECT ... FROM images ORDER BY upload_time DESC OFFSET ? ROWS...中ORDER BY存在实操心得遇到上传问题第一反应不是改代码而是看Tomcat日志。日志文件在C:\tomcat\logs\catalina.out用Notepad搜索“UploadServlet”或“Exception”90%的问题能直接定位。我曾帮某公司解决一个持续两周的上传失败问题最终发现是杀毒软件360拦截了Tomcat对uploads目录的写入关闭实时防护即恢复。5.2 分页跳转错乱的3个隐蔽陷阱分页功能看似稳定但以下3个陷阱常导致线上事故陷阱1URL参数未编码导致特殊字符截断现象当图片标题含符号如“会议培训”时分页链接/list?pageNum2pageSize10被浏览器解析为pageNum2pageSize丢失解决JSTL中用c:url自动编码禁止手写a href/list?...!-- 正确 -- a hrefc:url value/listc:param namepageNum value${page.nextPage}//c:url下一页/a !-- 错误 -- a href/list?pageNum${page.nextPage}pageSize${page.pageSize}下一页/a陷阱2Page对象pageNum从0开始导致首屏空白现象访问/list时显示空表格但/list?pageNum1正常原因Page构造函数中pageNum默认为0OFFSET 0 ROWS查到第0页不存在解决在ListServlet中强制pageNum Math.max(1, pageNum)并在Page类中增加校验if (pageNum 1) throw new IllegalArgumentException(页码必须1);陷阱3数据库时间字段类型不匹配引发排序失效现象分页时新上传图片不显示在首页时间顺序混乱根因SqlServer中upload_time字段为DATETIME精度3.33ms而Javanew Date()精度为毫秒导致ORDER BY upload_time DESC时相同毫秒值的记录顺序不确定方案将字段改为DATETIME2(3)精度1ms或在SQL中添加二级排序ORDER BY upload_time DESC, id DESC -- 用主键id保序5.3 生产环境加固建议来自某政务系统实战本项目虽为教学示例但若要部署到内网生产环境必须做以下加固1. 文件类型白名单校验不止contentType在UploadServlet中增加文件头校验// 读取文件前4字节比对Magic Number byte[] header new byte[4]; filePart.getInputStream().read(header); String hexHeader bytesToHex(header); if (!hexHeader.equals(FFD8FFDB) !hexHeader.equals(89504E47)) { throw new ServletException(非法图片文件非JPEG/PNG); }Magic Number说明FFD8FFDB是JPEG文件头89504E47是PNG文件头。仅靠contentType可被伪造。2. 上传目录权限隔离创建专用Windows用户tomcat_upload仅赋予D:\uploads读写权限