☰
Java Web简历管理系统:Servlet+DAO分层架构与招聘状态流转实战
2026/9/28 2:02:28 网站建设 项目流程

简介:面向毕业设计或课程设计场景的Java Web简历管理系统项目包,采用JSP、Servlet、Service层与Dao层分层设计,并以MySQL作为数据存储,构成一个可运行的简历管理平台。整套资源共162个文件,压缩包大小约2.75MB,其中包含55个Java源程序、61个编译生成的Class字节码、17个JSP页面模板、10个第三方依赖库文件,以及2个数据库建表脚本,另附工程导入所需的配置信息,方便在集成开发环境中直接部署。目前已有301人参与学习与下载。系统实现了用户注册与登录、简历的创建编辑与删除、简历信息的浏览查看、关键词搜索与条件筛选,以及普通用户和管理员的不同权限控制等功能;代码结构按控制层、业务层、数据访问层、视图层清晰划分,能够帮助学习者理解MVC模式在真实项目中的应用方式。项目对简历所含教育背景、工作经历、技能特长等字段进行了结构化设计,可直观感受从前端表单提交到数据库落库的完整过程。借助源码还可以掌握Servlet请求处理流程、JSP页面渲染、DAO封装、SQL表设计等核心知识点,适合作为Java Web开发能力的综合训练项目。

1. 这套企业简历管理系统的真正价值在排期与评分闭环

这套项目的价值点和大部分人的第一反应不一样:登录注册、简历增删改查只是外壳,真正体现设计功力的是“排期 + 笔试/面试成绩录入 + 状态流转”这条招聘闭环。从类名就能看出来——RMSResumeBrowsingServlet 管简历浏览、RMSPaperExamEnteringServlet 管笔试成绩录入、RMSInterviewEnteringServlet 管面试成绩录入、RMSResumeSchedulingServlet 管考核排期,四条链路串起来的是一份简历从投递到录用或淘汰的完整生命周期。对准备 Java Web 课程设计、毕业设计答辩的人来说,这套架构比单纯做个 CRUD 系统耐问得多。适合三类人:急需 Java Web 完整项目做课设的学生、想搞懂 Servlet + Service + DAO 分层实战的初学者、以及想快速改造出招聘流程演示系统的开发者。

2. 项目架构与数据模型:先看懂分层,再谈复现

2.1 三层架构拆解:Servlet 到底在替谁干活

先说结论:这个系统是典型的 JSP + Servlet + Service + DAO 手工分层,没用 Spring,也没用 MyBatis。对课程设计来说这反而是优点——答辩时考官问“请求是怎么走的”,你能从浏览器一路讲到数据库,每一层都拿得出代码。硬套 SSM 反而容易把自己绕进去。

我按名称把 Servlet 的责任范围理了一下,大致是这样:

Servlet 类名对应的 urlPatterns(常见映射)职责
RMSInitServlet/init初始化公共数据,例如基础配置、字典项加载到 application 作用域
RMSNavigationServlet/navigation统一页面跳转,根据参数转发到对应 JSP
RMSResumeBrowsingServlet/resumeBrowser简历分页列表、按条件搜索
RMSResumeSchedulingServlet/resumeSchedule给应聘者安排笔试/面试轮次,修改状态
RMSPaperExamEnteringServlet/paperExamEntering录入笔试成绩
RMSInterviewEnteringServlet/interviewEntering录入面试成绩,并推进招聘状态

这里的请求链路是固定的:浏览器请求 → web.xml 或 @WebServlet 注解匹配 Servlet → Servlet 调用 Service → Service 调 DAO → DAO 用 JDBC 访问 MySQL → 结果一层层返回,最后 Servlet 用 request.setAttribute 把数据塞给 JSP,再 forward 过去。

代码走查时我建议你重点看 RMSInitServlet 和 RMSNavigationServlet 的初始化逻辑,它们决定了系统启动后能不能正确跳转。在实际项目中,我会在 init 方法里读数据库配置放到 application 里,避免每个 Servlet 重复读取连接参数。这套代码如果也这么做,你找数据库连接配置时先看它的 init 段:

