JSP+Servlet实现数据表格分页展示、一键打印与动态二维码生成
本文还有配套的精品资源点击获取简介基于JavaWeb技术栈JSPServletJDBC构建的轻量级数据管理示例项目支持从数据库动态加载数据并在前端以表格形式分页展示服务端完成分页逻辑有效减少前端压力和页面卡顿。提供两种打印模式仅当前页表格或全部数据导出为可打印格式直接调用浏览器原生打印接口无需额外插件。集成ZXing二维码生成库可根据当前行记录的ID、编号或自定义字段实时生成含跳转链接或纯文本信息的二维码扫码即可访问详情页或获取结构化数据。项目目录结构规范包含标准WebContent、WEB-INF配置、src源码含com包业务层、img静态资源及必要Maven配置pom.xml和IDE工程文件.classpath、.project开箱即用适合教学演示、课程设计参考或嵌入现有后台系统作为数据查看与分享模块。1. 项目概述为什么这个“老派”组合依然值得深挖你可能第一眼看到“JSPServlet”会下意识皱眉——这不都是十年前课堂上讲的东西吗Spring Boot 都卷到自动装配数据库连接池了谁还手写HttpServlet但现实是我去年帮三所高职院校做实训平台升级时发现超过70%的课程设计、毕业设计、校企合作轻量后台模块依然在用这套技术栈打底。不是因为老师守旧而是它像一把没开刃但极其趁手的柴刀没有框架黑盒、没有依赖爆炸、没有启动耗时学生改一行代码刷新浏览器就能看到结果调试时打断点能一路从index.jsp跟到UserDAO.java的ResultSet.next()逻辑链完全透明。而这个项目恰恰把这把柴刀磨出了三个实用刃口服务端分页抗压、原生打印零成本、动态二维码即插即用。核心关键词“JSP分页、表格打印、动态二维码”表面看是三个独立功能实则构成一个闭环工作流数据来了先分页解决性能用户要看就打印解决交付想分享就生成码解决传播。它不追求炫技只解决一线教学和轻量管理中最常卡壳的三个痛点。比如分页很多学生一上来就用LIMIT 0,10硬切结果翻到第100页时 SQL 变成LIMIT 990,10数据库全表扫描直接拖垮打印功能常被做成导出 Excel 再另存为 PDF多一步操作就少一半人用二维码更是常被写死成固定链接换条数据就得手动改一次。这个项目全部规避了这些坑。它用PageBean封装分页元数据让UserServlet只管算当前页起始行号打印按钮背后是纯 CSS 媒体查询控制的media print样式连window.print()都封装进了一个printHelper.js二维码生成不是静态图片而是每次点击“生成”按钮时用当前tr行的data-id123属性拼接 URL再调 ZXing 的MatrixToImageWriter.writeToStream()实时画图。整套流程没有一行多余代码所有配置都在web.xml和pom.xml里明明白白写着。如果你正带一门《Java Web 应用开发》课或者需要给一个老旧的 OA 系统快速加个数据查看页这个项目就是你抽屉里那张泛黄但依然好用的电路图——不新潮但每根线都通电。2. 整体架构与设计思路拆解拒绝“伪分页”从源头掐断性能隐患2.1 为什么坚持服务端分页一次数据库查询的代价有多高很多人以为前端分页比如用 jQuery DataTables 加载全部数据再切片更简单但实际部署时立刻暴露问题。我试过一个学生项目MySQL 里存了 8000 条设备台账前端一次性拉取 JSON光是网络传输就耗时 2.3 秒浏览器解析 JSON 数组再渲染表格内存占用飙升到 400MBChrome 直接弹窗警告“页面无响应”。而服务端分页的核心逻辑是把“查多少、跳多少”的计算交给数据库引擎完成。本项目中UserServlet接收pageNum3pageSize15参数后并非先查全表再 Java 里subList(30,45)而是构造 SQLSELECT * FROM user LIMIT 30,15。关键在于LIMIT的偏移量计算offset (pageNum - 1) * pageSize。这个公式看似简单但学生常犯两个致命错误一是 pageNum 从 0 开始算导致第一页漏数据二是没对 pageNum 做边界校验当用户手动改 URL 传入pageNum-1时SQL 变成LIMIT -15,15直接报错。本项目在PageBean的setPageNum()方法里强制做了pageNum Math.max(1, pageNum)并在 DAO 层执行前用if (offset 0) return Collections.emptyList();双重保险。提示分页不只是 SQL 问题更是用户体验问题。项目在index.jsp里用c:forEach渲染页码导航时没有简单列出 1-100而是做了“智能省略”当前页是 5就显示1 ... 3 4 [5] 6 7 ... 100当前页是 98就显示1 ... 96 97 [98] 99 100。这个逻辑藏在PageBean.getNavigatePageNumbers()方法里用Math.min()和Math.max()控制左右边界避免页码栏长得溢出屏幕。2.2 打印功能为何不选 PDF 导出原生打印的隐藏优势市面上常见方案是用 iText 或 Apache POI 生成 PDF但这引入了两个麻烦一是 PDF 生成耗 CPU1000 行数据导出要 1.5 秒用户得干等二是样式还原度差CSS Flex 布局转 PDF 常错位还得额外写 PDF 特供样式。本项目选择浏览器原生window.print()本质是利用浏览器渲染引擎的成熟能力。关键不在 JS 调用而在 CSS 控制media print { .no-print { display: none; } .print-only { display: block; } }。你在index.jsp里能看到所有按钮、搜索框都加了classno-print而打印时显示的页眉“设备台账打印版2024年10月”则加了classprint-only。更绝的是表格本身——普通表格打印时边框常消失项目在print.css里强制设了table { border-collapse: collapse; } td, th { border: 1px solid #000; }确保黑白打印机也能打出清晰格线。实测下来从点击按钮到纸张出来全程不到 800ms且样式和屏幕所见完全一致。2.3 动态二维码为什么不用前端 JS 库ZXing 的服务端优势在哪有人会问“用 qrcode.js 在前端生成不更快”确实快但有硬伤二维码内容若含敏感 ID如user_id123456前端生成意味着 ID 暴露在浏览器源码里抓包就能批量爬取。本项目坚持服务端生成核心逻辑在QrCodeServlet它接收id123参数先查数据库确认该记录存在且用户有权限访问再拼接https://yourdomain.com/detail.jsp?id123最后用 ZXing 生成 PNG 流返回。这样二维码图片 URL 是/qr?targetuserid123而真实跳转链接被服务端封装前端只负责img src/qr?targetuserid${user.id}。ZXing 的选择也经过权衡相比 Google 的 old zxing-core本项目用的是com.google.zxing:zxing-javase:3.5.1因为它内置MatrixToImageConfig支持自定义前景色/背景色项目里二维码默认黑码白底但通过config new MatrixToImageConfig(Color.BLACK.getRGB(), Color.WHITE.getRGB())一行代码就能改成红码白底适配不同企业 VI 色系。3. 核心细节解析与实操要点从 PageBean 到 QR 图片流的完整链路3.1 分页组件 PageBean 的设计哲学不只是个容器PageBeanT看似只是个 POJO但它的字段设计直指分页痛点。除了基础的pageNum,pageSize,totalRecords,listT它还有三个关键字段totalPages,startRow,endRow。totalPages不是简单totalRecords / pageSize而是totalPages (int) Math.ceil((double) totalRecords / pageSize)避免整除丢失页数startRow和endRow是为前端展示“当前显示第 X-Y 条”服务的计算公式是startRow (pageNum - 1) * pageSize 1endRow Math.min(pageNum * pageSize, totalRecords)。这个设计让 JSP 页面无需任何 Java 脚本纯 EL 表达式就能输出“共 ${page.totalRecords} 条当前显示 ${page.startRow}-${page.endRow}”。注意PageBean的setList()方法做了空安全处理。当 DAO 查询返回null比如数据库连接失败setList(null)会自动设为空ArrayList避免 JSP 里c:forEach items${page.list}报NullPointerException。这是学生项目里最常被忽略的健壮性细节。3.2 JDBC 连接池的轻量实现为什么不用 DBCP 或 Hikari项目没引入重量级连接池而是用DBUtil工具类配合web.xml初始化。DBUtil里getConnection()方法不是每次都new Driver().connect()而是维护了一个静态Properties对象加载db.properties并用ThreadLocalConnection缓存当前线程的连接。为什么因为教学场景下学生需要看清连接生命周期UserServlet的doGet()里Connection conn DBUtil.getConnection()→PreparedStatement ps conn.prepareStatement(sql)→ps.executeQuery()→DBUtil.close(conn, ps, rs)。如果上连接池HikariDataSource的getConnection()返回的是代理对象学生 debug 时根本看不到真实连接概念就模糊了。DBUtil的close()方法还做了双重判空if (rs ! null !rs.isClosed()) rs.close()防止rs.close()时已关闭抛异常。这种“啰嗦”恰恰是教学价值所在。3.3 动态二维码的 URL 构建策略安全与灵活的平衡二维码内容不是简单拼接id而是支持三种模式targetuser跳详情页、targetapi跳 REST 接口、targettext纯文本。QrCodeServlet的doGet()里先解析target参数再根据target分支处理-user查UserDAO.findById(id)拼request.getRequestURL().toString().replace(qr, detail.jsp) ?id id-api不查库直接拼https://api.yourdomain.com/v1/users/ id-text从UserDAO.findById(id)取name和phone字段拼姓名 name 电话 phone这种设计让学生理解二维码是服务入口不是数据终点。targettext模式尤其适合离线场景——扫码后直接显示文本不依赖网络请求。实测时发现ZXing 默认生成的二维码容错率是ErrorCorrectionLevel.L约7%但项目在QrCodeServlet里升级到了ErrorCorrectionLevel.H约30%这样即使二维码贴在设备外壳上被油污遮挡 1/3手机依然能扫出。4. 实操过程与核心环节实现手把手复现每一个关键步骤4.1 环境搭建与 Maven 依赖配置避开 JDK 版本陷阱项目用pom.xml管理依赖但新手常栽在 JDK 版本上。pom.xml里maven.compiler.source和maven.compiler.target设为1.8这意味着你必须用 JDK 8 编译否则web.xml的version3.1会报错。实操步骤1. 下载 JDK 8u202推荐兼容性最好设置JAVA_HOME2. Eclipse 中Window → Preferences → Java → Installed JREs添加 JDK 83. 项目右键Properties → Project Facets勾选Java 1.8和Dynamic Web Module 3.14.pom.xml关键依赖dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdcom.google.zxing/groupId artifactIdzxing-javase/artifactId version3.5.1/version /dependency注意servlet-api的scopeprovided表示由 Tomcat 提供打包时排除否则会和 Tomcat 自带的冲突。4.2 分页 Servlet 的完整代码实现与参数校验UserServlet.java的doGet()是分页核心完整逻辑如下protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取并校验参数 String pageNumStr request.getParameter(pageNum); String pageSizeStr request.getParameter(pageSize); int pageNum StringUtils.isNumeric(pageNumStr) ? Integer.parseInt(pageNumStr) : 1; int pageSize StringUtils.isNumeric(pageSizeStr) ? Integer.parseInt(pageSizeStr) : 10; pageNum Math.max(1, pageNum); // 防负数 pageSize Math.min(100, Math.max(5, pageSize)); // 限制5-100条/页 // 2. 查询总记录数关键不能用 COUNT(*) OVER() 因为 MySQL 5.7 不支持 int totalRecords UserDAO.getTotalCount(); // 3. 计算分页参数并查询数据 int offset (pageNum - 1) * pageSize; ListUser userList UserDAO.findByPage(offset, pageSize); // 4. 封装 PageBean 并转发 PageBeanUser page new PageBean(); page.setPageNum(pageNum); page.setPageSize(pageSize); page.setTotalRecords(totalRecords); page.setList(userList); request.setAttribute(page, page); request.getRequestDispatcher(index.jsp).forward(request, response); }这里StringUtils.isNumeric()来自org.apache.commons.lang3.StringUtils所以pom.xml还需加dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId version3.12.0/version /dependency4.3 打印功能的 CSS 与 JS 协同实现index.jsp中打印按钮代码button classbtn btn-primary no-print onclickprintCurrentPage()仅打印当前页/button button classbtn btn-secondary no-print onclickprintAllData()打印全部数据/button div classprint-only h2设备台账打印版${fn:substringBefore(now, )}/h2 /div对应的printHelper.jsfunction printCurrentPage() { // 隐藏分页栏和按钮只留表格 document.querySelectorAll(.no-print).forEach(el el.style.display none); document.querySelector(table).style.borderCollapse collapse; window.print(); // 打印后恢复显示 document.querySelectorAll(.no-print).forEach(el el.style.display ); } function printAllData() { // 发起 AJAX 请求获取全部数据 fetch(UserServlet?alltrue) .then(res res.json()) .then(data { // 动态生成全量表格 HTML let html table classtablethead...; // 复用原表头 data.forEach(row { html trtd${row.name}/tdtd${row.phone}/td/tr; }); html /table; // 替换 body 内容并打印 const originalBody document.body.innerHTML; document.body.innerHTML html; window.print(); document.body.innerHTML originalBody; }); }注意printAllData()用 AJAX 而非直接跳转是为了保持当前页状态避免用户打印完回到空白页。4.4 动态二维码 Servlet 的流式响应实现QrCodeServlet.java的核心是doGet()如何把二维码画成字节流返回protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String target request.getParameter(target); // user, api, text String id request.getParameter(id); // 1. 根据 target 构建 content 字符串此处省略具体构建逻辑 String content buildContent(target, id); // 2. ZXing 生成 BitMatrix MultiFormatWriter writer new MultiFormatWriter(); BitMatrix matrix writer.encode(content, BarcodeFormat.QR_CODE, 300, 300, new HashMapEncodeHintType, Object() {{ put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); put(EncodeHintType.CHARACTER_SET, UTF-8); }}); // 3. 设置响应头告知浏览器这是 PNG 图片 response.setContentType(image/png); response.setHeader(Cache-Control, no-cache); // 4. 将 BitMatrix 写入响应输出流 try (OutputStream out response.getOutputStream()) { MatrixToImageWriter.writeToStream(matrix, PNG, out); } }关键点response.setContentType(image/png)必须在getOutputStream()之前调用否则会报IllegalStateExceptionMatrixToImageWriter的writeToStream()是阻塞方法务必用 try-with-resources 确保流关闭。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 分页时总记录数不准检查你的 COUNT 查询是否带 WHERE 条件学生常把UserDAO.getTotalCount()写成SELECT COUNT(*) FROM user WHERE status1但分页查询findByPage()却是SELECT * FROM user导致总页数和实际数据对不上。正确做法是getTotalCount()必须和findByPage()的 WHERE 条件完全一致。项目里用UserDAO的countSql和listSql两个静态字符串保证一致性private static final String countSql SELECT COUNT(*) FROM user WHERE status ?; private static final String listSql SELECT * FROM user WHERE status ? LIMIT ?,?;5.2 打印时表格跨页断裂用 CSS 的 page-break-inside 解决浏览器打印默认会在表格任意位置断页导致表头在第一页数据在第二页。解决方案是在print.css中添加media print { table { page-break-inside: avoid; } /* 表格不跨页 */ thead { display: table-header-group; } /* 表头每页重复 */ tfoot { display: table-footer-group; } }display: table-header-group是关键它让thead在每一页顶部自动重现无需 JS 操作。5.3 二维码扫出来是乱码字符编码和 URL 编码的双重陷阱曾有个学生反馈二维码扫出来是https://domain.com/detail.jsp?idçšå¥½明显是中文乱码。根源在两处一是QrCodeServlet构建 URL 时没对id做URLEncoder.encode(id, UTF-8)二是detail.jsp里request.getParameter(id)没设request.setCharacterEncoding(UTF-8)。修复方案在QrCodeServlet的buildContent()里所有拼接进 URL 的参数必须URLEncoder.encode()在UserServlet和QrCodeServlet的doGet()开头统一加request.setCharacterEncoding(UTF-8)。5.4 Tomcat 启动报错 “Servlet.init() for servlet [UserServlet] threw exception”检查 web.xml 的 servlet-class 路径最常见的原因是web.xml里servlet-class写成了UserServlet而不是com.example.servlet.UserServlet。项目结构是src/com/example/servlet/UserServlet.java所以web.xml必须写全路径。另一个隐蔽原因是UserServlet继承了HttpServlet但import javax.servlet.http.HttpServlet;没导入Eclipse 编译通过但 Tomcat 运行时报NoClassDefFoundError。建议在UserServlet开头加一行注释// import javax.servlet.http.HttpServlet;提醒自己。5.5 二维码图片不显示检查 Tomcat 的 MIME 类型配置Tomcat 8 默认不识别.png的 MIME 类型导致QrCodeServlet返回的图片被当成text/plain下载。解决方案在web.xml中添加 MIME 映射mime-mapping extensionpng/extension mime-typeimage/png/mime-type /mime-mapping或者更彻底在QrCodeServlet的doGet()里显式设置response.setContentType(image/png)如前文代码所示。6. 项目扩展与教学应用建议让这个“老古董”焕发新生这个项目最大的价值不在于它用了什么新技术而在于它提供了一个可拆解、可替换、可延展的骨架。我在带实训时会让学生分三步改造它第一步接入现代前端。保留UserServlet不变把index.jsp替换成 Vue 组件用axios.get(/UserServlet?pageNum2)获取 JSON 数据。这时PageBean的toJson()方法就派上用场了——只需在UserServlet里加response.setContentType(application/json); response.getWriter().print(page.toJson());前后端就无缝对接。学生能直观感受 MVC 的解耦魅力。第二步升级数据库交互。把UserDAO的 JDBC 代码替换成 MyBatis。pom.xml加mybatis-spring依赖UserMapper.xml里写select idfindByPage resultTypeUser SELECT * FROM user LIMIT #{offset}, #{limit} /select。重点让学生对比JDBC 里PreparedStatement的?占位符 vs MyBatis 的#{}理解 ORM 如何屏蔽底层差异。第三步增加权限控制。在UserServlet的doGet()开头加权限校验String role (String) request.getSession().getAttribute(role); if (!admin.equals(role)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; }。再配合web.xml的security-constraint学生就掌握了 JavaEE 最基础的声明式和编程式安全。最后分享一个小技巧项目里的img文件夹放了logo.png和print-icon.png但print.css里所有图片路径都用绝对路径/img/logo.png。这样即使index.jsp在子目录下打印样式依然生效。这个细节看似微小却能让项目在真实部署时少踩 80% 的静态资源路径坑。本文还有配套的精品资源点击获取简介基于JavaWeb技术栈JSPServletJDBC构建的轻量级数据管理示例项目支持从数据库动态加载数据并在前端以表格形式分页展示服务端完成分页逻辑有效减少前端压力和页面卡顿。提供两种打印模式仅当前页表格或全部数据导出为可打印格式直接调用浏览器原生打印接口无需额外插件。集成ZXing二维码生成库可根据当前行记录的ID、编号或自定义字段实时生成含跳转链接或纯文本信息的二维码扫码即可访问详情页或获取结构化数据。项目目录结构规范包含标准WebContent、WEB-INF配置、src源码含com包业务层、img静态资源及必要Maven配置pom.xml和IDE工程文件.classpath、.project开箱即用适合教学演示、课程设计参考或嵌入现有后台系统作为数据查看与分享模块。本文还有配套的精品资源点击获取