简介:一份基于 JavaWeb 的企业人事管理系统完整源码包,面向 Java 毕业设计者和正在学习 Servlet/JSP/JDBC 的开发者,可用于理解传统 JavaWeb 项目的 MVC 分层架构与业务开发流程。系统覆盖用户管理、员工信息、部门职位、考勤、薪酬福利、绩效、培训和报表统计等常见人事业务模块。压缩包内共包含 291 个文件,整体约 57.78MB,以 Java 源文件、JSP 页面、class 编译文件、jar 依赖库、前端 JavaScript/CSS 资源及 SQL 数据库脚本为主;其中 personnel.sql 可用于初始化数据库,帮助搭建演示环境。目前已有 134 人学习浏览。这套源码从数据库表设计、后端控制器与业务服务实现,到前端页面展示均有完整呈现,适合毕业设计参考和二次功能扩展。
1. 这毕业设计人事系统能落地吗:Servlet/JSP 老栈里的真东西
很多 Java 毕业设计一看标题就劝退,但这次这个基于 JavaWeb 的企业人事管理系统源码包里,实际东西比标题值钱。它不是那种只给个前端壳子的演示项目,而是把 Servlet、JSP、JDBC、MySQL 这一整套传统 JavaWeb 技术栈完整串起来的业务系统:员工、部门、职位、考勤、薪酬、绩效、培训、报表全都在里面。对正在做毕业设计或刚进公司想补 JavaWeb 底子的人来说,拆完这套代码,相当于把过滤器、会话管理、JDBC 封装、MVC 分层这些课本概念全部落到真实业务里过了一遍。我建议用 IDEA 或 Eclipse 配 Tomcat 8.5 跑起来,再拿着 personnel.sql 建库,一步步断点走业务流,比看十篇博客都管用。
2. 源码结构和 MVC 分层:从 .class 到可读代码的拆解路径
拿到压缩包,第一眼看到的是一堆.class文件,比如RainServiceImpl.class、UserController.class、EmployeeController.class、AuthorizedInterceptor.class。很多人会被这些编译产物吓到,以为源码不完整,其实这是 JavaWeb 项目最常见的一种打包方式:编译后的 class 文件连同源码一起拷出来,方便直接部署到 Tomcat 的classes目录下运行。想读代码的话,优先找src目录下的.java源文件,按 MVC 分包去读,不要对着 class 文件硬啃。
2.1 从类名推断模块边界
先看UserController、EmployeeController、DeptController、JobController、DocumentController、NoticeController,这一组类名对应的就是系统的对外入口,在传统 JavaWeb 里它们本质上是 Servlet,只不过类名用了 Controller 后缀。每个 Controller 负责一个业务域的请求接收和响应分发:EmployeeController管员工的增删改查和批量导入导出,DeptController管部门树结构,JobController管职位定义,NoticeController发通知公告,DocumentController处理文档上传下载。这个命名习惯和 Spring MVC 很像,但底层是纯粹的 Servlet 规范,所以读代码时要清楚:这里的 Controller 继承的是HttpServlet,而不是 Spring 的@Controller注解。
RainServiceImpl这个类名是很有观察价值的点。它对应业务层,Service 后缀说明它封装了具体业务逻辑,比如员工入职时的部门人数统计、薪资计算时的考勤数据汇总。Impl 后缀则是接口实现类的命名惯例,说明系统中应该有配套的RainService接口,接口定义方法签名,实现类写具体 SQL 和业务规则。这种接口加实现的方式在毕业设计里少见,但确实是规范写法,好处是后期替换实现类或用 Mock 对象做单元测试都方便。
AuthorizedInterceptor这个类名更值得注意。名字带了 Interceptor,但纯 Servlet 规范里并没有拦截器这个概念,标准做法是 Filter。所以这个类大概率是一个自定义的Filter,或者模拟 Spring MVC 拦截器写的一个权限校验组件。我一般打开这个类先看两处:doFilter方法里有没有放行白名单,以及 session 里保存用户信息的 key 是什么。这两个信息直接决定登录逻辑怎么依赖它,后面部署时如果想加一个免登录的测试入口,改这里最简单。
2.2 经典 JavaWeb 包结构与依赖关系
源码包的目录结构通常长这样:
src ├── com.xxx.controller # Servlet 控制器 ├── com.xxx.service # 业务接口 ├── com.xxx.service.impl # 业务实现 ├── com.xxx.dao # JDBC 数据访问 ├── com.xxx.model # 实体类 ├── com.xxx.util # 工具类 └── com.xxx.interceptor # 登录/权限拦截 WebContent ├── WEB-INF │ ├── web.xml # 部署描述符 │ └── lib # 项目依赖的 jar 包 ├── css / js / images # 静态资源 └── jsp 页面这里重点看web.xml。JavaWeb 项目里它管三件事:Servlet 路由映射、Filter 过滤规则、欢迎页面。如果项目用的是 Servlet 3.0 以上规范,部分配置可以用@WebServlet注解替代,但web.xml依然承担 Filter 顺序和初始化参数的兜底作用。读web.xml时我一般先查 servlet-mapping,搞清每个 URL 对应哪个 Controller,然后看 filter-mapping 确认拦截器作用在哪些路径上。一个常见的配置是/login、/register放行,其余路径全部经过AuthorizedInterceptor校验 session,校验失败直接重定向到登录页。
依赖 jar 包都放在WEB-INF/lib下,这也是传统 JavaWeb 项目没有 Maven 时的标准做法。看一眼 lib 目录里有哪些 jar,基本就知道系统用了什么组件:MySQL 驱动 jar、JSTL 标签库、commons-fileupload 处理上传,可能还有 commons-dbutils 做 JDBC 封装。这个信息和代码是配套的,读代码时如果看到QueryRunner,说明用了 dbutils;如果看到PreparedStatement,说明是原生 JDBC。两种写法没有绝对优劣,但 dbutils 封装过的代码行数少一半,可读性好很多。
2.3 拿到源码后第一件事:先编译验证
不管包里的代码能不能直接跑,我拿到手的第一步永远是编译验证。这不是不信任作者,而是 JavaWeb 项目最容易在环境差异上翻车:别人的 JDK 版本、Tomcat 版本、MySQL 版本和你本机不同,跑不起来是常态。新建一个空项目,把src里的.java文件拷进去,检查一遍 import 语句有没有缺依赖,然后用javac命令行手动编译,能过说明代码本身没有硬伤。
# 手动编译验证,javac 会按 import 关系自动查找 class 文件 # -encoding utf-8 必须加,否则中文注释可能编译报错 javac -encoding utf-8 -cp "WebContent/WEB-INF/lib/*" -d build/classes src/com/xxx/**/*.java这条命令的意思是把src下所有 Java 文件编译到build/classes目录,-cp后面指定依赖 jar 包路径,WebContent/WEB-INF/lib/*这种写法在 Linux 和 macOS 下可以直接用通配符,Windows 下如果报错,把*换成实际的 jar 文件列表用分号连接。-d参数指定编译输出目录,编译通过后这个目录里会生成对应包结构的 class 文件。如果编译报错,绝大多数情况是缺 jar 包或 JDK 版本不匹配,把报错信息里的类名拿去 lib 目录里搜一下就知道缺什么了。
编译通过后项目才具备手动部署的条件。接下来的部署方式有两种:一种是把编译好的 class 文件连同WebContent里的 JSP、静态资源一起拷到 Tomcat 的webapps目录下;另一种是直接用 IDEA 或 Eclipse 的 Artifact 功能打包成 war 再部署。我建议一步到位用 IDEA,因为后面调试断点、热部署都方便,命令行编译只是用来验证源码完整性的。
3. 数据库设计:personnel.sql 的表关系与字段取舍
解开压缩包后,personnel.sql是最值得先读的一个文件。它不是随便几条建表语句,而是整个系统的数据地基。我拿到 SQL 文件的第一件事是打开看建表语句的注释,哪些表先建、哪些表后建、外键怎么关联,这些信息直接反映作者的建模思路。企业人事系统里最核心的实体是员工,但员工不是孤立存在的,它一定关联着部门、职位、考勤、薪酬,这些表之间的外键关系如果设计得合理,后期写 SQL 报表会省很多事;如果设计得随意,业务逻辑层就会堆一堆没必要的聚合查询。
3.1 核心表梳理与字段解读
按一般表结构推断,这个库至少包含以下核心表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| t_user | 系统登录账号 | user_id, username, password, role |
| t_employee | 员工档案 | emp_id, emp_no, name, dept_id, job_id, hire_date |
| t_dept | 部门 | dept_id, dept_name, parent_id |
| t_job | 职位 | job_id, job_name, salary_base |
| t_attendance | 考勤 | att_id, emp_id, att_date, status |
| t_salary | 薪酬 | salary_id, emp_id, base_salary, bonus, deductions |
| t_notice | 公告 | notice_id, title, content, publish_time |
员工表关联部门和职位,考勤表关联员工,薪酬表关联员工,这个设计符合第三范式:基础数据存在各自的表里,业务数据通过外键引用,避免重复存储。例如薪酬表里不会直接冗余一个部门名称字段,而是通过emp_id关联到员工,再通过员工关联到部门,需要查部门维度的薪资统计时,JOIN 两张表就能拿到。这个设计思路本身没问题,但要注意一个场景:如果部门调整了,历史薪酬记录里关联的部门也变了,查历史报表时可能对不上账。解决方式是加一个dept_id历史快照字段,或者单独存一张薪酬历史表。
parent_id这个字段在部门表里非常关键。它用来支持部门树,根部门的parent_id为 NULL 或 0。我见过不少人对树结构无从下手,其实递归查询或循环遍历都行,关键看你用的数据库版本。MySQL 8.0 支持WITH RECURSIVE递归 CTE,一条 SQL 就能查出完整层级;如果是 MySQL 5.7 及更低版本,就要用 Java 代码循环拼装树结构。读DeptController的源码时,重点看它是怎样处理parent_id的,这直接关系到你有没有能力二次开发部门树的功能。
3.2 导入 SQL 的正确姿势和常见坑
导入personnel.sql时,新手最容易犯的错误是无脑用 Navicat 或 IDEA 的数据库面板直接执行整个文件。如果 SQL 文件里带了已有的建库语句,比如CREATE DATABASE IF NOT EXISTS personnel; USE personnel;,那么直接执行没问题;但如果文件里只有建表语句,没有建库语句,就会导入到当前选中的默认数据库里,后面 JDBC 连接串里写的库名对不上,系统一启动就报找不到表。稳妥做法是先用命令行确认文件内容:
# 查看 SQL 文件头部,确认是否包含建库语句 head -n 50 personnel.sql # 如果没有建库语句,先手动建库,再导入 mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS personnel DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p personnel < personnel.sqlMySQL 5.7 及以下版本对中文支持最好的是utf8mb4,它能存下 emoji 和特殊字符。如果 SQL 文件里建表语句指定的字符集是utf8,在 5.7 下的中文基本没问题,但如果你想统一用utf8mb4,导入前先用文本编辑器全局替换一遍。另外留意 SQL 文件里的ENGINE字段,InnoDB 支持外键约束和事务,MyISAM 虽然查询快但不支持事务,人事系统里有薪酬计算这种需要强一致性的场景,表引擎统一用 InnoDB 是底线。
导入成功后别急着跑项目,先执行几条查询验证数据完整性:
-- 验证核心表是否都有数据 SELECT COUNT(*) FROM t_employee; SELECT COUNT(*) FROM t_dept; SELECT COUNT(*) FROM t_user; -- 验证外键关系是否有效 SELECT e.emp_name, d.dept_name FROM t_employee e LEFT JOIN t_dept d ON e.dept_id = d.dept_id WHERE d.dept_id IS NULL;第二条 SQL 如果返回有记录,说明员工表里有员工挂在了一个不存在的部门上。这种情况在生产库是可修复的脏数据,在毕设题里则可能是 SQL 脚本里的 INSERT 语句顺序问题。外键缺失会直接导致员工列表页的「部门名称」一列显示为空,不懂的人会以为是代码 bug,其实数据就没对上。校验外键这一步能帮你排除掉 80% 的「页面白屏」和「查询不到数据」类问题。
3.3 JDBC 连接配置和连接池选型
personnel.sql搞定之后,JDBC 连接配置是下一个关键点。传统 JavaWeb 项目里,数据库连接信息一般写在db.properties或jdbc.properties文件里,放在src目录下。代码里通过Properties类加载这个文件,再拿 key-value 拼 JDBC URL。我打开这种项目时,第一眼就看这个文件里的四个参数:driver、url、username、password。其中最容易出问题的是 url 里的参数,MySQL 5.7 和 8.0 的连接串写法略有差异,比如 8.0 需要显式指定useSSL=false和serverTimezone=Asia/Shanghai,不然会报时区错误。
# 典型的 JDBC 连接配置,MySQL 8.0 必须加时区参数 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/personnel?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456如果系统里用了连接池,比如 C3P0 或 Druid,配置项会更多。Druid 的话一般还有initialSize、minIdle、maxActive这些参数,毕设项目通常不会涉及高并发调优,保持默认值就行,但maxActive至少要大于 10,否则测试的时候多开几个页面就可能连接不够用。如果你发现代码里每次请求都新建连接,没用连接池,那也别急着改,先跑通再说。性能优化是后话,能跑通是最低目标。
4. 在 IDEA 里跑起来:Tomcat 部署、JDBC 连接与首个请求
从零到一跑通这个项目是最大的坎。IDE 环境和命令行不一样,IDEA 里配置 Web 项目有一套固定的操作路径,我按自己常用的步骤写一遍,跟着做基本不会卡死。跑通之后再看代码逻辑,心里就有底了。
4.1 IDEA 导入项目的两种方式
第一种方式适合源码包里有完整src和WebContent目录的情况:新建一个空项目,然后把src目录标记为 Sources Root,把WebContent目录标记为 Web 资源目录。第二种方式是把整个解压目录当做一个已存在的项目直接 Open,IDEA 会自动识别目录结构。我推荐第二种,简单粗暴,不会漏文件。
关键步骤是配置 Artifact。打开 Project Structure(Ctrl+Alt+Shift+S),在 Artifacts 里点加号,选择 Web Application: Exploded,把WebContent目录设为 Web Resource Directory。这样 IDEA 才能把 JSP、静态资源、class 文件打包成可部署的结构,否则即使配置了 Tomcat 也不知道该往哪儿发请求。
然后配置 Tomcat Server。Run -> Edit Configurations,点加号选 Tomcat Server -> Local。在 Deployment 标签页把刚才建好的 Artifact 加进去,Application context 建议设为/,这样访问项目不需要加额外前缀,直接http://localhost:8080/index.jsp就能打开。如果不小心设成了/personnel,后面所有代码里的跳转路径都要带前缀,麻烦不说,还容易和代码里写死的路径冲突。
4.2 启动顺序与第一个请求的验证链路
启动前检查一遍:MySQL 是否启动、personnel库是否存在、Tomcat 端口是否被占用。这三件事里最容易踩的是端口占用,Tomcat 默认 8080,如果本机装了别的服务占了这个端口,IDEA 启动日志会直接报Address already in use。改端口在conf/server.xml里把<Connector port="8080">改成别的端口,或者在 IDEA 的 Tomcat 配置里改 HTTP port。
启动 Tomcat 后会看到类似下面的日志:
信息: Deployment of web application archive ... has finished 信息: Server startup in [5,342] milliseconds看到Server startup说明 Web 应用部署成功了。这时访问登录页面,拿一个真实用户名密码登录,然后用浏览器的开发者工具看网络请求:登录请求发出后,服务器返回的 Set-Cookie 里应该带JSESSIONID,后续的每个请求都会带上这个 Cookie。AuthorizedInterceptor的拦截逻辑就是靠这个 session id 来识别的。如果登录后跳回了登录页,八成是 Filter 校验失败;如果直接 404,八成是路径映射不对。这两个问题的排查方法,第 5 章会具体展开。
数据库连接这块,如果启动时报ClassNotFoundException,说明 MySQL 驱动 jar 没放到WEB-INF/lib下;如果报Communications link failure,说明数据库连接串有问题,先试telnet localhost 3306看 MySQL 端口通不通,再确认用户名密码。IDEA 里跑调试模式比命令行方便得多,在RainServiceImpl里某个查数据库的方法打个断点,F8 单步走到 JDBC 那一行,能直接看到 SQL 字符串拼出来是什么样,排查 SQL 语法问题特别好用。
5. 避坑指南:编码、404、驱动、Session 与中文乱码的五个现场
JavaWeb 项目的坑高度集中在几个固定位置。以下每一条都是真实场景里的高频问题,按「现象 -> 原因 -> 解决」记下来,下次遇到能直接对上。
5.1 中文乱码:从 JSP 到数据库全线崩溃
现象:页面上的员工姓名、部门名称显示成问号或乱码,数据库里存的也是乱码。
原因:乱码问题几乎都是字符集不一致导致的。JSP 页面用GBK编码,Tomcat 请求解析用ISO-8859-1,MySQL 表用utf8mb4,三处各玩各的,数据一趟走下来早就花了。最常见的一处是request.setCharacterEncoding("UTF-8")没写,POST 请求的中文参数从 Tomcat 默认的ISO-8859-1解码,进数据库前已经变乱码,神仙也救不回来。
解决:JSP 页面顶部加<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>,Servlet 里在读取任何请求参数前调用request.setCharacterEncoding("UTF-8"),JDBC 连接串里带上characterEncoding=utf8。这三处统一了,乱码基本绝迹。
5.2 Filter 拦截导致登录后循环跳转
现象:输入正确的用户名密码,浏览器地址栏一直在登录页和首页之间来回跳,或者卡在index.jsp上不动。
原因:AuthorizedInterceptor的过滤逻辑有问题。常见情况是过滤规则没把/login请求放行,登录动作本身也被拦截了;另一种情况是登录成功后session.setAttribute的 key 和 Filter 里检查的 key 不一致,比如登录代码写的是admin,Filter 里检查的是user,永远认证失败。
解决:打开AuthorizedInterceptor或对应的 Filter 类,先看 doFilter 里的放行逻辑:
// 典型的登录拦截逻辑,注意放行路径和 session key public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录页、登录接口和静态资源 if (uri.endsWith("login.jsp") || uri.endsWith("/login") || uri.contains("/css/") || uri.contains("/js/") || uri.contains("/images/")) { chain.doFilter(req, resp); return; } // 校验 session Object user = request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); }这段代码里最关键的是uri.endsWith("/login")这个放行条件,没有它登录请求就被拦截成死循环了。然后确认登录成功后写入 session 的 key 是不是user,如果不是,把 Filter 里的getAttribute("user")改成实际写入的 key。
5.3 MySQL 驱动版本引发Public Key Retrieval is not allowed
现象:启动时连接数据库报错,异常信息包含Public Key Retrieval is not allowed。
原因:MySQL 8.0 默认使用caching_sha2_password认证插件,连接时如果没有通过 SSL 加密,需要从服务器检索公钥,而 JDBC 驱动默认不允许这种检索行为。
解决:JDBC URL 里加一个参数:
allowPublicKeyRetrieval=true&useSSL=false加上之后重启 Tomcat 就能连上。注意useSSL=false要一并写上,否则 8.0 还会要求 SSL 证书验证,又引出另一串报错。如果这里踩坑了,直接把第 3 章里的连接串拿过去用就行。
5.4 请求 404:路径映射和物理路径对不上
现象:访问某个 URI 报 404,比如请求/employee/list找不到资源,但/index.jsp能正常打开。
原因:404 分两种。一种是 Tomcat 层面就没这个 Servlet 映射,web.xml或注解里没写/employee/list和EmployeeController的对应关系。另一种是映射存在,但代码里使用request.getRequestDispatcher("employee/list.jsp")跳转时,相对路径写错了,跳到了一个不存在的 JSP 文件。
解决:打开web.xml查 servlet-mapping,确认 URI 和 Servlet 的对应关系有没有问题。如果映射正常,就检查 JSP 跳转路径,建议把相对路径改成以/开头的绝对路径,前面加request.getContextPath()拼接:
// 绝对路径跳转,避免相对路径在不同层级页面下失效 response.sendRedirect(request.getContextPath() + "/employee/list");这种写法在任何目录层级下都不会错,代价只是代码稍微啰嗦一些。
5.5 Session 失效:过一会儿就要重新登录
现象:页面停了几分钟,再点一下链接就被踢回登录页。
原因:Tomcat 默认 session 超时时间是 30 分钟,这个值在web.xml里可以配。如果配了<session-config><session-timeout>10</session-timeout></session-config>,就是 10 分钟,低于 15 分钟在演示的时候很容易出现「讲到一半掉线」的尴尬。
解决:看一下web.xml里的 session-timeout 配置,没有配就保持默认,配了就改成 60:
<session-config> <session-timeout>60</session-timeout> </session-config>上面这段的意思是 session 最大空闲时间 60 分钟,单位是分钟。注意这个配置只管空闲时间,用户一直在操作时 session 会不断续期,不操作才会过期。
6. 从 Servlet 到 Spring Boot 的改造路线:映射对照与 DAO 替换
跑通源码只是第一步,想真正提升,把这套系统改造成 Spring Boot 是性价比最高的练手项目。改造不是重写,而是对照着现有代码逐个模块平移,好处是功能已经是验证过的,你只需要关心写法差异。我自己的经验是先别急着动数据库,把路由映射和服务层先平移完,能跑通流水线再优化。
6.1 路由映射对照表
| 原 Servlet/JSP 写法 | Spring Boot 写法 |
|---|---|
web.xml里配<servlet-mapping> | @RestController+@RequestMapping("/employee") |
HttpServletRequest/HttpServletResponse | @RequestParam/@PathVariable接收参数 |
request.getSession().getAttribute("user") | @SessionAttribute("user")注入 |
response.sendRedirect("/login.jsp") | 返回"redirect:/login" |
request.getRequestDispatcher("xxx.jsp").forward() | 返回视图名或ModelAndView |
这个表格是核心心智模型。例如EmployeeController里的doGet和doPost,在 Spring Boot 里合并成一个普通方法,用@GetMapping和@PostMapping区分请求方式。JDBC 这层改造最重,把RainServiceImpl里直接拼 SQL 的写法替换成 Spring 的JdbcTemplate:
// 改造前:原生 JDBC 写一大坨 try-catch-finally,可读性差 // 改造后:JdbcTemplate 一行查完,代码量直接缩到五分之一 @Autowired private JdbcTemplate jdbcTemplate; public List<Employee> getEmployeesByDeptId(Integer deptId) { String sql = "SELECT * FROM t_employee WHERE dept_id = ?"; return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Employee.class), deptId); }JdbcTemplate 帮我们搞定连接的获取和释放,BeanPropertyRowMapper自动把查询结果映射到实体类,前提是表字段名和实体类属性名能对应上,比如dept_id对应deptId。如果字段名有下划线,可以在 Spring Boot 配置文件里加spring.jpa.hibernate.naming.physical-strategy配置开启驼峰映射,或者直接用@Select注解写自定义 SQL。
Filter 的改造是另一个要留神的点。原来的AuthorizedInterceptor在 Spring Boot 里用一个HandlerInterceptor实现,注册方式也变了,但核心逻辑不用动,放行路径和 session key 直接复制过来改个写法就行。改造到这一步,你就会发现当初读源码时理解的那些业务规则全部变成了自己的东西。从那以后我每次拿到新的 JavaWeb 毕设源码,都会先跑通再改一版 Spring Boot,用这个流程把老项目的业务逻辑完整吃透,再去看 MyBatis 的分页插件、Redis 的 session 共享,才有落脚的根基。希望帮到你。
本文还有配套的精品资源,点击获取