public void init() throws ServletException { // 读取数据库连接和系统参数,放入application作用域 String dbUrl = getServletContext().getInitParameter("jdbcUrl"); String dbUser = getServletContext().getInitParameter("dbUser"); String dbPassword = getServletContext().getInitParameter("dbPassword"); getServletContext().setAttribute("dbUrl", dbUrl); getServletContext().setAttribute("dbUser", dbUser); getServletContext().setAttribute("dbPassword", dbPassword); }

这里我说明一下:把连接信息放 application 作用域是课程项目的常见做法,能让你在一个地方改配置、全局生效。但生产环境绝不要这样干,密码应该放在加密配置中心或环境变量里,这是我需要提醒你的边界。

判断一个 Java Web 项目值不值得下载,我的习惯是先看它有没有“业务深度”。纯 CRUD 的课程项目到处都是,但带多状态流转、多轮考核评分这种业务逻辑的,才值得你花时间拆。这套系统明显属于后者。

2.2 数据模型梳理:排期、轮次和成绩表的关系

接下来是数据库。这套包里的 SQL 脚本和 DAO 类名已经透露了表结构方向——有应聘者表、考核轮次表、成绩表、用户表。我先给你一个按常见实现还原出的建表参照,你导入 SQL 后可以对照检查字段:

-- 应聘者主表:一份简历的核心信息 CREATE TABLE rms_applicant ( applicant_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '姓名', gender VARCHAR(10) COMMENT '性别', phone VARCHAR(20) COMMENT '联系电话', email VARCHAR(50) COMMENT '邮箱', education VARCHAR(20) COMMENT '最高学历', experience TEXT COMMENT '工作经验描述', skill VARCHAR(255) COMMENT '技能特长', resume_text TEXT COMMENT '简历全文内容', current_status INT DEFAULT 0 COMMENT '状态: 0待筛选 1笔试中 2面试中 3已录用 4已淘汰', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 考核轮次表:记录笔试、一面、二面等排期 CREATE TABLE rms_round ( round_id INT PRIMARY KEY AUTO_INCREMENT, applicant_id INT NOT NULL COMMENT '关联应聘者', round_type INT COMMENT '1笔试 2面试 3终面', round_name VARCHAR(50) COMMENT '轮次名称', round_date DATETIME COMMENT '考核时间', location VARCHAR(100) COMMENT '地点/线上链接', remark VARCHAR(255) COMMENT '备注', FOREIGN KEY (applicant_id) REFERENCES rms_applicant(applicant_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 成绩记录表:分数与评价 CREATE TABLE rms_score ( score_id INT PRIMARY KEY AUTO_INCREMENT, round_id INT NOT NULL COMMENT '关联轮次', applicant_id INT NOT NULL COMMENT '关联应聘者', score DECIMAL(5,2) COMMENT '得分,例如85.50', evaluator VARCHAR(50) COMMENT '评分人', comment VARCHAR(500) COMMENT '评价意见', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (round_id) REFERENCES rms_round(round_id), FOREIGN KEY (applicant_id) REFERENCES rms_applicant(applicant_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 管理员表 CREATE TABLE rms_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, role INT DEFAULT 1 COMMENT '1普通用户 2管理员' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段建表 SQL 是这套系统数据关系的核心概括。你需要这样读:rms_applicant 是主表,current_status 字段控制整条招聘流程走到哪一步;rms_round 表记录了考核排期,每次“排期”操作就是向这张表插一条记录;rms_score 表存分数,它挂在 round 下面,一个轮次可以有多人打分。理解了这个关系,你再看 DAO 层代码就不会晕。

有个地方要特别留意:rms_score 和 rms_round 用外键做了关联,但某些版本的 SQL 脚本里外键约束可能没写完整。你在 MySQL 里SHOW CREATE TABLE rms_score;看一眼外键在不在,没有的话建议手动补上,否则后期录入成绩时可能出现悬空数据。我在课设复现时最容易在这块翻车。

3. 核心业务实现拆解:从简历浏览到成绩录入的完整链路

3.1 简历浏览:分页查询的 condition 拼装

简历浏览是系统的最前端入口。RMSResumeBrowsingServlet 承担分页列表和多条件筛选,DA 层里对应的是 ApplicantDaoImpl。核心逻辑是“动态拼 SQL”,根据用户勾选的学历、输入的姓名关键词,决定 WHERE 子句长什么样。

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 请求和响应编码统一成UTF-8,防止中文参数变乱码 request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); int pageNum = 1; int pageSize = 10; String keyword = request.getParameter("keyword"); String education = request.getParameter("education"); // 页码从请求中获取,但要校验非法值 String pageParam = request.getParameter("page"); if (pageParam != null && !"".equals(pageParam)) { pageNum = Integer.parseInt(pageParam); } // 保证pageNum最小为1,避免负数导致SQL异常 if (pageNum < 1) pageNum = 1; // 封装查询条件 Map<String, Object> condition = new HashMap<>(); condition.put("keyword", keyword); condition.put("education", education); ApplicantDao dao = new ApplicantDaoImpl(); PageResult<Applicant> pageResult = dao.pageQuery(pageNum, pageSize, condition); request.setAttribute("pageResult", pageResult); request.setAttribute("keyword", keyword); request.setAttribute("education", education); request.getRequestDispatcher("/WEB-INF/jsp/resume_list.jsp").forward(request, response); }

这段代码的逻辑我拆开讲:pageNum 和 pageSize 是分页的两根支柱,pageSize 决定每页多少条简历,一般课程项目用 10,如果你演示的数据量大可以调到 20,但注意 JSP 页面里分页按钮的显示逻辑。condition Map 是传参的容器,把 keyword 和 education 打包传给 DAO,这样的好处是后续加筛选条件不用改方法签名,只改 Map 的内容。最后 forward 到 JSP,而不是 sendRedirect,因为 forward 能把 request 里的属性带过去,JSP 直接pageResult.list就能迭代出数据列表。

对应 DAO 层的实现逻辑是这个系统的样板,我建议你把这段当模板背下来:

public PageResult<Applicant> pageQuery(int pageNum, intPageSize, Map<String, Object> condition) { List<Object> params = new ArrayList<>(); StringBuilder sql = new StringBuilder("SELECT * FROM rms_applicant WHERE 1=1 "); // 名字、技能、简历内容三个字段做模糊匹配 if (condition.get("keyword") != null && !"".equals(condition.get("keyword"))) { String keyword = "%" + condition.get("keyword") + "%"; sql.append(" AND (name LIKE ? OR skill LIKE ? OR resume_text LIKE ?) "); params.add(keyword); params.add(keyword); params.add(keyword); } // 学历精确匹配 if (condition.get("education") != null && !"".equals(condition.get("education"))) { sql.append(" AND education = ? "); params.add(condition.get("education")); } // 统计总数、查当前页数据 // SELECT COUNT(*) FROM rms_applicant + 同样的where条件 // SELECT * FROM rms_applicant + where条件 + LIMIT ?, ? int offset = (pageNum - 1) * pageSize; sql.append(" ORDER BY create_time DESC LIMIT ?, ?"); params.add(offset); params.add(pageSize); // 执行两条SQL后封装成PageResult对象返回 }

这里的关键参数是 LIMIT 的 offset 和 end 计算——第一条记录的偏移量永远是 (pageNum-1) × pageSize,这是 JDBC 手写分页最常见的数学逻辑,面试也常问。你再看这个 DAO 里用?占位符而不是字符串拼接,就是为了防止 SQL 注入,输入的关键词里带单引号也不会把 SQL 搞坏。很多改作业的学生会把这里写成字符串拼接然后被注入攻击演示打穿,答辩前一定要改成 PreparedStatement 风格。

3.2 笔试与面试成绩录入:round_id 与 score 联动

成绩录入环节是本系统最有业务厚度的一块。笔试成绩走 RMSPaperExamEnteringServlet,面试成绩走 RMSInterviewEnteringServlet,两者最终都落到 ScoreDaoImpl。数据关系上,一份成绩必须挂在某个 round 下面,所以录入成绩前你得先确定轮次 ID:

public int saveScore(Score score) { String sql = "INSERT INTO rms_score (round_id, applicant_id, score, evaluator, comment) " + "VALUES (?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, score.getRoundId()); ps.setInt(2, score.getApplicantId()); // 分数可能是小数,用setDouble而不是setInt ps.setDouble(3, score.getScore()); ps.setString(4, score.getEvaluator()); ps.setString(5, score.getComment()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } }

这里我特别强调一个参数坑:分数字段我用 setDouble 是因为 SQL 表里定义的是 DECIMAL(5,2),支持 85.50 这种带小数的笔试分。你要是图省事全用 setString,数据库虽然能隐式转换,但遇到非数字字符串会在 DAO 层直接抛异常,而且这种 bug 很难查,因为你看到的报错信息是那一长串 SQLException 而不是“你传错类型了”。录入后正常情况返回 1,返回 0 就是在告诉你连数据库都没写进去,先查连接。

面试成绩录入的 Servlet 除了调 saveScore,通常还会连带更新应聘者状态——面试通过就把 current_status 改成 3 已录用或者进入下一轮。这一步涉及事务:

public boolean passInterview(int applicantId, int nextStatus, Score score) { Connection conn = null; try { conn = DBUtil.getConnection(); // 关闭自动提交,让多条操作绑成一个事务 conn.setAutoCommit(false); // 第一步:插入成绩记录 String insertScore = "INSERT INTO rms_score (round_id, applicant_id, score, evaluator, comment) " + "VALUES (?, ?, ?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(insertScore)) { ps.setInt(1, score.getRoundId()); ps.setInt(2, applicantId); ps.setDouble(3, score.getScore()); ps.setString(4, score.getEvaluator()); ps.setString(5, score.getComment()); ps.executeUpdate(); } // 第二步:更新应聘者状态 String updateStatus = "UPDATE rms_applicant SET current_status = ? WHERE applicant_id = ?"; try (PreparedStatement ps = conn.prepareStatement(updateStatus)) { ps.setInt(1, nextStatus); ps.setInt(2, applicantId); ps.executeUpdate(); } // 都成功才提交 conn.commit(); return true; } catch (SQLException e) { // 只要有一个失败就回滚 if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtil.close(conn, null, null); } }

这段代码要讲的参数细节有两个。conn.setAutoCommit(false) 是把 JDBC 的默认自动提交关掉,让两条 SQL 形成一个原子操作——成绩写成功但状态没改成功时全部回滚,不会出现“分数在但简历状态卡住”这种脏数据。DBUtil.close 是自定义工具类,负责把 Connection、Statement、ResultSet 按顺序关掉,顺序反了会内存泄漏。你答辩时如果被问到“事务你怎么控制的”,这段就是标准答案。

3.3 状态流转:排期 Servlet 的调度逻辑

RMSResumeSchedulingServlet 是这套系统里最容易被忽略但最关键的组件。它做的事情是给应聘者排考核:当前状态是“待筛选”,排笔试,状态推进到“笔试中”;笔试通过后,安排面试,状态改“面试中”。它本质上是一个状态机控制器,用 current_status 字段的值驱动流程。

我在读这种排期代码时有一个固定的看码路径:先找状态字段的赋值点,再看有没有前置校验。好的排期逻辑应该拒绝“从待筛选直接跳到已录用”这种非法操作,因为中间少了笔试和面试,招进来的人是什么水平都没验证过。这属于业务规则,应该写在 Service 层而不是 JSP 里硬判。

实务中我会这样设计 Service 的排期方法:

public boolean scheduleRound(int applicantId, int roundType, String roundName, Date roundDate, String location) { // 根据应聘者当前状态判断该轮次是否合法 Applicant applicant = applicantDao.findById(applicantId); if (applicant == null) { return false; } int current = applicant.getCurrentStatus(); // 状态机校验:1笔试中 -> 2面试中 -> 3已录用,不允许逆向跳转 if (roundType == 1 && current != 0) { return false; } if (roundType == 2 && current != 1) { return false; } if (roundType == 3 && current != 2) { return false; } // 插入轮次记录 Round round = new Round(); round.setApplicantId(applicantId); round.setRoundType(roundType); round.setRoundName(roundName); round.setRoundDate(roundDate); round.setLocation(location); int insertResult = roundDao.insert(round); // 推进应聘者状态 int nextStatus = roundType; // 笔试中=1 面试中=2 终面=3 int updateResult = applicantDao.updateStatus(applicantId, nextStatus); return insertResult > 0 && updateResult > 0; }

你注意看 roundType 同时是轮次类型和下一状态的映射,这种写法在课程项目里很常见也够用。它的问题是不够通用——如果你要加“性格测试”“背调”这种阶段,状态机硬编码就得改好几个地方,这是它作为课设的边界。扩产思路是建一张 rms_status_dict 表存状态流转规则,但那是给人日后的项目用的,课程设计做到这版已经算超纲了。理解这个边界很重要,这也是我推荐你读这套代码的原因——它展示了业务逻辑怎么从代码里长出来,而不是停留在增删改查层面。

4. 把项目跑起来:JDK、Tomcat、MySQL 的版本搭配与部署

4.1 环境搭配:别让 Tomcat 版本卡住你

下载完源码包第一时间不是解压看代码,而是确认环境。很多学生卡在第一步不是因为代码错,而是 Tomcat 10 把包名从 javax.servlet 换成了 jakarta.servlet,旧代码直接编译不通过。我建议你按这个组合来配:

组件推荐版本说明
JDK8 或 11不要用 17,某些老依赖可能不兼容
Tomcat8.5 或 9.0关键:必须 9 及以下,避免包名地狱
MySQL5.7 或 8.08.0 需要新驱动,连接串多一个时区参数
EclipseEclipse IDE for Enterprise Java and Web Developers自带 WTP 插件,直接右键 Run on Server

JDK 版本这里我说一下我的习惯:JDK 8 最稳,因为绝大多数 Java Web 课设源码都是在 JDK 8 下写的,你在 JDK 11 下编译通常也没问题,但遇到 JavaMail 这类老库时可能报模块访问错误,犯不上为版本折腾。确认 JDK 的方式是命令行:

java -version

输出里有 1.8 字样就是 JDK 8,不要装 JRE 就完事,Servlet 编译需要 javac 工具。

Tomcat 这里有个更隐蔽的坑:如果你用的是集成环境(比如 IDEA Ultimate 自带的 Tomcat 或者某些绿色版),它可能内置了 Tomcat 10 而你毫无感知。第一次编译出The import javax.servlet cannot be resolved时,先查这个,别急着改代码。

4.2 初始化数据库:导入 SQL 和连接串配置

数据库是这套系统能跑起来的地基。解压包里应该带 SQL 脚本文件,按文件名排序找到它,用命令行导入或者 Navicat 执行都行。命令行导入的方式:

mysql -u root -p -h 127.0.0.1 < rms_db.sql

执行完成后进入 MySQL 验证:

mysql -u root -p USE rms_db; SHOW TABLES;

SHOW TABLES 能看到前面建表语句对应的那几张表。如果一张表都没建出来,先检查你执行 SQL 时有没有先创建同名数据库——有的脚本第一行有 CREATE DATABASE,有的没有,没有的话你得手动执行一次CREATE DATABASE rms_db DEFAULT CHARACTER SET utf8mb4;再导。

然后改数据库连接。数据库连接配置在源码里通常是 DBUtil.java 或者 db.properties 文件,你要找 Class.forName 和 DriverManager.getConnection 这两个位置。以 MySQL 8.0 为例:

static { try { // MySQL8必须用cj驱动类名,5.7时代的老类名已废弃 Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/rms_db" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"; conn = DriverManager.getConnection(url, "root", "你的密码"); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } }

这里的参数我给逐项说明:com.mysql.cj.jdbc.Driver 是 MySQL Connector/J 8 指定的驱动入口,写旧版的 com.mysql.jdbc.Driver 在 MySQL 8 下会直接报找不到类;useSSL=false 关闭 SSL 握手,本地开发时能省去证书配置;serverTimezone=Asia/Shanghai 是 MySQL 8 刚需,因为新版驱动默认用 UTC 时区,不指定它你和数据库的时间对不上。characterEncoding=utf8 这个参数负责让数据库连接的字符串传输是 UTF-8 编码,少了它,后面往数据库插中文大概率变问号。

连接串改完别急着运行,先把 MySQL Connector/J 的 jar 包放进项目的 WebContent/WEB-INF/lib 目录。很多新手只改了代码没放驱动包,启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver完全不知道怎么解决——就是这一步漏了。驱动版本和 MySQL 服务端版本不必严格一致,大版本匹配就行,8.0.x 的驱动连 5.7 数据库也没问题。

4.3 用 Eclipse 导入项目并启动首屏

导入项目的操作路径是:菜单 File > Import > Existing Projects into Workspace,选择解压后的项目文件夹,注意勾选 Copy projects into workspace,这样 Eclipse 会在自己的工作空间里生成一份副本,避免直接污染源码包。导入后项目列表里出现红叉时,优先检查两处。

第一处是 JRE System Library 的版本。右键项目 > Properties > Java Build Path > Libraries,确认 JRE 是你本地装的 JDK 8 而不是 Eclipse 默认的 JRE 17。这个错很阴间:代码本身没毛病,但编译器版本不对,报的全是莫名其妙的语法错误。第二处是 Targeted Runtime 有没有选 Tomcat。Window > Preferences > Server > Runtime Environment 里添加你的 Tomcat 安装目录,然后项目右键 Properties > Targeted Runtimes 勾上。没勾的话 Project Facets 校验会失败,一键 Run on Server 的按钮是灰色。

配置完成后把项目加到 Tomcat 里启动,启动成功后浏览器访问 login.jsp 或首页路径。如果看到 Tomcat 默认的 404 页面,大概率是欢迎页没配置对。在 web.xml 里加一段:

<welcome-file-list> <welcome-file>login.jsp</welcome-file> <welcome-file>index.jsp</welcome-file> </welcome-file-list>

顺序表示找 login.jsp 不行就找 index.jsp。这一步做完再看浏览器能不能正常进到登录页。进到这个页面,数据库连接基本就是通的,因为登录页的数据校验要查 rms_user 表,查不到表时系统会直接抛异常而不是乖乖渲染页面。到这一步,部署就算成功了一大半。

5. 避坑记录:四个最容易把课设卡死的真实故障

这里的每一条都是我在实操和帮人调课设时翻过的车。现象都是实际报错,原因和解决办法可以直接照抄。

5.1 Tomcat 10 的包名地狱

现象:项目导入后在 Problems 视图全是红色叉号,提示The import javax.servlet cannot be resolved,但你的 JDK 没问题、Tomcat 也添加了。

原因:Tomcat 10 开始把 Jakarta EE 的包名从 javax.servlet 迁移到 jakarta.servlet,这个源码是老的 javax.servlet 时代写的,两套包名互不兼容。你装了 Tomcat 10 就相当于拿新版容器跑旧代码,编译期直接死掉。

解决:确认你的服务器版本,卸载 Tomcat 10,换成 Tomcat 9.0.x 或 8.5.x。装完之后在 Eclipse 的 Server 视图里把旧 Server 删掉重新建一个,注意新 Server 的 Runtime Environment 要指向新 Tomcat 目录。装完 Tomcat 9 后代码一行都不用改就能编译通过。

5.2 MySQL 8 驱动类名与时区问题

现象:启动时抛java.lang.ClassNotFoundException: com.mysql.jdbc.Driver,或者报The server time zone value '�й���׼ʱ��' is unrecognized。

原因:驱动类名写的是老版 MySQL 5 时代的 com.mysql.jdbc.Driver,而 MySQL 8 的驱动必须写 com.mysql.cj.jdbc.Driver。时区报错是因为新版驱动默认走 UTC 时区,而你的系统时区是 Asia/Shanghai,两边不一致。另外要注意!号那个'�й���׼ʱ��'是乱码,实际是“中国标准时间”,说明连接串没指定编码。

解决:驱动类名改成com.mysql.cj.jdbc.Driver,连接串末尾加?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8。改完清一下 Tomcat 的缓存再重启,因为 Tomcat 有时会缓存旧的类路径,你不清理它还会用旧配置启动导致同样的报错。

5.3 中文乱码:请求、响应、连接串三层凑齐

现象:简历列表页打开后,名字和技能显示成??或者一堆乱码字符,数据库里存的也是???。

原因:中文乱码从来不是单点问题。第一层是请求参数编码,form 表单提交时没指定 accept-charset,Tomcat 默认按 ISO-8859-1 解码,中文就坏了;第二层是响应编码,JSP 页面没写 pageEncoding,浏览器按默认编码渲染;第三层是数据库连接串没带 characterEncoding=utf8,即使前端不乱,MySQL 存储时也会乱。

解决:三层一起修。JSP 文件头加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>;在 Java 里对 request 设置request.setCharacterEncoding("UTF-8");数据库连接串里加characterEncoding=utf8。注意表单提交是 POST 时没问题,如果是 GET 请求,Tomcat 8 以上默认 UTF-8 不用改,Tomcat 7 需要在 conf/server.xml 里配置 URIEncoding="UTF-8"。

5.4 表名大小写:Windows 能跑、Linux 上就崩

现象:本地 Windows 上系统运行完美,但把项目 + 数据库导到服务器(Linux 环境)后,一查表就报Table 'rms_db.RMS_APPLICANT' doesn't exist。

原因:Windows 的 MySQL 默认把表名大小写不敏感,而 Linux 下lower_case_table_names=0是严格区分大小写的。如果 SQL 脚本里建表用的 RMS_APPLICANT(全大写)而 Java 代码里查的是 rms_applicant(小写),或者反过来,Windows 上没事,Linux 上直接找不到表。

解决:统一表名风格。我习惯的做法是把 SQL 脚本里的所有表名都改成小写,Java 代码里的 SQL 语句也全部用小写表名,字段名保持原样。改完后重新导一次数据库,建表时字段名也尽量用下划线分隔的小写命名,这样最保险。同时把 MySQL 配置文件里的 lower_case_table_names=1 设上也行,但那是数据库层面的妥协,不如代码统一来得干净。

6. 进阶用法:给系统加一个 Excel 名单导出功能

系统跑通后,大部分人交完作业就停了。但答辩时评委最容易被“加分项”打动——比如把录用名单导出成 Excel。这里我用 Apache POI 给你演示一条最快见效的加功能路径。原理很简单:新增一个导出 Servlet,从数据库查出状态为“已录用”的应聘者,用 POI 生成 .xlsx 工作簿,通过 HTTP 响应流直接下载。

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置导出文件的响应头 response.setContentType("application/vnd.ms-excel;charset=UTF-8"); String fileName = URLEncoder.encode("录用名单.xlsx", "UTF-8"); response.setHeader("Content-Disposition", "attachment;filename=" + fileName); // 查已录用应聘者 ApplicantDao dao = new ApplicantDaoImpl(); List<Applicant> hiredList = dao.listByStatus(3); // 创建工作簿和表格 try (Workbook workbook = new XSSFWorkbook(); OutputStream out = response.getOutputStream()) { Sheet sheet = workbook.createSheet("录用名单"); Row header = sheet.createRow(0); header.createCell(0).setCellValue("姓名"); header.createCell(1).setCellValue("学历"); header.createCell(2).setCellValue("联系方式"); for (int i = 0; i < hiredList.size(); i++) { Applicant a = hiredList.get(i); Row row = sheet.createRow(i + 1); row.createCell(0).setCellValue(a.getName()); row.createCell(1).setCellValue(a.getEducation()); row.createCell(2).setCellValue(a.getPhone()); } workbook.write(out); out.flush(); } }

写完之后在 JSP 页面放一个“导出录用名单”的链接,指向这个 Servlet 的映射路径就行。要注意的是 response.getOutputStream 不能在获取之后再调 getWriter,否则会报 IllegalStateException,因为你已经拿了输出流就不能再要字符流了。文件名用 URLEncoder.encode 包一层是为了防止浏览器把中文文件名变成乱码或者截断,这在 IE 时代是个大坑,现代浏览器反而宽容很多。

做这个功能最有价值的不是代码本身,而是它帮你建立了一条完整的“查库 → 数据组装 → 响应输出”的链路心智。从那以后我每次做 Java Web 功能,都会强制先画一遍请求到响应的数据流草图,再落代码。这个习惯帮我避开了至少一半的“我明明写对了为什么没反应”的尴尬。希望这套系统和你加上去的导出功能,能让你在答辩时讲出一条完整有深度的业务故事——同样的东西在不同人手里,价值差好几倍,差别往往就在这种看不见的小功能上。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询