拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JSP科研成果申报管理系统:从数据库设计到部署避坑实战

JSP科研成果申报管理系统:从数据库设计到部署避坑实战

简介:这是一份科研成果申报管理系统的设计与实现分析文档,采用JSP+JavaBean架构并结合SQL Server 2000数据库,适合高校计算机专业毕业设计及科研管理平台开发者参考。文档从系统开发背景与设计目标入手,依次介绍了技术选型、系统建模、可行性分析和需求分析,划分出申报管理、审批管理、用户管理等核心功能模块;数据库设计部分给出了数据流图、实体关系图与数据表结构定义,保障数据的一致性和完整性。系统实现部分分析了JavaBean关键组件,以及主界面、科研申报、登录、课题申报、个人信息修改、审批、查询等模块的运行效果和核心源代码。资源为单个doc文件,大小516KB,已有62人学习,可作为毕业设计论文参考和系统开发蓝本。

1. 为什么到现在还有人用 JSP 做科研成果申报系统:先把业务账算清楚

JSP(Java Server Pages)做科研成果申报管理系统,听起来像是上一个 Web 时代的组合,但在高校和科研院所的毕设选题池里,它依然高频出现。这个标题背后是一个很实际的诉求:用 JSP 把"申报—审核—统计"这一整条业务闭环落地,从数据库设计到页面渲染,每一步怎么选、怎么实现、坑在哪。适合两类人:准备拿它做毕设或课设的学生,以及想快速给单位搭一个内网管理系统的初级开发。读完你会得到一套最小可运行的 JSP 项目骨架,以及比骨架更值钱的部署细节、并发审核边界和典型翻车点。

2. 科研成果申报管理系统到底在管什么:从业务流程到数据模型

做系统之前先把业务账算清楚,很多毕设翻车不是代码写不出来,是表设计不对、流程没理顺。科研成果申报这个场景,放在任何一所高校或科研院所,流程都惊人地相似:教师填报成果,科研秘书初审,分管领导终审,年底再按成果类型做统计。想清楚这三段流程,数据表和页面结构就都跟着出来了。

2.1 申报、审核、统计的三段式流程:状态值怎么设计

整个系统的核心不是页面做得多好看,而是一条状态主线。每个申报单从创建到入库,要经过这么几个状态:

  • 0:草稿,教师保存了还没提交,本人可见
  • 1:已提交待初审,科研秘书进入审核列表
  • 2:初审通过待终审,分管领导进入终审列表
  • 3:终审通过(入库),可以参与年度统计
  • -1:初审驳回,教师修改后可再次提交
  • -2:终审驳回,教师修改后可再次提交

状态用数字而不是字符串,是因为数字在数据库里占 1 个字节、索引效率高,代码里用常量类统一管理,不会出现"已通过""审核通过"" approved "这种同一含义三种写法的问题。状态只走这一条主线,不建议再加"撤回""转交"之类分支动作——流程每多一个分支,前端菜单、权限判断、SQL 条件都会跟着复杂一截,对毕设来说性价比太低。如果导师坚持要"撤回"功能,也简单:撤回本质是把状态从 1 改回 0,前提是审核人还没点过通过按钮,SQL 里带上 status = 1 的 WHERE 条件就行,这个细节在第 4 章审核流里会再讲。

统计这一段依赖的是状态为 3 的数据。注意统计口径要和状态绑定,不能看 create_time,而要看 submit_time,因为有的教师会把一份申报单当草稿存两个月,提交时间才是真正的"申报年度"。

2.2 数据表设计:申报表、成果表、审核记录的字段取舍

四张表能覆盖所有需求:用户表、申报主表、成果明细表、审核日志表。下面是可直接执行的建表 SQL,字符集统一用 utf8mb4,避免以后存入生僻字或 Emoji 时出现乱码。

