
简介一套基于Java Web的企业简历管理系统完整源码包适合课程设计、毕业设计及Java初学者进阶练习可直接用于毕业设计答辩演示。压缩包内共162个文件包含55个java源码、17个jsp页面、10个jar依赖包、2个sql数据库脚本以及编译后的class文件、图片与配置资源整体大小2.75MB目录结构清晰便于导入IDE后直接运行和二次开发。系统实现用户注册登录、简历创建编辑、简历查看、搜索筛选与权限管理等典型功能采用JSPServletServiceDao分层架构配合MySQL数据库直观展示MVC模式在真实项目中的组织方式。通过研读源码与SQL脚本可以掌握从页面请求到Servlet控制、Service业务处理、Dao数据访问的完整调用链学习用户表、简历表设计思路及常见权限控制写法。目前已有301人学习下载是一份轻量完整、贴合教学实战的Java Web项目参考资料。1. 企业简历管理系统课设答辩能直接讲的 Java Web 三层架构做课设最怕的不是写不出代码而是答辩时被问“你这分层到底分在哪”。这份基于 Java Web 的企业简历管理系统恰恰把 JSP Servlet Service Dao MySQL 这条经典技术栈完整串了起来。它不是花架子项目而是把简历创建、筛选、面试录入、笔试评分都收进了同一个系统里适合拿来写课程设计或毕业设计的实践案例。我拆完源码后发现一个值得说的点它比大多数只做增删改查的课设多了“招聘流程闭环”这一层业务逻辑这在答辩时是很加分的谈资。2. 三层架构到四个关键 Servlet代码包里的真实业务脉络2.1 为什么是 JSP Servlet 而不是 Spring Boot拿到这个项目的同学可能会有疑问现在企业里都 Spring Boot 一把梭了为什么课设还要用 Servlet答案很现实课程大纲考的就是手动实现 MVC。这份资源里没有 Spring、没有 MyBatis所有请求路径、转发、Dao 层连接管理都是手写的恰恰能让你把 HTTP 请求从进入 Tomcat 到返回 JSP 页面的完整链路讲清楚。从代码包里能够看到一组带有Servlet.class后缀的文件这类文件直接暴露了系统的访问入口。比如RMSResumeSchedulingServlet负责简历调度RMSInterviewEnteringServlet负责面试信息录入RMSPaperExamEnteringServlet负责笔试成绩录入RMSResumeBrowsingServlet负责简历浏览。四个入口对应了招聘流程的四个节点筛选、面试、笔试、归档。这种按“业务流程”而不是按“数据表”来划分控制器的思路是区分课设水平和项目经验的关键信号。MVC 在这个项目里的分工很清晰View 层是 JSP负责渲染表单和列表。Controller 层是 Servlet负责接收请求参数、调用业务服务、决定跳转页面。Model 层由 Service Dao 组成前者封装业务规则后者负责 SQL。MySQL 在最后端存储用户表、简历表、面试轮次表和笔试成绩表。2.2 Dao 层设计从 impl 到接口的依赖倒置代码清单里有ApplicantDaoImpl.class和ScoreDaoImpl.class。这里要注意它们不是孤立类一般是某个接口的实现接口名大概率叫ApplicantDao或ScoreDao。为什么要在课设阶段就引入接口常见做法是 Service 层只依赖接口不依赖具体实现类以后如果要换成 MyBatis 的 Mapper只需新写一个实现类Service 层代码一行都不用改。我一般看一个课设项目是否合格先看它有没有分Dao接口和DaoImpl实现。如果只有 Impl 没有接口说明作者没理解解耦。如果接口和 Impl 都有就可以在答辩时说“用到了依赖倒置原则业务层不直接依赖数据访问细节”。打开ApplicantDaoImpl的源码后核心方法往往围绕三块展开insertApplicant()新增一份简历主记录。updateApplicant()更新基本信息、联系方式、期望薪资。queryApplicantByCondition()按关键词、学历、工作经验做联合查询。2.3 业务层封装RoundDaoImpl 揭示的“轮次”模型很多人第一次看到RoundDaoImpl会愣住简历系统为什么要管“轮次”这其实是一个面试安排的多轮记录表。应届生求职流程中一个候选人可能要经历一面、二面、HR 面。RoundDao大概率的表结构是round_id轮次主键。applicant_id关联简历主表。round_type一面、二面、笔试。interviewer面试官。round_result通过 / 淘汰 / 待定。score笔试或面试分数。remark评价备注。这个设计的价值在于同一候选人可以对应多条轮次记录业务层通过查询该申请者下最大的round_id来判断当前处于第几轮。理解了这张表的含义去读RoundDaoImpl里的 SQL 就会很快基本都是SELECT ... WHERE applicant_id ? ORDER BY round_id这类操作。2.4 启动过程RMSInitServlet 与 RMSNavigationServlet 的分工项目里有两个容易被忽略的 Servlet一个是RMSInitServlet一个是RMSNavigationServlet。前者大概率做了初始化操作比如加载系统配置、建立数据库连接池或写入默认管理员账号。后者通常是一个统一的导航分发器根据请求参数决定页面跳转方向。这里有个很实用的架构细节与其在每个 Servlet 里重复写request.getRequestDispatcher(/xxx.jsp).forward(request, response)不如抽一个导航 Servlet 来统一处理。这虽然在 Servlet 规范里不算强制要求但可以在答辩中体现“消除重复代码”的意识。它的典型工作过程是接收pageresumeList之类参数。判断参数对应的 JSP 路径。携带request和response对象完成转发。2.5 六个核心类串起完整招聘闭环到这里基本可以把资源包里的 8 个类还原成一个完整业务流。简历浏览RMSResumeBrowsingServlet把简历列表查出来翻到 JSP 展示。简历调度RMSResumeSchedulingServlet对合格简历做面试排期。面试录入RMSInterviewEnteringServlet填入面试官、时间、评价。笔试录入RMSPaperExamEnteringServlet填入笔试成绩和排名。数据对象MultiInfo作为复合值对象承接多表查询的返回结果。数据访问ApplicantDaoImpl、ScoreDaoImpl、RoundDaoImpl提供各自的持久化操作。当我看到MultiInfo这个类名时第一反应是作者踩过“单表返回不够用”的坑。在简历列表页打出候选人及其最高学历、最近工作单位、面试轮次时单靠Applicant实体根本装不下必须弄一个复合对象来装多表 join 的结果。数据库里面这种名字很常见它就是一个没有业务逻辑的纯数据容器。3. 把项目跑起来环境准备、数据库导入和三条起步命令3.1 环境版本选择别用 JDK 17 给自己挖坑这个项目基于 Servlet 3.x 或更早的规范所以环境搭配非常重要。我建议使用以下组合组件推荐版本说明JDK1.8兼容性最好不会遇到 module 报错Tomcat8.5 / 9.0支持 Servlet 3.1稳定MySQL5.7 / 8.0若用 8.0 注意驱动包版本IDEEclipse / IDEA直接导入项目数据库驱动mysql-connector-java-5.1.495.x 驱动兼容 5.7 和 8.0很多同学一上来装了最新的 JDK 17结果 Tomcat 启动直接报UnsupportedClassVersionError原因就是 class 文件版本号太高。课程设计项目不需要追求新开箱即跑的稳定才是硬道理。如果你只有 JDK 11 以上版本需要在 IDEA 里把 Project Structure 的 Project SDK 和 Java Compiler 都调到一致并且尽量用 Tomcat 9。Tomcat 9 官方支持 Servlet 4.0跑这种老项目问题不大但 Tomcat 10 要谨慎因为它的 Jakarta EE 命名空间改了javax.servlet包名全换成了jakarta.servlet这个项目的源码基于javax.servlet直接扔到 Tomcat 10 里会编译失败。3.2 导入数据库从 SQL 脚本到建库建表资源包里应该有.sql的数据库脚本如果你没有找到单独的 SQL 文件就打开DbUtil或DBConnection类的源码里面通常写了数据库名、账号密码和初始化表结构。我处理课设项目的习惯是先创建一个专用数据库CREATE DATABASE IF NOT EXISTS resume_db DEFAULT CHARACTER SET utf8mb4; USE resume_db; SOURCE /your/path/resume_db.sql;执行完后重点检查四张核心表是否建全user 表存储账号密码和角色、applicant/resume 表简历基本信息、round 表面试轮次、score 表笔试或评分记录。如果发现缺少某个字段可以对照源码中 Dao 层的 SQL 来反推表结构。这里有一个值得避开的坑不要用 root 账号直接把密码硬编码在源码里并提交到公开仓库。虽然课设项目无所谓但老师问到安全问题时容易露怯。比较好的做法是把数据库连接配置抽到一个 properties 文件中代码里只读取配置。3.3 IDEA 部署 Tomcat从 Artifact 到启动验证以 IntelliJ IDEA 为例完整部署流程可以按下述步骤操作打开 IDEA选择File - New - Project from Existing Sources选择项目根目录。选择Import project from external model并勾选Eclipse或直接使用Web目录结构。配置 Project SDK 为 JDK 1.8。打开Project Structure - Facets添加 Web 模块指定web.xml路径。打开Project Structure - Artifacts新建Web Application: Exploded。配置 Tomcat Server在 Deployment 标签页加入这个 Artifact。修改 Application context 为/resume保证访问路径和代码里跳转路径一致。启动成功后浏览器访问地址应该是http://localhost:8080/resume/login.jsp如果首页是直接跳转的启动路径可能是http://localhost:8080/resume/。注意项目如果用了RMSInitServlet做初始化第一次启动时它会自动往数据库塞管理员账号类似admin / admin123登录前先刷新一下数据库表看有没有插入成功。3.4 连接 MySQL 8.0 的参数变化如果你的 MySQL 是 8.0 版本驱动类和连接串写法需要相应调整。旧代码里常见写法是Class.forName(com.mysql.jdbc.Driver); String url jdbc:mysql://localhost:3306/resume_db;MySQL 8.0 之后的正确写法Class.forName(com.mysql.cj.jdbc.Driver); String url jdbc:mysql://localhost:3306/resume_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8;区别在于驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver并且必须加上serverTimezone参数否则会报时区相关的 SQLException。这是一个非常经典的迁移问题十个跑 MySQL 8 的课设组里至少有五个会踩到。在 Dao 层连接方式上这套代码大概率用的是DriverManager.getConnection()直连方式资源包里没有看到连接池相关内容。如果答辩想加分可以改造成 Druid 或 HikariCP 连接池改动范围集中在 DbUtil 类大约 30 到 50 行但对系统稳定性的解释空间会大很多。4. 手写核心登录与简历管理安全判断和查询 SQL 的细节4.1 登录检查用 Session 控制页面访问权限虽然这个系统有注册登录功能但我更关注的是登录状态是否在每个 Servlet 里都做了拦截。一个标准的课设登录校验代码大概是这个样子public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); User user userService.login(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(currentUser, user); response.sendRedirect(index.jsp); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } }这段代码里有三个小细节。第一是sendRedirect和forward的选择登录成功后用重定向可以避免刷新页面时表单重复提交登录失败后用转发可以在同一请求里携带错误信息回显给 JSP。第二是密码存储层面课设阶段大多数没做 MD5 加密如果答辩时老师问“密码明文存数据库怎么办”最好提前把登录模块改成 MD5 加盐校验。第三是权限拦截很多 Servlet 的 doGet 里没有判断 Session 是否为空直接访问某个页面会报空指针。可以加一个简单的过滤器Filter统一处理这是体现安全意识的好改动点。4.2 简历创建的多表写入事务的边界简历创建不是只 insert 一张表至少包含主表、教育经历表、工作经历表和多条技能记录。在一个没有框架的项目里多表写入最怕的是中途报错导致数据不一致。我一般会在 Service 层用一个事务方法把所有 insert 包起来public boolean addResume(Resume resume, ListEducation eduList, ListWorkExperience workList) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); applicantDao.insertResume(conn, resume); for (Education edu : eduList) { educationDao.insert(conn, edu); } for (WorkExperience work : workList) { workDao.insert(conn, work); } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { DBUtil.closeQuietly(conn); } }注意上述代码里每个 Dao 方法都接收一个Connection参数这是手写事务的关键。如果每个 Dao 内部自己getConnection()那事务就没法跨多个 Dao 共享后一半插入失败时前面已提交的数据就撤不回来了。这个项目若想把事务写好务必检查DBUtil是否提供了getCurrentThreadConnection()这类方法或者直接把 Connection 当作参数逐层传下去。Dao 层常见的insertResume实现是使用PreparedStatementString sql INSERT INTO applicant (name, phone, email, education, work_years, expected_salary, create_time) VALUES (?, ?, ?, ?, ?, ?, NOW()); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, resume.getName()); ps.setString(2, resume.getPhone()); ps.setString(3, resume.getEmail()); ps.setString(4, resume.getEducation()); ps.setInt(5, resume.getWorkYears()); ps.setBigDecimal(6, resume.getExpectedSalary()); ps.executeUpdate(); ResultSet keys ps.getGeneratedKeys(); if (keys.next()) { resume.setId(keys.getInt(1)); }RETURN_GENERATED_KEYS这一步特别重要因为后面插入教育经历表时需要用简历主键做外键关联。如果漏掉这一步多表数据就接不上关系。4.3 简历搜索和筛选动态拼接 SQL 的注意点系统的搜索功能支持按关键词、学历、工作经验等条件筛选这意味着queryApplicantByCondition需要动态拼接 SQL。常见的写法用StringBuilder需要注意 SQL 注入StringBuilder sql new StringBuilder(SELECT * FROM applicant WHERE 11); ListString conditions new ArrayList(); ListObject params new ArrayList(); if (!StringUtils.isEmpty(keyword)) { conditions.add((name LIKE ? OR phone LIKE ? OR email LIKE ?)); params.add(% keyword %); params.add(% keyword %); params.add(% keyword %); } if (education ! null) { conditions.add(education ?); params.add(education); } if (workYears 0) { conditions.add(work_years ?); params.add(workYears); } for (String condition : conditions) { sql.append( AND ).append(condition); } sql.append( ORDER BY create_time DESC);两个关键点值得在答辩时讲明白。第一WHERE 11不是多余的拼接技巧它是为了让后续所有条件都可以统一用AND开头避免判断“到底是不是第一个条件”的麻烦。第二所有条件值都用?占位符参数通过PreparedStatement传入从机制上杜绝了 SQL 注入。解释清楚这两点比背十遍 MVC 定义更能证明你懂实际开发。4.4 把复合查询结果装进 MultiInfo前面提到MultiInfo这个类它存在的理由是列表页很可能需要展示申请人姓名、最高学历、当前状态、最近一轮面试官、最近一次评分。这些数据分布在 applicant、round、score 三张表里所以 SQL 大概率是这样SELECT a.id, a.name, a.education, r.round_result, s.score, r.interviewer FROM applicant a LEFT JOIN round r ON a.id r.applicant_id LEFT JOIN score s ON a.id s.applicant_id WHERE a.id ? ORDER BY r.round_id DESC LIMIT 1此处LEFT JOIN的使用也值得注意如果一个人还没进入面试轮次round表里就没有他的记录此时INNER JOIN会把这个人直接过滤掉但简历列表里应该还是要看到他。用LEFT JOIN保证主表数据不丢这是列表查询的基本习惯。MultiInfo内部字段就是上面 SQL 的查询结果集合它不承担业务行为纯粹当作数据传输对象。在 JSP 页面用 EL 表达式直接取字段渲染表格c:forEach items${applicantList} varitem tr td${item.name}/td td${item.education}/td td${item.roundResult}/td td${item.score}/td /tr /c:forEach如果 JSP 页面直接写 Java 脚本片段代码里一定会出现%标签那会让页面难以维护。想让项目更有工程味道就尽量引导使用 JSTL 和 EL 表达式。5. 部署与代码评审避坑六条常见的课设翻车记录5.1 Tomcat 启动后访问全是 404现象Tomcat 能正常启动日志也没报错但访问http://localhost:8080/resume/login.jsp时页面 404。原因Artifact 没有正确部署到 Tomcat或 Application context 设置成了/实际访问路径与部署路径不一致。解决检查 IDEA 的 Run Configuration 中 Deployment 标签页确认 Artifact 已加入且 Application context 是/resume。启动后在 Tomcat 的webapps目录看是否自动生成了resume文件夹如果没有说明部署失败删除 Tomcat 配置里的部署项后重新添加。5.2 数据库连接报Public Key Retrieval is not allowed现象首次访问页面时抛出 SQLNonTransientConnectionException提示Public Key Retrieval is not allowed。原因MySQL 8.0 默认使用 caching_sha2_password 认证客户端连接时默认不允许获取公钥。解决在 JDBC URL 后面追加allowPublicKeyRetrievaltrueuseSSLfalse。完整连接串是这样jdbc:mysql://localhost:3306/resume_db?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai5.3 JSP 页面中文全部乱码现象页面输出中文变成问号或乱码数据库里存进去的数据也是乱码。原因数据流环节中有至少一处字符编码不是 UTF-8。解决按三层排查。第一数据库连接串加characterEncodingutf8第二JSP 页面顶部写% page contentTypetext/html;charsetUTF-8 languagejava %第三ServletdoPost开头加request.setCharacterEncoding(UTF-8)。如果用了 Tomcat 8.5 以上GET 请求默认 UTF-8不需要额外配置 URIEncoding。5.4ClassNotFoundException: com.mysql.jdbc.Driver现象Tomcat 启动后调用数据库操作时报类找不到驱动。原因MySQL 驱动 jar 没有放进 Artifact 的 WEB-INF/lib 目录或者驱动版本与 MySQL 版本不匹配。解决在 IDEA 的 Project Structure - Artifacts - Output Layout 中右键加入 Library Files把 mysql-connector 的 jar 加进去重新 Build Artifact 后重启 Tomcat。直接放在项目/WEB-INF/lib目录下是最稳妥的做法不要放在 JDK 的扩展目录里。5.5 简历创建报外键约束失败现象创建简历时提示Cannot add or update a child row: a foreign key constraint fails。原因代码里插入教育经历或工作经历时传给外键字段的 applicant_id 是 0 或 null。典型的错误是 Dao 里INSERT ... RETURN_GENERATED_KEYS没有执行或没有调ResultSet的next()。解决在insertResume方法执行后打印主键值确认。如果主键获取成功再去教育经历表的 insert 方法里逐行打个断点核对参数顺序很多情况下是ps.setInt(index, value)的 index 插错位。5.6 Session 失效后操作报 NullPointerException现象长期停留在页面不操作再点某个链接时后台空指针异常堆栈定位到session.getAttribute(currentUser).getId()这一行。原因Session 默认 30 分钟过期currentUser变成 null。解决在 Servlet 入口处统一加判断或写一个过滤器校验登录态。核心代码可以这样做public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); String uri req.getRequestURI(); if (uri.contains(login) || uri.endsWith(.css) || uri.endsWith(.js)) { chain.doFilter(request, response); return; } if (session null || session.getAttribute(currentUser) null) { resp.sendRedirect(login.jsp); return; } chain.doFilter(request, response); }在web.xml里注册这个过滤器url-pattern设为/*。这一处改动直接决定答辩时老师对系统完整度的直观印象。6. 给课设加加分把简历数据做成可视化统计与 Excel 导出系统本身是管理型应用课设答辩时如果能拿出数据可视化的页面会让人觉得这个项目“超出预期”。我个人通常会在原系统里增加两个功能一个按学历分布统计柱状图另一个把面试结果导出为 Excel 文件。6.1 用 ECharts 展示学历分布不引入后端图表库直接让 Dao 层提供一个统计方法返回每个学历对应的人数前端用 ECharts 画图。Dao 层新增方法public ListMapString, Object countByEducation() { ListMapString, Object result new ArrayList(); String sql SELECT education AS name, COUNT(*) AS value FROM applicant GROUP BY education; // 省略 JDBC 执行细节 return result; }对应新增一个统计 Servlet把返回的 JSON 写入 responseresponse.setContentType(application/json;charsetUTF-8); PrintWriter out response.getWriter(); out.write(new Gson().toJson(countList)); out.flush();前端页面引入 ECharts$.ajax({ url: statisticsServlet, dataType: json, success: function(data) { var chart echarts.init(document.getElementById(main)); chart.setOption({ tooltip: {}, xAxis: { data: data.map(item item.name) }, yAxis: {}, series: [{ type: bar, data: data.map(item item.value) }] }); } });重点是在答辩时说明统计 SQL 的聚合逻辑GROUP BY education把相同学历的记录合并COUNT(*)统计每一组行数这种方式比在 Java 内存里循环统计要高效得多。6.2 导出 Excel用 POI 生成面试结果导出功能在课设项目里出现频率不高如果实现了含金量会明显提升。核心代码如下public void exportInterviewResult(ListMultiInfo list, HttpServletResponse response) throws IOException { Workbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(面试记录); String[] headers {姓名, 学历, 面试官, 结果, 备注}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } int rowNum 1; for (MultiInfo info : list) { Row row sheet.createRow(rowNum); row.createCell(0).setCellValue(info.getName()); row.createCell(1).setCellValue(info.getEducation()); row.createCell(2).setCellValue(info.getInterviewer()); row.createCell(3).setCellValue(info.getRoundResult()); row.createCell(4).setCellValue(info.getRemark()); } for (int i 0; i headers.length; i) { sheet.autoSizeColumn(i); } response.setContentType(application/vnd.ms-excel;charsetutf-8); response.setHeader(Content-Disposition, attachment;filenameinterview_result.xlsx); workbook.write(response.getOutputStream()); workbook.close(); }XSSFWorkbook是 POI 操作 Excel 2007 及以上格式的入口autoSizeColumn会根据内容自动拉宽列。注意响应头里Content-Disposition的filename用 ASCII 命名避免不同浏览器中文文件名乱码。从那以后我每次拿到一个课设源码都会按同样的顺序做三件事打开 Dao 层看 SQL 是否全部用 PreparedStatement打开 Servlet 看 Session 判断是否覆盖所有入口最后看数据库脚本有没有设置外键。三个检查做完这个项目能不能跑、能不能答辩、值不值得看心里大概就有数了。本次拆解的资源在这三件事上都没有敷衍只要把环境对齐就能顺利启动。希望帮到你。本文还有配套的精品资源点击获取