JavaWeb合同管理系统部署实战:从JSP/Servlet到MySQL与Tomcat

发布时间:2026/10/7 5:44:11
JavaWeb合同管理系统部署实战:从JSP/Servlet到MySQL与Tomcat 简介一套基于JavaWeb的合同管理系统完整项目面向JavaWeb学习者、毕业设计人员及企业信息化开发者覆盖合同录入、查询、记录、分析与权限控制等核心流程。资源为Eclipse工程包共941个文件、约16.18MBJSP页面负责前端展示Java类与Servlet处理业务逻辑并配有数据访问接口及数据库配置文件同时包含前端脚本、图片样式和依赖库结构清晰便于导入运行。已有333人浏览学习适合课程设计、企业实训或二次开发参考。压缩包内包含登录验证、图片验证码、合同增删改查、查询分析、导出Excel等源码模块可帮助读者理解Servlet与JSP组合开发模式提升从书本到项目的实践能力。1. 为什么还要自己部署一个 JavaWeb 合同管理系统公司合同管理还在靠 Excel 台账加微信群催办的话早晚会在合同到期、续签、归档这些环节上翻车。这个 JavaWeb 合同管理系统把合同的录入、审批、查询、到期预警收进一个 Web 应用里只要浏览器能访问业务人员就能操作不需要安装客户端。它的整体结构是典型的 JavaWeb 三层架构JSP 做页面展示Servlet 处理请求MySQL 存数据部署在 Tomcat 上。适合两类人一是要在公司内网快速搭一套合同管理工具的实施人员二是刚学完 JavaWeb、想拆一个完整项目案例来复现的开发者。下面我会按真实部署顺序把环境配置、数据库初始化、代码走读和踩坑点一次讲透。2. 核心功能与数据模型合同表结构怎么设计才够用2.1 功能边界与角色权限先搞清楚系统管到哪一层合同管理系统最容易犯的错是功能越做越重最后变成半个 OA。拆这个项目时我建议你先看它的权限模型。系统固定了三类角色管理员、法务、普通员工。普通员工只能录入合同、查看自己发起的合同法务负责审核管理员拥有全部权限包括删除、导出、分配审批人。这种设计在中小公司足够用而且实现简单——一张 user 表加一个 role 字段配合一个登录过滤器就能控制页面访问不需要引入 Spring Security 那套重武器。业务流程上合同从录入开始进入“待审批”状态法务审批通过后变为“生效中”合同到期前一天系统自动把状态标记为“待续签”管理员可以在列表页直接看到。还有一个容易被忽略的点合同变更。系统用“变更记录”表存每次修改的字段和操作人而不是直接覆盖主表这样审计时能追溯是谁在什么时候改了什么字段。做合同管理可追溯性比花哨功能重要得多。2.2 数据库设计五张核心表与关键字段说明我打开项目里的sql/contract.sql脚本核心表一共五张用户表t_user、合同主表t_contract、合同附件表t_contract_file、审批记录表t_approval、变更记录表t_contract_log。下面是合同主表的建表脚本字段和注释直接贴在 SQL 里CREATE TABLE t_contract ( contract_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 合同ID, contract_no VARCHAR(64) NOT NULL UNIQUE COMMENT 合同编号业务唯一键, contract_name VARCHAR(128) NOT NULL COMMENT 合同名称, party_a VARCHAR(128) NOT NULL COMMENT 甲方我方, party_b VARCHAR(128) NOT NULL COMMENT 乙方对方, amount DECIMAL(12,2) DEFAULT 0.00 COMMENT 合同金额单位元, sign_date DATE COMMENT 签订日期, start_date DATE NOT NULL COMMENT 生效开始日期, end_date DATE NOT NULL COMMENT 生效结束日期, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审批 1生效中 2已终止 3待续签, contract_type VARCHAR(32) DEFAULT 采购 COMMENT 合同类型采购/销售/租赁/其他, create_by INT NOT NULL COMMENT 创建人ID关联t_user, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT合同主表;这段脚本里有几个设计点值得说。合同编号contract_no设置了 UNIQUE 约束这是为了防止并发录入时产生重复编号——在 JavaWeb 项目里如果只靠应用层判断重复两台 Tomcat 同时插入就会破功数据库约束才是最后防线。status用 TINYINT 存数字状态而不是字符串省空间而且排序方便但代价是代码里必须写清楚状态字典我见过不少项目死在“0 到底是待审批还是已删除”这种歧义上。amount用 DECIMAL(12,2) 而不是 FLOAT 或 DOUBLE这是个血泪经验。浮点数算金额会有精度丢失1.1 加 2.2 等于 3.3000000000000003合同金额算错了是要背责任的。日期字段拆成sign_date、start_date、end_date三个是为了后面做到期提醒时可以直接用WHERE end_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 1 DAY)这种条件扫描不用在业务代码里做日期运算。2.3 为什么这套结构用 JSP Servlet 而不是 Spring Boot现在新建 JavaWeb 项目多数人会直接上 Spring Boot但这个合同管理系统用的是 JSP Servlet JDBC 的老三样。这不是技术落后而是这类项目有它的现实约束部署环境可能是客户内网里一台老服务器只装了 JDK 8 和 Tomcat 8.5直接扔一个 WAR 包就能跑不用配 Maven 私服、不用处理 Spring Boot 内嵌 Tomcat 和外部 Tomcat 的版本冲突。另外作为学习案例JSP Servlet 把 HTTP 请求到响应的完整链路暴露得清清楚楚请求进来先到 Servlet 的doPost调 Service 层Service 调 DAO 层访问 MySQL最后forward回 JSP 渲染。这套结构虽然代码量比 Spring Boot 大但每一步发生了什么都能在源码里找到排查问题不需要跳十几层封装。后面第三章我会完整演示怎么在 IDEA 里把它跑起来包含 JDK、Tomcat、MySQL 的版本搭配和初始化脚本导入。3. 在 IDEA 里把项目跑起来环境版本搭配与初始化步骤3.1 环境版本这一步错了后面全是坑跑起来之前先把版本对齐。我实际测试过这个项目在下面的组合下是最稳的JDK 1.8、Tomcat 8.5.x、MySQL 5.7 或 8.0、IDEA 2022 及以上版本。Tomcat 10 不建议用因为它把javax.servlet换成了jakarta.servlet而这个项目的代码里全是import javax.servlet.*直接部署会报ClassNotFoundException这是新手最容易掉的坑。如果机器上已经装了多个 JDK注意 IDEA 里要同时确认 Project Structure 和 Tomcat 的 JRE 路径是一致的。我见过的情况是Project SDK 选了 JDK 17Tomcat 配置里却指向 JRE 1.8启动时控制台报UnsupportedClassVersionError折腾半天才发现是两边版本没对齐。数据库方面MySQL 8.0 需要把驱动换成com.mysql.cj.jdbc.Driver5.7 则用com.mysql.jdbc.Driver项目里默认配的是 5.7装 8.0 的话需要改两处后面会说到。3.2 导入项目和数据库初始化先建库再跑代码打开 IDEA选择File - New - Project from Existing Sources选中项目根目录让 IDEA 识别成 Web 项目。导入后先别急着启动第一步是初始化数据库。用 Navicat 或命令行执行项目里的sql/contract.sql它会自动创建数据库contract_db并写入初始数据。mysql -u root -p sql/contract.sql执行成功后在 MySQL 里执行USE contract_db; SHOW TABLES;应该能看到五张表t_user、t_contract、t_contract_file、t_approval、t_contract_log。这一步是后面所有操作的基础——我见过有人跳过建表直接启动结果一访问登录页就报Table contract_db.t_user doesnt exist属于典型的顺序问题。如果表已经存在想重来先执行DROP DATABASE contract_db;再重新导入。数据库连接配置在src/main/resources/db.properties里默认内容如下jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/contract_db?useUnicodetruecharacterEncodingutf-8useSSLfalse jdbc.usernameroot jdbc.password123456注意useSSLfalse这个参数。MySQL 8.0 默认开启 SSL如果连接串不带这个参数控制台会刷一堆 SSL 警告虽然不影响运行但日志会很吵。另外characterEncodingutf-8一定要保留后面中文乱码那一节会专门讲。如果你是 MySQL 8.0把驱动改成com.mysql.cj.jdbc.Driver即可。3.3 配置 Tomcat 并启动两步就能出界面在 IDEA 里点Run - Edit Configurations新增一个 Tomcat Server - Local。在Deployment选项卡里点加号选择Artifact选中项目名的war exploded包Application context填/contract。这样访问路径就是http://localhost:8080/contract/。# 启动前确认8080端口没有被占用 netstat -ano | grep 8080启动 Tomcat 后浏览器访问http://localhost:8080/contract/看到登录页说明部署成功。系统预置了两个账号管理员admin / admin123普通员工user / user123。这里有个小细节项目里的登录过滤器把/login.jsp、/login和静态资源目录static/放行了其他路径都会被拦所以直接访问index.jsp会跳回登录页这是预期行为。3.4 验证环境正常一个典型的合同录入请求链路登录成功后我们追踪一次合同录入请求确认整个链路是通的。业务人员在页面上填合同表单点提交后请求发到ContractServlet?actionadd。Servlet 里先做参数校验再调ContractService.addContract()插入主表记录同时把附件信息写入t_contract_file表最后response.sendRedirect(list.jsp)跳回列表页。如果你在 Servlet 的doPost里打个断点能看到request.getParameter(contractName)拿到的值和页面表单里的 name 属性一一对应。这一步验证的核心是“页面 → Servlet → Service → DAO → 数据库”整条链路有没有断点。如果出现了 500点开 IDEA 控制台的异常堆栈定位到哪个类哪个方法抛的错如果列表页显示出来但数据是空的优先检查数据库连接串里contract_db库名是否写对——这种问题通常不是代码逻辑而是环境配置的锅。4. 业务代码走读合同 CRUD 与审批流转是怎么实现的4.1 用 Filter 把登录态管起来比手动判断省心十倍整个系统的权限控制核心不在 Servlet 里而在一个LoginFilter里。这个过滤器在web.xml中配置了拦截路径/*每次请求进来先走它下面是核心逻辑public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); String uri request.getRequestURI().replace(request.getContextPath(), ); // 放行登录页、登录接口和静态资源 if (/login.jsp.equals(uri) || /login.equals(uri) || uri.startsWith(/static/)) { chain.doFilter(request, response); return; } // 会话里没有 user说明未登录跳回登录页 if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(request, response); }为什么用 Filter 而不是在每个 Servlet 里写判断因为合同系统里现在有合同管理、审批管理、统计分析十几个 Servlet如果每个都写一套if (session.getAttribute(user) null)代码重复且容易漏。过滤器把横切逻辑抽出来新增页面时只要在web.xml里确认拦截路径就够不需要动业务代码。参数上注意request.getSession(false)的写法。传false表示如果没有会话就返回 null而不是新建一个。如果写成request.getSession()那么未登录用户访问任意页面都会被动创建一个 session攻击者可以用这个特性做会话固定攻击虽然在小系统里风险不大但养成好习惯没坏处。4.2 审批流转用状态字段而不是流程表这个系统的审批逻辑没有用 Activiti 那类工作流引擎而是靠t_contract表的status字段推动。法务登录后在待审批列表里点通过请求到达ApprovalServlet?actionpasscontractId23代码逻辑如下public void pass(HttpServletRequest request, HttpServletResponse response) { int contractId Integer.parseInt(request.getParameter(contractId)); Contract contract contractDao.findById(contractId); // 只有待审批状态才能通过防止重复审批 if (contract.getStatus() ! 0) { request.setAttribute(msg, 该合同已被处理请刷新列表); request.getRequestDispatcher(approval_list.jsp).forward(request, response); return; } contractDao.updateStatus(contractId, 1); // 0待审批 - 1生效中 ApprovalLog log new ApprovalLog(); log.setContractId(contractId); log.setOperatorId(((User) request.getSession().getAttribute(user)).getId()); log.setAction(pass); log.setRemark(request.getParameter(remark)); approvalDao.insert(log); response.sendRedirect(approval_list.jsp); }这段代码最值得关注的是状态校验if (contract.getStatus() ! 0)。它防止了两个法务同时打开审批列表、同时点同一个合同导致重复审批。数据库层面updateStatus的 SQL 是UPDATE t_contract SET status 1 WHERE contract_id ? AND status 0双重校验保证了并发时只有一条更新生效。这是合同管理系统里的典型并发边界如果不做这个保护会出现一条合同被审批两次、状态却还是生效中的脏数据。审批记录单独落到t_approval表而不是只更新主表状态。这样管理员在合同详情页能看到完整的审批轨迹谁在什么时间做了通过操作、备注了什么。合同管理系统的审计要求决定了这类操作记录不能省一旦合同出问题第一件事就是查审批日志。4.3 列表页分页与搜索SQL 里的 LIMIT 和 COUNT 要一起做合同列表页支持按名称模糊搜索和分页展示对应ContractServlet?actionlistkeyword采购page1。DAO 层的实现分两步第一步查符合条件的总条数第二步查当前页的数据// 第一步查总数用于计算总页数 String countSql SELECT COUNT(*) FROM t_contract WHERE contract_name LIKE ?; PreparedStatement ps conn.prepareStatement(countSql); ps.setString(1, % keyword %); ResultSet rs ps.executeQuery(); int total 0; if (rs.next()) { total rs.getInt(1); } // 第二步查当前页数据每页10条 String pageSql SELECT * FROM t_contract WHERE contract_name LIKE ? ORDER BY create_time DESC LIMIT ?, 10; ps conn.prepareStatement(pageSql); ps.setString(1, % keyword %); ps.setInt(2, (page - 1) * 10);注意这里用PreparedStatement拼接参数而不是字符串拼接 SQL。% keyword %是作为参数传进去的不是拼到 SQL 文本里这样能防 SQL 注入。如果你看到某段代码写的是SELECT * FROM t_contract WHERE contract_name LIKE % keyword %那最佳实践是立即改成预处理参数。分页参数(page - 1) * 10是 MySQL 的LIMIT偏移量计算方式第一页偏移 0第二页偏移 10第三页偏移 20。前端 JSP 页面里通过request.getAttribute(totalPages)渲染页码条点击页码时重新携带keyword和page参数请求。搜索和分页组合在一起时最容易翻车的点是把 keyword 丢了——点第二页发现搜索结果全没了。解决办法是翻页链接里同时带上keyword${param.keyword}这个细节在第六章还会提到。5. 避坑JavaWeb 合同系统部署与使用的五条排查记录5.1 MySQL 版本导致驱动类找不到现象启动项目后访问登录页控制台报ClassNotFoundException: com.mysql.jdbc.Driver。原因项目默认配置文件里写的是 MySQL 5.7 的驱动类com.mysql.jdbc.Driver但如果你装的是 MySQL 8.0驱动类已经改名为com.mysql.cj.jdbc.Driver旧名字在新驱动里被移除。解决打开db.properties把jdbc.driver改成com.mysql.cj.jdbc.Driver同时检查lib目录下的mysql-connector-javaJAR 包版本是不是 8.x。版本对应上之后重启 Tomcat 即可。判断装的是哪个版本在 MySQL 命令行执行SELECT VERSION();一眼就能看出来。5.2 中文乱码页面显示全是问号现象合同名称、甲方乙方在列表中显示成???但英文和数字正常。原因MySQL 连接串里没有characterEncodingutf-8参数或者数据库表不是 utf8 字符集。JSP 页面本身是 UTF-8但 JDBC 连接 MySQL 时如果没指定编码会用服务端默认的 latin1 传输中文字符到 Java 字符串时已经损坏。解决三步走。第一步确认db.properties的连接串带了useUnicodetruecharacterEncodingutf-8第二步执行SHOW CREATE TABLE t_contract;看表字符集如果不是 utf8mb4执行ALTER TABLE t_contract CONVERT TO CHARACTER SET utf8mb4;第三步检查 Tomcat 的server.xml里Connector是否配置了URIEncodingUTF-8没配的话 GET 请求的中文参数也会乱。这三步做完乱码基本能根除。5.3 上传附件后找不到文件现象合同附件上传成功提示“已上传”但在服务器磁盘上找不到对应文件。原因项目的上传路径写的是相对路径upload/这个相对路径是相对于 Tomcat 进程的工作目录也就是tomcat/bin目录不是项目部署目录。文件被写到了tomcat/bin/upload/下面当然找不到。解决打开FileUploadServlet把保存路径改成绝对路径比如D:/contract_upload/或者在启动参数里指定。比较稳妥的做法是在类里加一个配置项// 在 web.xml 或配置文件中定义上传根目录 private static final String UPLOAD_DIR System.getProperty(user.dir) File.separator contract_upload;同时注意下载附件时需要把存储路径同步到t_contract_file.file_path字段否则附件列表里点击下载会 404。这类问题在本地开发时不容易暴露因为 IDEA 的工作目录刚好在项目根下部署到独立 Tomcat 时就翻车了。5.4 Tomcat 端口被占用启动直接失败现象启动 Tomcat 时控制台报Port 8080 is already in use项目起不来。原因本机已经有别的进程占了 8080 端口或者上一个 Tomcat 没关干净。解决先找到占用端口的进程netstat -ano | findstr 8080拿到 PID 后在任务管理器里结束进程。如果不想关掉现有服务也可以在 IDEA 的 Tomcat 配置里把 HTTP port 改成 8081同时在 Deploymen 里确认访问路径访问http://localhost:8081/contract/即可。改端口后要注意如果项目代码里有写死的绝对路径http://localhost:8080也需要同步改否则页面跳转会 404。5.5 登录成功后跳回登录页形成死循环现象输入正确的账号密码点登录页面闪了一下又回到登录页。原因登录逻辑里session.setAttribute(user, user)没有执行成功或者执行了但 Filter 里拿到的 session 不是同一个。常见原因是有两个 Tomcat 在跑IDEA 用的是 8081 端口浏览器访问的却是 8080 端口那个旧项目两边的 session 不互通。解决确认浏览器访问的端口和 IDEA 启动的 Tomcat 端口一致。系统里如果有多个 Tomcat 服务先全部停掉只保留一个。然后在LoginServlet里打日志确认登录成功后打印session.getId()在 Filter 里也打印session.getId()两个 ID 不一致就说明 session 不是同一个重点查 Tomcat 的 context 路径配置。6. 进阶合同 PDF 打印与到期自动提醒两个实用改造系统跑通之后有两个高频需求值得优先改合同详情页打印成 PDF、到期合同自动提醒。这两个功能直接影响业务人员的使用体验也是这类管理系统常用的扩展点。PDF 打印不一定要后端生成合同详情页本质是 HTML直接用浏览器的打印能力就能出 PDF。在contract_detail.jsp底部加一个按钮调用window.print()配合一段只对打印生效的 CSSmedia print { .no-print { display: none; } body { font-size: 12px; } }把页面上操作按钮、导航栏都加上no-print类打印时就只会输出合同主体内容业务人员用 Chrome 的“另存为 PDF”就能导出。如果需要带骑缝章或页眉页脚再考虑后端用 OpenPDF 生成真正的 PDF 文件但多数内部场景不需要浏览器打印方案零依赖、见效快。到期提醒有两个方案。一是最省事的登录后扫描管理员登录时系统自动执行一条 SQL查出三天内到期的合同在列表顶部显示红色提醒条。另一种是定时任务用Scheduled或Timer每天凌晨跑一次把到期合同生成待办写入数据库下一次登录时展示。内网小系统用登录扫描就够了只有合同数量大、需要主动推送时才上定时任务。做定时任务时不要写while(true) { Thread.sleep(86400000) }这种线程裸奔的方案Tomcat 重载时线程会丢失重启后提醒就断了。我后来改造成用ScheduledExecutorService配合启动时初始化这才稳定下来。从那以后我每次做合同系统都会在改造说明里写一条提醒功能必须跟着应用生命周期走别用裸线程。希望帮到你。本文还有配套的精品资源点击获取