CREATE TABLE t_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, real_name VARCHAR(50) NOT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT '1-教师 2-科研秘书 3-分管领导 4-管理员', dept VARCHAR(100) DEFAULT '', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_declaration ( decl_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(200) NOT NULL, category VARCHAR(50) NOT NULL COMMENT '论文/专利/获奖/课题', abstract_txt TEXT, status TINYINT NOT NULL DEFAULT 0 COMMENT '0-草稿 1-已提交 2-初审通过 3-终审通过 -1初审驳回 -2终审驳回', submit_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_status (status), KEY idx_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_achievement ( ach_id INT PRIMARY KEY AUTO_INCREMENT, decl_id INT NOT NULL, journal_name VARCHAR(200), publish_date DATE, authors VARCHAR(255), attachment_path VARCHAR(255), score DECIMAL(5,2) DEFAULT 0 COMMENT '用于年度工作量结算', KEY idx_decl (decl_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_audit_log ( log_id INT PRIMARY KEY AUTO_INCREMENT, decl_id INT NOT NULL, auditor_id INT NOT NULL, action VARCHAR(20) NOT NULL COMMENT 'submit/approve/reject', comment_text VARCHAR(500), audit_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_decl (decl_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段取舍上最容易出问题的是三处。第一,t_declaration 的 status 为什么不用字符串 varchar,上面已经解释了,数字状态配合代码里的 StatusConstant 类最省心。第二,t_audit_log 为什么要单独开表,而不是在申报主表里放一个 audit_comment 和一个 audit_time:因为一份申报单可能被驳回、修改、再提交、再审核,主表只放一个意见字段会被下一次审核覆盖掉,历史记录全丢;单独一张日志表才能追溯"谁在什么时候做了什么决定",答辩时这是很好的加分点。第三,附件不直接存数据库,数据库只存 attachment_path 路径,文件落本地磁盘或对象存储,否则数据库体积会迅速膨胀,备份和迁移都痛苦。

2.3 角色权限:教师、科研秘书、分管领导各自看到什么

权限模型不搞复杂的 RBAC 框架,三种角色加一个管理员,足够承载全部需求。权限矩阵如下:

功能教师科研秘书分管领导管理员
新建/修改申报单能不能不能视同教师
提交申报单能不能不能不能
初审(状态 1->2 或 -1)不能能不能不能
终审(状态 2->3 或 -2)不能不能能不能
查看统计报表只能看自己的全院/全所全院/全所全量
用户管理不能不能不能能

实现上不引入 Spring Security 这类重武器,用 Servlet Filter 按 URL 前缀过滤就够了。约定 /decl/* 开头需要登录,/audit/* 开头只允许角色 2 和 3,/admin/* 只允许角色 4。页面菜单用 JSTL 判断当前登录用户的 role 字段渲染,教师登录后根本看不到"审核列表"这个入口。这种写法的好处是过滤器一眼能看懂,坏处是 URL 约定一旦被绕过就会越权,所以过滤器里要同时校验 session 中的 user 对象是否为空、角色是否匹配,两个条件缺一不可。

3. 搭建一个能跑的最小 JSP 项目:环境选型与 IDEA 创建参数

这一章聊的是 jsp 入门的关键一步:环境怎么选、项目怎么建。很多新手在 IDEA 里新建项目时纠结选 Jakarta EE 还是 Maven,其实方向错了。你现在要的不是最新最炫的框架,而是一个能稳定跑完答辩、代码在网上搜得到参考的经典组合。

3.1 环境选型:JDK 8 + Tomcat 8.5 + Servlet 3.1 的兼容性理由

先说版本结论,这是血泪经验换来的:JDK 8 + Tomcat 8.5 + IntelliJ IDEA 2021 或更早的版本,是跑传统 JSP 项目最稳的组合。原因有三层。

第一,JDK 8 直到今天仍然是大量高校机房的默认版本,你写代码用的语法特性(Lambda、Stream)在 JDK 8 里都支持,但到了 JDK 17 或 21 反而要适配模块化,纯属给自己加戏。第二,Tomcat 8.5 支持 Servlet 3.1,请求默认就用 UTF-8 解码,相比 Tomcat 7 少踩一个 GET 请求中文乱码的坑,这对管理系统来说是刚需。第三,Servlet 3.1 支持 @WebServlet 注解,不需要在 web.xml 里给每个接口写一大段<servlet-mapping>,代码整洁程度高一个档次。

不建议用 Tomcat 10 的原因更实际:它把包名从 javax.servlet 迁移到了 jakarta.servlet,而你网上搜到的绝大多数 JSP 教程、CSDN 示例、学长学姐留下的项目代码,全是 javax 包名。照着抄会直接编译报错,对毕设来说这是纯时间成本。等做完了核心功能想升级再升级,不要一开始就挑战生态不兼容的版本。

3.2 在 IDEA 里新建 JSP 项目:Maven 骨架与 pom.xml 关键依赖

IDEA 新建 jsp 项目,我推荐走 Maven Archetype 而不是 New Project 里的 Jakarta EE 模板,原因是 Maven 骨架自带 pom.xml 和打包插件,后续传统 JSP 项目打包 war 直接mvn package就完事。操作步骤是:File → New → Project → Maven Archetype,选择 maven-archetype-webapp,填好 groupId 和 artifactId,然后手动补出 src/main/java 和 src/main/resources 两个目录。

pom.xml 的关键依赖如下,直接抄这份配置,逐条解释在后面。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>scientific-research-decl</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> </dependencies> <build> <finalName>scientific-research-decl</finalName> </build> </project>

逻辑说明:packaging必须是 war,这样构建产物才能被 Tomcat 直接识别。javax.servlet-api的 scope 设置成provided很关键,因为 Tomcat 自带了 Servlet 容器实现,如果打成 jar 包塞进 WEB-INF/lib,运行时反而会出现类冲突或方法签名不一致。jstl1.2 一个 jar 就同时包含 API 和实现,不需要再引 standard.jar。mysql-connector-java用 5.1.49 是因为它兼容 MySQL 5.7 和 8.0,写 JDBC URL 时不需要额外加时区参数。finalName的值决定最终 WAR 文件名,也就是部署后的上下文路径,起一个有意义的名字后面排查 404 时能少点头疼。

web.xml 在 src/main/webapp/WEB-INF 下,最小配置只要声明一个 welcome-file 列表,让访问根路径时自动跳转到首页。前提是目录结构完整,标准 Maven Web 项目结构是这样的:

src/main/java # Java 源码 src/main/resources # 配置文件 src/main/webapp # JSP、静态资源、WEB-INF

3.3 JSP + Servlet + JavaBean 三层写法:从登录接口看请求链路

JSP 项目的经典三层分工是:JSP 只负责渲染页面,Servlet 只做请求控制,JavaBean/DAO 只碰数据库。最常见的反模式是在 JSP 里直接写<% %>连数据库查数据,那种代码当时能跑,加一个页面就乱成一坨,答辩老师看到也会皱眉。

用一个登录接口串起请求链路。先写 JavaBean,只存放数据不写业务逻辑:

public class User { private int userId; private String username; private String password; private String realName; private int role; private String dept; public int getRole() { return role; } public void setRole(int role) { this.role = role; } // 其余字段的 getter/setter 省略,IDEA 自动生成即可 }

再写 LoginServlet,注意第一行代码必须是设置请求字符集,这一行放错位置乱码就出现了:

@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); UserDao dao = new UserDao(); User user = dao.findByUsernameAndPassword(username, password); if (user != null) { request.getSession().setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }

逻辑说明:登录成功后用sendRedirect返回 302 让浏览器重新发起请求,然后由 index.jsp 渲染首页;登录失败用forward在服务端内部跳转回登录页,同时把错误信息放进 request 作用域。这两个跳转方式的区别值得在答辩时讲清楚:sendRedirect 会丢失 request 属性,所以错误信息必须用 forward 传;而 index.jsp 要从 session 里取用户信息,所以登录成功后把 user 放进 session。

最后是 login.jsp 的表单主体,form 的 action 指向 Servlet 的映射路径,method 必须为 post:

<form action="${pageContext.request.contextPath}/login" method="post"> <input type="text" name="username" placeholder="用户名"/> <input type="password" name="password" placeholder="密码"/> <button type="submit">登录</button> </form> <c:if test="${not empty errorMsg}"> <p style="color:red">${errorMsg}</p> </c:if>

这里的${pageContext.request.contextPath}是 JSP EL 表达式,用来动态获取项目上下文路径,这样无论部署后叫 scientific-research-decl 还是 ROOT,表单都不需要改。整套流程走下来,"用户点击登录 → Tomcat 解析参数 → Servlet 调 DAO → 查库 → 回写 session → 页面跳转"这条链路就完整了。

4. 核心模块设计实现:申报表单、审核事务与统计报表

系统能不能用,就看申报、审核、统计这三个模块。前一章把骨架搭好,这一章把肉填上。

4.1 申报表单:文件上传与 multipart 表单解析

科研成果申报里,论文的 PDF、专利的扫描件都要作为附件上传。JSP 页面里表单只要加了enctype="multipart/form-data",普通字段的获取方式就完全变了,这是新手最容易困惑的点。

<form action="${pageContext.request.contextPath}/decl/submitDecl" method="post" enctype="multipart/form-data"> <input type="text" name="title" placeholder="成果标题"/> <input type="text" name="category" placeholder="成果类别"/> <input type="file" name="attachment"/> <button type="submit">提交申报</button> </form>

对应 Servlet 里的解析代码:

@WebServlet("/decl/submitDecl") public class SubmitDeclServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); if (!ServletFileUpload.isMultipartContent(request)) { response.sendError(400, "请使用 multipart/form-data 编码提交"); return; } DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); // 超过 1MB 写入临时文件 ServletFileUpload upload = new ServletFileUpload(factory); upload.setFileSizeMax(10 * 1024 * 1024); // 单文件最大 10MB try { List<FileItem> items = upload.parseRequest(request); String title = null; String category = null; FileItem attachItem = null; for (FileItem item : items) { if (item.isFormField()) { if ("title".equals(item.getFieldName())) { title = item.getString("UTF-8"); } else if ("category".equals(item.getFieldName())) { category = item.getString("UTF-8"); } } else { attachItem = item; } } String saveDir = "/data/decl_attachments"; File dir = new File(saveDir); if (!dir.exists()) dir.mkdirs(); String fileName = UUID.randomUUID().toString() + "_" + attachItem.getName(); attachItem.write(new File(dir, fileName)); // 组装 Declaration 对象并插入数据库,此处省略 DAO 调用 } catch (Exception e) { throw new ServletException("文件上传解析失败", e); } } }

逻辑说明:表单一旦声明了 multipart,request.getParameter("title")就一定返回 null,因为 multipart 的请求体是二进制流格式,Tomcat 不会主动把字段解析进 parameterMap。必须通过upload.parseRequest(request)拿回 FileItem 列表,再逐个判断isFormField()区分普通字段和文件字段。item.getString("UTF-8")解决的是 multipart 表单里中文字段的乱码问题;文件名前拼 UUID 是为了防止两个教师传了同名文件互相覆盖;setSizeThreshold和setFileSizeMax是文件上传必须调的两个参数,前者控制内存阈值,后者控制文件总大小上限,生产环境里还要把临时目录单独配置。

4.2 审核流:用状态字段加事务边界防并发翻车

审核动作本质上就是一句话:把状态从当前值改成下一个值,同时记录一条日志。听着简单,但并发环境下很容易翻车。两个审核员同时打开同一份申报单,一个点通过一个点驳回,没有控制的话最终状态完全取决于谁后提交,这就出事故了。

解决办法是条件更新加事务。下面是审核方法的核心骨架:

public boolean audit(int declId, int auditorId, String action, String comment) { Connection conn = null; PreparedStatement psUpdate = null; PreparedStatement psLog = null; try { conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); // 条件更新:只允许从 expectedStatus 变到 nextStatus int expectedStatus = "approve".equals(action) ? 1 : 2; // 初审通过 1->2 int nextStatus = "approve".equals(action) ? 2 : -1; String updateSql = "UPDATE t_declaration SET status=?, audit_comment=?, audit_time=NOW() " + "WHERE decl_id=? AND status=?"; psUpdate = conn.prepareStatement(updateSql); psUpdate.setInt(1, nextStatus); psUpdate.setString(2, comment); psUpdate.setInt(3, declId); psUpdate.setInt(4, expectedStatus); int rows = psUpdate.executeUpdate(); if (rows == 0) { conn.rollback(); return false; // 说明状态已被别人改过,本次审核作废 } // 插入审核日志 String logSql = "INSERT INTO t_audit_log(decl_id, auditor_id, action, comment_text) VALUES(?,?,?,?)"; psLog = conn.prepareStatement(logSql); psLog.setInt(1, declId); psLog.setInt(2, auditorId); psLog.setString(3, action); psLog.setString(4, comment); psLog.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException("审核失败", e); } finally { JdbcUtil.close(psUpdate, psLog, conn); } }

逻辑说明:UPDATE ... WHERE decl_id=? AND status=?这行是关键,它保证只有当前状态等于预期状态时才会更新,executeUpdate()返回值是 0 说明已有其他人改过状态,直接回滚并返回失败。conn.setAutoCommit(false)解决的是主表状态更新和日志插入的一致性问题——如果只更新状态不插日志,一旦出问题无法追溯;如果先插日志后更新状态、更新又失败,日志就成了"谎话"。commit 之前发生任何异常都 rollback,两条 SQL 要么都成功要么都失败,这就是事务边界的意义。JdbcUtil.close的核心是把 Connection、Statement、ResultSet 按逆序全部关闭,具体写法在第五章避坑里再展开。

4.3 统计报表:把 SQL 聚合结果交给 JSTL 渲染

统计报表的价值在于让领导一眼看到"今年全院发了几篇论文、几项专利,总工作量多少"。报表数据尽量在 SQL 层算好,不要在 JSP 里写循环累加,那样页面卡顿且代码难看。给一个按年度和类别统计的查询:

SELECT d.category AS 成果类别, COUNT(*) AS 申报数量, SUM(CASE WHEN d.status >= 3 THEN 1 ELSE 0 END) AS 通过数量, SUM(CASE WHEN d.status >= 3 THEN a.score ELSE 0 END) AS 总工作量 FROM t_declaration d LEFT JOIN t_achievement a ON d.decl_id = a.decl_id WHERE YEAR(d.submit_time) = ? GROUP BY d.category;

统计 Servlet 里用 PreparedStatement 传入年份参数,把查询结果封装成 List<Map<String, Object>>,存进 request 后转发到 report.jsp。JSP 页面的渲染代码很短,核心就是 JSTL 的 forEach:

<table border="1"> <tr> <th>成果类别</th> <th>申报数量</th> <th>通过数量</th> <th>总工作量</th> </tr> <c:forEach items="${reportList}" var="row"> <tr> <td>${row['成果类别']}</td> <td>${row['申报数量']}</td> <td>${row['通过数量']}</td> <td>${row['总工作量']}</td> </tr> </c:forEach> </table>

这里用 LEFT JOIN 而不是 INNER JOIN 是有讲究的:有的申报单状态是"已提交"还没走到成果明细录入,如果用 INNER JOIN,这些申报单会直接从统计表里消失,数量就对不上。WHERE YEAR(submit_time) = ?让统计维度跟着提交年度走,不是跟着创建时间走,这个细节和第二章的状态设计是呼应的。

5. 常见问题与避坑:JSP 项目从开发到部署的 5 个典型坑

这一章写的是从开发到上线的过程中我实际踩过的坑,每一条都是"现象 → 原因 → 解决"的完整链路,直接照着排查能省一半时间。

5.1 中文乱码:POST 与 GET 的两套解法

现象:表单里输入"张三",数据库存进去变成"寮犱笁",页面上显示"??? ",这几乎是 JSP 入门必遇的鬼故事。

原因:HTTP 请求携带的参数是字节流,Tomcat 用什么字符集解码决定了你能不能还原出中文。POST 请求和 GET 请求的解码路径不同,所以解法分两套。POST 请求要靠业务代码在读取参数前设置字符集;GET 请求的参数在 URL 里,由 Tomcat 的 connector 统一解码。

解决:POST 请求在 Servlet 的 doPost 第一行写上request.setCharacterEncoding("UTF-8"),并且保证这行代码在第一次调用getParameter()之前执行,否则已经解析过的参数不会再重新解码。GET 请求在 Tomcat 8.5 里默认就是 UTF-8,基本不用管;如果你用的 Tomcat 7 或更早,打开 conf/server.xml,在<Connector>标签里加上URIEncoding="UTF-8"。响应端别忘了response.setContentType("text/html;charset=UTF-8"),否则浏览器不知道页面是什么编码。三处都设对,中文才能全程无恙。

5.2 文件上传时 request.getParameter() 返回 null

现象:表单加了enctype="multipart/form-data"后,原来的request.getParameter("title")突然全是 null,代码没动过,玄学问题。

原因:不是玄学。multipart 是一种复合请求体,把普通字段和文件二进制混在一个流里,Tomcat 默认不会解析这种格式,所以 request 的 parameterMap 为空。这是 Servlet 规范的既定行为,不是 bug。

解决:按 4.1 节的代码走ServletFileUpload.parseRequest(),从 FileItem 列表里取字段。注意item.getString("UTF-8")是必要的,否则取出来的中文标题照样乱码。另外不要自己去拼接解析 multipart 边界字符串,那是个大坑,边界值(boundary)是随机生成的,手写解析器会把文件内容截错,直接用 commons-fileupload 这套成熟方案。

5.3 数据库连接用完不关导致的连接耗尽

现象:系统跑一两个小时,日志里出现Cannot get connection: wait millis 60000, active 20, maxActive 20,数据库连接数被占满,系统假死。

原因:代码里获取了 Connection 但没在 finally 里关闭。有的同学只关了 Connection,PreparedStatement 和 ResultSet 没关,同样造成连接泄漏。Druid 连接池的 maxActive 是 20,连接被借走不还,池子就空了。

解决:写一个统一的 JDBC 工具类,关闭操作按逆序执行,并在 finally 里调用。

public static void close(ResultSet rs, Statement stmt, Connection conn) { 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(); } } }

逻辑说明:ResultSet 依赖 Statement,Statement 依赖 Connection,关闭顺序必须从内到外,否则先关 Connection 再关 Statement,内部的游标可能未被释放。另外 Druid 的初始化参数里,setInitialSize(5)、setMinIdle(2)、setMaxActive(20)这三个值要配套,maxActive 不要一下子设成 200,内网管理系统 20 足够,设太大反而拖垮数据库。

5.4 IDEA 里跑得好好的,部署到 Tomcat 就 404

现象:IDEA 里配置了本地 Tomcat,点运行按钮打开浏览器能正常访问首页;把项目打成 WAR 包丢到 Tomcat 的 webapps 目录,访问却 404。

原因:两种运行方式的环境不一样。IDEA 运行时用的是它自己创建的 Artifact,部署到了 Tomcat 的conf/Catalina/localhost下一个临时路径,上下文名是 IDEA 给你起的;直接丢 WAR 到 webapps,Tomcat 解压后生成的目录名是 WAR 文件名,如果你访问的路径是/scientific-research-decl/,而 WAR 名叫demo.war,自然匹配不上。另一个常见原因是 WAR 包结构不对,缺 WEB-INF/classes 下的编译产物,或者缺 WEB-INF/lib 下的依赖 jar。

解决:第一步,确认访问路径和 WAR 文件名一致,最简单方法是在 webapps 下看解压出来的目录名。第二步,解压 WAR 看一眼结构:

jar tf scientific-research-decl.war

输出里必须有 WEB-INF/classes 下的 .class 文件,以及 WEB-INF/lib 下的 mysql、druid、jstl 等 jar 包。如果 classes 为空,说明 Maven 编译输出目录没配好;如果 lib 为空,说明依赖 scope 或打包插件配置错了。排查完这两点,90% 的 404 都能解决,剩下 10% 去看 Tomcat 的日志文件logs/catalina.out或logs/localhost.2025-xx-xx.log,那里有真正的报错堆栈。

5.5 传统 JSP 项目打包 WAR:构建配置与 nginx 转发 Tomcat 的注意点

现象:执行mvn clean package成功后,部署到 Tomcat 报ClassNotFoundException: com.mysql.jdbc.Driver。

原因:Maven 默认的 war 打包不会把 provided scope 的依赖打进去,这本身是对的;问题往往出在你把 mysql-connector-java、druid、jstl 这些运行时依赖写成了 provided,或者根本没引入。传统 JSP 项目打包 war 时,只有 javax.servlet-api 需要 provided,其他依赖都必须以 compile 或 runtime scope 进入 WEB-INF/lib。

解决:回到 pom.xml 确认依赖 scope。servlet-api 保持 provided,mysql-connector-java、jstl、commons-fileupload、druid 全部不要写 scope,默认 compile 即可。然后重新打包,检查 WEB-INF/lib。

部署上线后还有一个高频问题:项目跑在 8080 端口,但领导只想通过 80 端口访问,这时候就会想到"nginx 支持 jsp 吗"。答案是 nginx 本身不解析 JSP,它只处理静态文件和转发请求,必须把动态请求转到 Tomcat 去执行。最小配置如下:

server { listen 80; server_name decl.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

这里的proxy_pass就是把请求原样转交给监听 8080 的 Tomcat,JSP 文件最终是由 Tomcat 的 JSP 引擎编译执行的。对内网管理系统,整个/全转发就够了;如果以后要拆静态资源,再单独配置/static/路径的 alias 指向本地目录。注意修改 nginx 配置后要执行nginx -s reload而不是 restart,reload 是无损重载。

6. 上线前的验证:状态迁移自检、接口测试与部署后排查

系统写完不叫完事,状态机跑得通、数据能对上才算。我习惯在上线前做一轮最朴素的验证,不用自动化测试框架,就用 curl 和 SQL,十分钟能把核心链路全过一遍。

6.1 用 curl 走一遍核心接口流程

先启动 Tomcat,然后用 curl 带 cookie jar 模拟登录会话:

# 第一次访问首页,预期收到 200 和登录页 HTML curl -i http://localhost:8080/scientific-research-decl/ # 登录:保存 cookie 到文件,预期收到 302 跳转 curl -i -c cookies.txt -d "username=teacher1&password=123456" \ http://localhost:8080/scientific-research-decl/login # 带着 cookie 访问教师申报列表页,预期 200 curl -b cookies.txt http://localhost:8080/scientific-research-decl/decl/declList.jsp

-c cookies.txt是把服务端返回的 Set-Cookie 存下来,-b cookies.txt是让后续请求带上这个 cookie。看到 302 说明登录跳转正常,看到 200 说明会话认证生效。如果返回 401 或 403,先检查 Filter 里的 session 取值和角色判断逻辑。

6.2 数据库侧的状态迁移自检与日志排查

接口通了只能说明请求链路正常,状态迁移对不对要看数据库。用两条 SQL 就能完成自检:

-- 查某份申报单的完整状态轨迹 SELECT decl_id, status, submit_time FROM t_declaration WHERE decl_id = 1; -- 查这张单子的所有审核记录 SELECT action, comment_text, audit_time FROM t_audit_log WHERE decl_id = 1 ORDER BY audit_time;

正确的结果是:申报单状态从 0 变 1、再变 2、再变 3,同时 t_audit_log 里有三条对应记录——submit、approve、approve。如果状态是 3 但日志只有两条,说明事务没包住,赶紧回 4.2 节检查 setAutoCommit 和 try-catch-finally。我个人的习惯是每次改完状态机代码,都手动把"草稿 → 提交 → 初审通过 → 终审通过 → 驳回 → 重新提交"完整跑一遍,每一步都查数据库,因为状态字段改起来太容易了,日志漏了才难看。这套流程在答辩演示时也很有说服力——直接给评委看数据变化,比空口讲设计好得多。希望今天的这些细节能让你少走几段弯路,祝你的系统一次跑通。

本文还有配套的精品资源,点击获取

返回列表