
简介这是一份基于Java的校园在线打印系统毕业设计源码包面向计算机相关专业毕业生、Java Web开发者及毕业设计备赛学生。系统以SpringBootMybatis构建后端Thymeleaf模板渲染页面前端集成Layui框架配合MySQL数据库实现了首页商品展示、印品类型详情、订单管理、后台管理等核心功能项目结构按标准Maven模式分层便于快速理解业务与二次开发。压缩包共258个文件包含68个Java源码、30个HTML页面、34个JavaScript脚本、9个CSS样式、2个SQL脚本及多项图片、配置与字体资源源码覆盖控制层、业务层、持久层完整链路SQL脚本可直接创建数据库表静态资源还原了可运行的前端界面整体体积仅2.45MB方便下载研读。此份源码已有426人学习既能帮助读者快速搭建一个完整的Web打印业务流也能直观掌握SpringBoot与Mybatis整合、Layui后台布局及订单状态流转等关键编码思路对完成课程设计或毕业设计具有较高的参考价值。1. 从毕业设计到能跑的服务这个 JAVA 在线打印系统到底能给你什么每年到这个季节总有人拿着「JAVA 在线打印系统」的题目来问我要路子。原因不外乎两点这题不像电商、OA 那么烂大街又比贪吃蛇、图书管理这类纯 CRUD 有讲头。这套在线打印系统源码我拆过不止一遍它的核心是 JSP Servlet MySQL 三层结构业务上涵盖了用户登录、文件上传、订单计价、打印任务状态流转这些恰好都是 Java Web 里最常被面试官追问的点。它适合两类人一是毕设选题定了在线打印但还没开工的想直接拿一份完整工程跑通再改成自己的二是学完 Java 基础但没完整做过项目的人想看看一个能答辩的 Web 系统到底长什么样。下面我按拆包的顺序把架构、跑通方法、核心代码和几个真正会让人翻车的细节过一遍。2. 分层识源码JSP Servlet MySQL的请求流转与表结构拿到这套源码第一件事千万别急着点运行。先把工程结构展开看一遍搞清楚谁在承接请求、谁在读写数据库后面所有模块调不通的问题大半都能在这一章里找到答案。2.1 一条打印请求从前端到数据库经过了哪些层这套系统用的是最经典的 JSP Servlet 三层架构没有引入 Spring 那套重量级框架。不是技术保守而是毕业设计答辩时你用手画一张请求流程图说清楚数据经过了哪三层比讲一堆注解要实在得多。一次完整的在线打印请求在代码里是这样走的浏览器提交打印表单JSP 页面把用户选的打印机、纸张、份数、彩色还是黑白封装成请求参数。web.xml 里配置的 Servlet 拦截到 URL比如/order在 doPost 方法里接收参数。Servlet 层做参数校验比如份数不能大于 100文件大小不能超过设定值。校验通过后调用 Service 层典型如OrderService.createOrder()。Service 层调用 DAO 层的OrderDao.insert(order)DAO 层用 JDBC 执行 INSERT。数据库返回自增主键Service 层再把订单号写回跳转到支付页或成功页。我习惯在源码里直接搜response.sendRedirect和request.getRequestDispatcher这两个方法跳转语句一出来整个页面的流转路径就清晰了。这套系统里/print/upload是上传入口/print/order是下单入口/admin/task/list是后台任务列表三个入口对应三段独立的 Servlet。2.2 数据库五张表用户、文件、打印机、订单和任务怎么串起来在线打印系统的数据结构不复杂但表之间的外键关系要能自圆其说答辩老师最爱问「为什么任务表和订单表分开建」。我给这套源码里的核心表结构按常见实现补全一下方便你对照理解CREATE TABLE t_user ( id INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR ( 64 ) NOT NULL COMMENT 登录名, password VARCHAR ( 128 ) NOT NULL COMMENT MD5加密后的密码, phone VARCHAR ( 20 ) DEFAULT NULL COMMENT 手机号用于登录, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE INNODB DEFAULT CHARSET utf8mb4 COMMENT 用户表; CREATE TABLE t_file ( id INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT 文件ID, user_id INT ( 11 ) NOT NULL COMMENT 上传用户, file_name VARCHAR ( 255 ) NOT NULL COMMENT 原始文件名, file_path VARCHAR ( 255 ) NOT NULL COMMENT 服务端存储路径, file_size BIGINT DEFAULT 0 COMMENT 文件大小单位字节, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINE INNODB DEFAULT CHARSET utf8mb4 COMMENT 上传文件表; CREATE TABLE t_printer ( id INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT 打印机ID, printer_name VARCHAR ( 128 ) NOT NULL COMMENT 打印机名称或IP, location VARCHAR ( 128 ) DEFAULT NULL COMMENT 所在位置, status TINYINT DEFAULT 1 COMMENT 1可用 0离线, PRIMARY KEY (id) ) ENGINE INNODB DEFAULT CHARSET utf8mb4 COMMENT 打印机表; CREATE TABLE t_order ( id INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR ( 32 ) NOT NULL COMMENT 业务订单号, user_id INT ( 11 ) NOT NULL, file_id INT ( 11 ) NOT NULL, printer_id INT ( 11 ) NOT NULL, total_amount INT ( 11 ) NOT NULL COMMENT 总金额单位分, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINE INNODB DEFAULT CHARSET utf8mb4 COMMENT 打印订单表; CREATE TABLE t_print_task ( id INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT 任务ID, order_id INT ( 11 ) NOT NULL COMMENT 关联订单, task_status TINYINT DEFAULT 0 COMMENT 0排队中 1打印中 2已完成 3失败, pages INT DEFAULT 1 COMMENT 打印页数, copies INT DEFAULT 1 COMMENT 打印份数, color_type TINYINT DEFAULT 0 COMMENT 0黑白 1彩色, duplex TINYINT DEFAULT 0 COMMENT 0单面 1双面, finish_time DATETIME DEFAULT NULL COMMENT 完成时间, PRIMARY KEY (id) ) ENGINE INNODB DEFAULT CHARSET utf8mb4 COMMENT 打印任务表;这里有三处值得注意。路径字段file_path用的是 VARCHAR(255)不是 TEXT因为存储的是相对路径比如uploads/202406/xxx.pdf相对路径的好处是项目迁移时不用改数据库里的绝对路径。金额字段total_amount我用 INT 以「分」为单位不用 DOUBLEJava 里0.1 0.2的浮点误差在计费系统里是典型坑用整型分可以彻底绕开。任务表和订单表分开是合理的一张订单可以包含多个文件、多次打印任务比如一个订单里同时打印两份不同的 PDF。如果合在一张表里每次加一个文件就要复制一遍订单号冗余严重拆开后订单表只关心支付状态任务表只关心打印状态两件事互不干扰。2.3 连接管理用 JDBC 工具类别把 Connection 散落在每个 DAO 里拆这套源码时我第一个看的就是数据库连接怎么管理。最稳妥的写法是写一个DBUtil工具类把驱动的加载和连接的关闭统一收口import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/print_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { // 驱动类在 MySQL 8.x 下必须显式加载 Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败请检查 jar 包); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }serverTimezoneAsia/Shanghai这个参数不能省新版 MySQL 驱动的时区校验很严格缺了它连接会直接报错。useSSLfalse则是因为本地开发环境没必要做 SSL 握手省去一层开销。DAO 层的写法也有讲究。我在源码里见到有人直接在 Servlet 里写Class.forName这属于把代码写死换环境就得改 Servlet 源码。正确做法是数据访问统一收到 DAO 类里Servlet 只调方法public User findByUsername(String username) { String sql SELECT id, username, password, phone FROM t_user WHERE username ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setPassword(rs.getString(password)); u.setPhone(rs.getString(phone)); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }所有 SQL 都用PreparedStatement拼参数禁止用字符串拼接 SQL。findByUsername如果写成SELECT * FROM t_user WHERE username username 输入 or 11就能绕过登录校验这是答辩时的送命题。3. 快速跑通环境版本搭配与导入部署的六个关键点源码拿到手环境不对照样跑不起来。这一章我按自己平时的部署习惯把从解压到看到首页的整个流程拆成六步每步都有具体的版本和配置。3.1 环境别乱追新JDK 8、Tomcat 8.5、MySQL 5.7 的搭配逻辑这套源码是按传统 Java Web 工程写的环境配错是启动失败的第一大原因。我给的组合是组件推荐版本选型理由JDK1.8源码的编译级别是 Java 8高版本 JDK 会有模块化访问限制Tomcat8.5.xServlet 3.1 规范支持注解配置和 JDK 8 配合最稳MySQL5.7 或 8.05.7 默认字符集够用8.0 需注意驱动版本要用com.mysql.cj.jdbc.DriverIDEEclipse 或 IDEA两者都行导入时统一把编码切成 UTF-8为什么不用 Tomcat 9 或更高Tomcat 9 对应 Servlet 4.0本身兼容但这种老工程里如果引了某些旧版依赖在高版本容器下会出现NoClassDefFoundError排查起来非常浪费时间。毕业设计阶段稳定跑通比追新版本重要得多。3.2 导入与配置从解压到数据库连接串改对只差这一步导入前先做一件事把整个工程目录检查一遍确认里面有src、WebContent或webapp、pom.xml如果是 Maven 工程这三层结构。如果是普通的 Dynamic Web Project没有pom.xml那依赖的 jar 包一定在WebContent/WEB-INF/lib下数一下 jar 个数少了马上补。数据库连接配置通常在src目录下的db.properties里jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/print_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456密码是最容易出问题的地方。如果你的本地 MySQL 密码是root123改完这里就完事。但要注意MySQL 8.0 默认的认证插件是caching_sha2_password而老版本 JDBC 驱动只认mysql_native_password。解决方法是把驱动换成mysql-connector-java 8.0.x或者在 MySQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;改完连接串、导入 SQL 脚本建库建表之后才算把准备工作做完。3.3 启动顺序与验证先看日志再看页面别一上来就疯狂刷新启动 Tomcat 的正确顺序是先确认 MySQL 服务在跑再启动 Tomcat。如果反过来Tomcat 启动时数据源初始化失败即使后面 MySQL 起来了应用也不会自动重新连接。在 IDEA 或 Eclipse 里部署后先切到Console标签看日志# 看到这一行说明应用部署成功 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [D:\apache-tomcat-8.5.98\webapps\print.war] # 看到这一行说明容器加载完成没有报错 INFO: Server startup in [3,842] milliseconds如果启动日志里出现Exception或Caused by:不要往下翻了直接看第一行异常信息百分之八十是数据库连接问题。应用里自带一个初始化 Servlet 或ApplicationListener启动时会自动初始化打印机列表如果日志里没有「初始化打印机数量xx」这类输出说明数据库连接没通。验证页面的顺序是固定的先访问http://localhost:8080/print/看首页再注册一个新账号然后用这个账号登录进入用户中心。如果首页能出来但登录后 500问题多半在 Session 或者用户表的字段匹配上。4. 核心业务落到代码文件上传、计价逻辑与任务状态流转跑通只是第一步。答辩时老师问的不只是「能不能跑」而是「这个功能怎么实现的」。在线打印系统最核心的三个业务点必须能指着代码讲出来。4.1 文件上传要用 multipart 解析库容量和类型双重限制原生 Servlet 解析上传文件非常痛苦要自己读InputStream拆 boundary。这套源码里用的是 Apache Commons FileUpload这是一个成熟的解析库。上传 Servlet 的核心写法WebServlet(/print/upload) MultipartConfig(maxFileSize 10485760, // 单个文件最大10MB maxRequestSize 20971520, // 整个请求最大20MB fileSizeThreshold 4096) // 超过4KB写临时文件 public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); // 从会话里取用户 ID确认登录状态 HttpSession session request.getSession(); Integer userId (Integer) session.getAttribute(userId); if (userId null) { response.sendRedirect(login.jsp); return; } Part filePart request.getPart(file); String submittedFileName filePart.getSubmittedFileName(); // 白名单校验防止上传 jsp、exe 等危险文件 String ext submittedFileName.substring(submittedFileName.lastIndexOf(.) 1).toLowerCase(); if (!pdf.equals(ext) !doc.equals(ext) !docx.equals(ext)) { request.setAttribute(error, 仅支持 PDF / DOC / DOCX 格式); request.getRequestDispatcher(upload.jsp).forward(request, response); return; } // 用时间戳重命名避免文件名中文乱码和服务端同名覆盖 String newName System.currentTimeMillis() _ submittedFileName; String savePath getServletContext().getRealPath(/uploads) File.separator newName; filePart.write(savePath); // 保存文件记录到数据库 FileDao fileDao new FileDao(); fileDao.insert(userId, submittedFileName, uploads/ newName, filePart.getSize()); response.sendRedirect(order.jsp?fileId fileDao.getLastInsertId()); } }MultipartConfig的三个参数值得专门讲。maxFileSize限制的是单个文件但用户可能一次传五个文件所以还要用maxRequestSize卡住整个请求的总大小。fileSizeThreshold的意思是文件小于 4KB 时直接放内存超过则落临时文件避免大文件挤爆堆内存。文件名处理是这里最大的坑。用户上传的文件名可能叫「毕业论文终稿2.pdf」里面有中文和空格。保存到服务器前必须重命名我习惯用System.currentTimeMillis()加下划线前缀这样既避免中文乱码又不会和别人的文件冲突。同时数据库里存的是原始文件名页面展示时用数据库里的名字服务端用重命名后的名字两边各司其职。扩展名白名单必须用lastIndexOf(.)而不是endsWith(.pdf)。如果用户传的文件名末尾是xxx.pdf.jpgendsWith拦截不住后面再拼一个.jsp的情况而lastIndexOf取的是最后一个点后面的部分能挡住这种绕过。4.2 计价逻辑金额用「分」存储页数乘以单价再加装订费计价规则通常是答辩时的高频问题。这套系统的计价核心可以抽象成一张配置表项目价格分说明黑白单面10 分/页最基础的打印黑白双面16 分/页双面打印价格按页数算但一份纸印两面彩色单面50 分/页彩色耗材成本高彩色双面80 分/页装订费200 分/本可选按本数收计价代码要注意浮点陷阱我写成这样public int calcAmount(PrintTask task, int pageCount) { // 单价都定义为 int单位是分 int singlePrice 0; if (task.getColorType() 0 task.getDuplex() 0) { singlePrice 10; } else if (task.getColorType() 0 task.getDuplex() 1) { singlePrice 16; } else if (task.getColorType() 1 task.getDuplex() 0) { singlePrice 50; } else if (task.getColorType() 1 task.getDuplex() 1) { singlePrice 80; } // 双面打印时页数除以2向上取整 int effectivePages (task.getDuplex() 1) ? (pageCount 1) / 2 : pageCount; int amount effectivePages * singlePrice * task.getCopies() (task.isBinding() ? 200 : 0); return amount; }关键在双面打印的页数换算。一份 15 页的 PDF双面打印需要 8 张纸所以(15 1) / 2的向上取整写法比Math.ceil更直观。向上取整的意义在于第 15 页不能和第 16 页拼在一张纸上多出来的半张纸也是成本。为什么金额要用分不用元因为 Java 的double做乘法会出2.0 * 0.1 0.20000000000000001订单金额出现这种尾巴没法看。用分做单位全部走整数运算最后展示时再除以 100 转成字符串。4.3 任务状态机七种状态之间谁允许跳到谁用常量类管住打印任务最怕的是状态乱跳。已取消的订单不能再变成已支付这个约束必须写在代码里。我的做法是在系统内定义一组状态常量public class TaskStatus { public static final int PENDING_PAY 0; // 待支付 public static final int PAID 1; // 已支付排队中 public static final int PRINTING 2; // 打印中 public static final int COMPLETED 3; // 已完成 public static final int CANCELED 4; // 已取消 public static final int FAILED 5; // 打印失败 // 允许的状态迁移路径key 是当前状态value 是可达状态 public static final MapInteger, ListInteger TRANSITIONS new HashMap(); static { TRANSITIONS.put(PENDING_PAY, Arrays.asList(PAID, CANCELED)); TRANSITIONS.put(PAID, Arrays.asList(PRINTING, CANCELED)); TRANSITIONS.put(PRINTING, Arrays.asList(COMPLETED, FAILED)); } public static boolean canTransition(int from, int to) { return TRANSITIONS.get(from) ! null TRANSITIONS.get(from).contains(to); } }不要小看这个 Map 的作用。没有它代码里谁都敢写task.status 3哪天出现一个从待支付直接跳到已完成的订单你查三天都查不出来。有了canTransition每次更新前先校验非法流转直接抛异常数据层面的脏状态从根本上被拦住。实际执行更新时SQL 要带上当前状态条件防止并发下两次更新都成功public int updateStatus(int taskId, int fromStatus, int toStatus) { String sql UPDATE t_print_task SET task_status ? WHERE id ? AND task_status ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, toStatus); ps.setInt(2, taskId); ps.setInt(3, fromStatus); // 必须匹配当前状态 return ps.executeUpdate(); // 返回0说明状态不是 fromStatus更新失败 } catch (SQLException e) { e.printStackTrace(); return -1; } }WHERE id ? AND task_status ?这叫乐观锁的简陋版。两个人同时点了「取消」和「支付」只有一个人的 UPDATE 能成功 — 另一个人因为task_status已被改成PAID不再匹配PENDING_PAY更新结果返回 0代码拿到 0 就提示「操作失败请刷新」。4.4 支付回调与打印完成异步状态更新怎么模拟真实打印系统里支付成功回调是第三方支付平台异步通知的。但这套毕业设计系统没有接入支付宝或微信支付它用的是「余额扣除」或「模拟支付」模式。模拟支付的做法很朴素支付 Servlet 里把订单状态从0 待支付改成1 已支付同时扣减用户余额两个操作放在同一个事务里。事务的写法值得单独提一下Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 扣减用户余额 userDao.deductBalance(conn, userId, amount); // 更新订单状态 orderDao.updateStatus(conn, orderId, TaskStatus.PAID); conn.commit(); // 两件事都成功才提交 } catch (SQLException e) { if (conn ! null) try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } throw new ServletException(支付失败订单已回滚, e); } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { } }注意这里为什么不用之前的DBUtil.getConnection()工具类获取连接然后直接操作因为工具类的关闭方法是立刻conn.close()而事务要求同一个连接里完成多条 SQL最后统一提交或回滚。如果每个 DAO 方法都自己拿连接、自己关事务就散了。至于「打印完成」这种异步事件毕业设计级别不需要真的对接打印机硬件。系统在后台任务列表里放了一个「模拟完成」按钮管理员点击后把任务状态从2 打印中改成3 已完成并记录完成时间。如果你想做得更真实一点可以在打印任务列表页面加一个定时刷新// 每15秒轮询一次任务状态页面自动刷新 setInterval(function () { fetch(task/status?taskId taskId) .then(res res.json()) .then(data { if (data.status 3) { document.getElementById(taskState).innerText 已完成; } }); }, 15000);这段代码的作用是让前台页面能感知后台的状态变化不用用户手动刷新页面。15 秒的轮询间隔不会给服务器造成压力又保证了演示时任务的完成状态能及时出现在屏幕上。5. 避坑清单部署和演示前最容易翻车的五个位置这章整理的是我从接手这类源码起反复踩过的坑每一条都对应一个具体的报错或界面异常。建议你在正式答辩前把这五条逐个对着源码检查一遍。5.1 上传的中文文件名变成乱码页面还报 404现象用户上传「毕业论文终稿.pdf」系统里显示????.pdf打开打印预览时图片加载不出来。原因Tomcat 8.5 默认的 URI 编码是 UTF-8但如果你在 Eclipse 里把工作区的编码设成 GBK或者项目里web.xml没有声明编码过滤器文件名经过 request 解析时就乱码了。解决在web.xml里加一个编码过滤器或者确认项目的Content Type设置。最直接的办法是在上传 Servlet 的doPost最开头加上request.setCharacterEncoding(UTF-8)然后检查 Tomcat 的conf/server.xml中 Connector 段落Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 /这个配置里没有URIEncoding时Tomcat 8.5 默认用 UTF-8 解析 URL但表单 POST 的 body 编码取决于页面charset。所以推荐在 JSP 文件第一行确认统一为% page contentTypetext/html;charsetUTF-8 %。5.2 上传超过 10MB 的文件一点就报 413 或直接弹错误页现象传一个 20MB 的 PDF页面直接 500后台日志报FileUploadException: The request was rejected because its size exceeds the configured maximum。原因MultipartConfig(maxFileSize 10485760)把单文件限死在 10MB。这个限制是硬性的超过就拒绝请求不是警告。解决需要调大限制同时调整 Tomcat 层级的限制。Tomcat 的maxPostSize默认只有 2MB如果走的是 multipart 表单 POST这个参数会先拦截。在server.xml的 Connector 里加Connector port8080 protocolHTTP/1.1 maxPostSize10485760 maxSwallowSize20971520 /maxPostSize控制请求体上限maxSwallowSize控制连接器在响应后还能继续读取多少请求体的字节。这两个值要跟MultipartConfig保持一致或更大否则会有「Servlet 层放行了、容器层又给拦回来」的诡异现象。5.3 明明加了 MySQL 驱动 jar启动还是报 ClassNotFoundException现象Tomcat 启动到一半抛java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver但你看WEB-INF/lib目录里明明有mysql-connector-java-8.0.x.jar。原因jar 包在工程目录里但没被部署到 Tomcat 的WEB-INF/lib下。Eclipse 里这个情况特别常见 — 你把 jar 拖进了WebContent/WEB-INF/lib但没执行「右键项目 → Properties → Deployment Assembly → Add → Java Build Path Entries」。解决先打开 Eclipse 的Deployment Assembly面板确认WEB-INF/lib的条目里包含 MySQL 驱动 jar。更稳妥的做法是直接看 Tomcat 的部署目录# Tomcat 的 webapps 下展开的应用目录 ls -la apache-tomcat-8.5/webapps/print/WEB-INF/lib/如果这里没有 jar说明工程构建时没把依赖带进去。在 IDEA 里则要看Project Structure → Artifacts → Output Layout把依赖的 jar 勾进WEB-INF/lib后再重新 Build。5.4 数据库连接报 Communications link failure 或 Connection refused现象启动时日志出现com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure有时是Connection refused (Connection refused)。原因绝大多数情况不是驱动写错而是 MySQL 服务没启动或者端口不对。MySQL 8 默认端口 3306但如果机器上装了多个版本后装的把端口改成了 3307连接串里还是 3306 就必然连不上。解决先用命令行确认端口通不通# 检查 MySQL 端口是否在监听 netstat -ano | findstr 3306 # 如果没有任何输出说明 MySQL 服务没起来 # 在 Windows 服务里启动 MySQL或者用命令行启动 net start mysql如果端口通但仍报错再检查连接串里的serverTimezone参数是否带上。MySQL 8 驱动对时区敏感缺了serverTimezoneAsia/Shanghai会抛The server time zone value相关的异常这条几乎每个接手 MySQL 8 的人都会遇到。5.5 打印预览图片带反斜杠路径Linux 部署后白屏现象本地 Windows 上一切正常传到 Linux 服务器后上传文件后预览页图片全部加载失败浏览器 DevTools 里显示的图片 URL 是uploads/202406\28.jpg。原因代码里拼接文件访问 URL 时用了File.separatorWindows 下它是\拼出来的相对路径变成了uploads\202406\28.jpg。浏览器严格按/解析 URL反斜杠会导致路径失效。解决路径拼接固定写/不要用File.separator。服务端保存文件时可以用File.separator拼接真实磁盘路径但存进数据库的相对路径必须统一用/// 保存到磁盘用 File.separator这样 Windows 和 Linux 都对 String savePath uploadDir File.separator newName; filePart.write(savePath); // 存数据库只存 Web 访问路径固定用斜杠 String dbPath uploads/ newName; fileDao.insert(userId, submittedFileName, dbPath, filePart.getSize());顺带检查一下 JSP 或 HTML 模板里写死的资源路径前是否带了request.getContextPath()。如果不带项目部署在根路径没问题一旦打成 WAR 部署到/print子目录下所有 CSS、JS、图片都会 404。6. 答辩前不用慌JConsole 验证 JVM 状态再按脚本走一遍完整流程从「能跑」到「能演示」只差一步验证。我见过太多人答辩前五分钟系统突然卡死十有八九是 JVM 堆内存不够或演示流程没走过完整链路。用 JDK 自带的 JConsole 做一次体检。先启动 Tomcat再打开命令行运行jconsole弹出窗口后选择本地进程里的org.apache.catalina.startup.Bootstrap这就是 Tomcat 的 JVM。连接后看两个指标堆内存和线程数。堆内存曲线如果呈锯齿状不断上涨且 GC 后降不下来说明有对象泄漏多半是 Session 里存了太多大对象 — 我拆这套源码时发现有人把整个上传文件 byte 数组塞进了 session铁定是隐患。上传文件应该只在 request 范围内使用处理完立即释放引用。线程数如果稳定在几十个说明没有线程泄漏。如果持续增长检查数据库连接是否每次用完后真正关闭了 — 之前close()方法里如果漏掉了rs的关闭连接池里连接就攒不回来。体检完按这个脚本走一遍完整演示流程每步别跳启动 MySQL确认print_db库存在检查t_printer表里有没有可用打印机。启动 Tomcat访问http://localhost:8080/print/首页能打开且样式正常。注册新用户用test这种不会被占用的名字注册后跳到登录页重新登录。上传一个 2MB 左右的 PDF点击上传等待提示成功。进入下单页选择打印机、彩色/黑白、单面/双面、份数 3点击提交。查看订单详情金额计算是否正确然后模拟支付。切到后台管理端把任务状态从排队改为打印中再改为已完成。回到用户端确认任务状态同步为已完成订单历史里有这条记录。这套走完一遍大概五分钟但足以覆盖几乎所有 Demo 环节。当初我替一个学生调试这套系统结果就发现上传的 PDF 在 Linux 服务器上预览白屏 — 文件名里的中文从数据库取出来经过 URL 编码后和实际磁盘文件名对不上后来我在浏览器的 Network 面板里抓到真实的请求地址才发现是缩略图生成时没做 URL 编码用URLEncoder.encode(fileName, UTF-8)才解决。从那以后我每次接手毕业设计源码都会强制走一遍「上传中文文件 → 跨环境部署 → 完整流程演示」三步验证确认这条路是通的答辩时才不会在评委面前卡住。这套资源把完整的 JAVA 在线打印系统源码、数据库初始化 SQL、部署说明和开题相关的设计文档都打包在了一起。按前面章节的流程走你拿到手先建库、改连接串、部署、走一遍演示脚本再根据自己学校的要求去改功能细节整个流程会顺很多。希望帮到你。本文还有配套的精品资源点击获取