
简介面向Java Web开发者和企业信息化人员这款基于SSHStruts2SpringHibernate与MySQL实现的企业人事人力资源管理系统可帮助企业实现员工档案、部门管理、职位变动、工资核算、福利计算、培训信息等全流程信息化管理有效解决手工方式下信息滞后、统计不准、劳动量大等问题。压缩包共127个文件、约16.9MB包含34个JAR依赖库、21个Java源文件、21个Class编译文件、20个XML配置、8个JSP页面及SQL初始化脚本等结构清晰适合直接导入工程学习或二次开发。系统功能已接近商用级别支持将统计结果导出为PDF或Excel可单个或批量导出源码中还包含登录拦截器、员工操作等核心模块并附带视频指导教程可学习如何修改版权信息。目前已有164人学习对想掌握SSH整合开发或快速搭建人事系统的开发者很有参考价值。1. 拿到 ssh 人事资源管理系统源码先判断这是不是你要的 JavaWeb 企业人事人力资源把“ssh人事资源管理系统源码.rar”解压后第一眼看到的往往不是 README而是一批 .class 文件EmployeeAction.class、UserInterceptor.class、BaseDaoImpl.class……这里一个容易误判断的坑是把它当成普通的 struts2 增删改查 demo。实际上这个 JavaWeb 企业人事人力资源项目技术栈是 struts2 spring hibernate简称 ssh覆盖员工档案、部门维护、职位变动、工资福利、培训管理还有把统计信息导出成 pdf、excel 的能力。如果是为了毕业设计、企业内部信息化改造或者找一套能改版的人事资源管理系统源码这套东西比 spring boot 版更好讲分层action 接请求service 管业务dao 碰数据库。适合一年以上 java web 经验的人做移植也适合学生拆开讲每一层的作用。2. SSH 整合链从 web.xml 到 applicationContext 的启动顺序与配置落点2.1 解压 rar 后先做结构盘点源码、配置、SQL 脚本分别在哪先不要急着用 IDE 打开我一般先把 rar 解压到纯英文路径比如 D:/hrms避免 Tomcat 和 JDBC 在中文路径下出现编码干扰。然后按三层结构去对号入座表示层看 struts 配置和 action业务层看 service 接口与实现持久层看 dao 和实体映射。这套人事资源管理系统源码里的类名已经能体现分层拿到手先建一张映射表后面改代码不用整个工程翻。分层典型类对应配置或资源表示层EmployeeAction, AdminAction, UserInterceptorstruts.xml, web.xml业务层EmployeeServiceImpl, AdminServiceImplapplicationContext.xml 中 service bean持久层BaseDaoImpl, EmployeeDaoImpl, DeptDaoImpl, AdminDaoImplHibernate SessionFactory 配置实体与数据Employee.class以及 dept、salary、training 相关实体mysql 初始化 sql 脚本如果解压后只有 .class 文件没有 .java要看是不是发布时只打了编译产物完整的人事资源管理系统源码包通常会附带源码目录、数据库脚本和视频教程。我最关心三个地方struts.xml 里的 action 映射applicationContext.xml 里的数据源以及 sql 脚本里的表结构。把这三处连起来项目就能在本地跑起来剩下的都是业务代码的搬运。2.2 web.xml 启动顺序谁先加载 Spring谁拦 action 请求SSH 项目启动时Tomcat 先读 web.xml再把 Spring 容器和 Struts2 过滤器装配起来。关键点在于Spring 容器必须在第一个 action 请求发生前已经创建好否则 struts2-spring-plugin 在去 applicationContext 里找 bean 时会报空指针。常见 web.xml 片段如下。?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.0 !-- Spring 容器位置可配多个 xml用逗号分隔 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- Struts2 核心过滤器2.3.x 是 ng.filter2.5.x 要去掉 ng -- filter filter-namestruts2/filter-name filter-classorg.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter/filter-class /filter filter-mapping filter-namestruts2/filter-name url-pattern*.action/url-pattern /filter-mapping /web-app代码里contextConfigLocation指定 Spring 的 XML 位置多个配置文件用逗号分隔比如classpath:applicationContext.xml,classpath:spring-*.xml。ContextLoaderListener是 Spring 提供的监听器负责在 Web 容器启动时创建 WebApplicationContext。filter-mapping的 URL 模式写成*.action静态资源和 jsp 不经过 Struts2 核心过滤器比直接写/*更稳如果写成/*图片和 css 都会被过滤器过一遍后面再配 Struts2 的静态资源放行逻辑属于自己给自己加需求。老项目经常会在这里踩版本坑struts2 2.3.x 里的过滤器类带ng2.5.x 把类移到了org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter。如果从一个 rar 里复制 lib 目录但 struts.xml 里的 DTD 是 2.5过滤器却还是 2.3启动时会出现ClassNotFoundException或者Unable to load configuration。我一般先看 lib 下 struts2-core 的版本号再决定 web.xml 和 struts.xml 的 schema 版本保证三者一致。2.3 Spring 管理 SessionFactory数据源、方言与事务实现这套系统后端是 ssh mysql所以 applicationContext.xml 里最核心的是 dataSource 和 sessionFactory。业务代码里到处用getHibernateTemplate()还是getSession()取决于配置里注入的是 HibernateTemplate 还是 SessionFactory。老一点的项目用 HibernateDaoSupport 居多新一点的项目直接注入SessionFactory再拿getCurrentSession()。bean iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource property namedriverClassName valuecom.mysql.jdbc.Driver / property nameurl valuejdbc:mysql://localhost:3306/hrms?characterEncodingutf8 / property nameusername valueroot / property namepassword value123456 / /bean bean idsessionFactory classorg.springframework.orm.hibernate5.LocalSessionFactoryBean property namedataSource refdataSource / property namepackagesToScan valuecom.hrms.entity / property namehibernateProperties props prop keyhibernate.dialectorg.hibernate.dialect.MySQL5Dialect/prop prop keyhibernate.show_sqltrue/prop prop keyhibernate.format_sqltrue/prop prop keyhibernate.jdbc.batch_size20/prop /props /property /bean bean idtransactionManager classorg.springframework.orm.hibernate5.HibernateTransactionManager property namesessionFactory refsessionFactory / /bean这里DriverManagerDataSource只适合学习和单机内网部署线上并发一高就会暴露连接不回收的问题我会换成 c3p0 或 Druid替换时只改 dataSource 的 class 和连接属性sessionFactory 不用动。packagesToScan告诉 Hibernate 扫描com.hrms.entity包下的Entity注解类使用 Hibernate 映射文件时这个属性要换成mappingLocations。hibernate.dialect影响 SQL 生成MySQL 5 用MySQL5DialectMySQL 8 建议用MySQL57Dialect或根据驱动版本调整避免时间类型和分页语法不对。提示不要把hibernate.hbm2ddl.auto默认打开。人事系统的表结构通常在 sql 脚本里维护自动建表在已有库上执行会出现表已存在或字段变更误操作问题。2.4 启动失败优先排查点jar 包、MySQL 驱动、拦截器注册现象可能原因处理方向Tomcat 启动报ClassNotFound: FilterDispatcherstruts2-core 版本与 web.xml 过滤器类不匹配更换过滤器类名或统一 jar 版本Spring 报NoSuchBeanDefinitionException: employeeActionstruts2-spring-plugin 缺失或 action 的 bean id 和 struts.xml 中 class 不一致在 applicationContext.xml 配置employeeAction并检查 struts.xml class 属性连接数据库报Access denied for usermysql 用户或密码不对或 root 只允许 localhost用命令行mysql -uroot -p先验证账号再改配置Hibernate 查询出现Unknown column ... in where clause数据库脚本与实体类字段不一致以 sql 脚本为准调整实体类Column(name...)映射排查启动问题不要只看最后一行异常。SSH 的异常经常是连环报前面一行Caused by才是真正原因。比如Unable to load configuration后面跟着java.lang.NoClassDefFoundError: ognl/PropertyAccessor说明 ognl 和 struts2-core 版本冲突换到同一个 struts2 发行版的 lib 目录即可。3. Employee 实体、BaseDao 泛型与员工档案的持久层设计3.1 Employee 实体与 employee 表人事字段怎么映射人事资源管理系统的核心表是 employee几乎每个功能都在围绕这个实体做文章。员工档案、部门调动、工资福利、培训记录都以 employee 为主表或外键关联表。新项目习惯用注解老项目常配Employee.hbm.xml。无论如何字段命名最好和表单组件、数据库列名一一对应减少 Hibernate 映射时的隐性错误。字段类型说明idint主键自增emp_novarchar(20)工号员工界面常用查询条件namevarchar(50)姓名dept_idint外键关联部门表position_namevarchar(50)职位岗位职位变动时更新base_salarydecimal(10,2)基本工资welfare_ratedecimal(5,2)福利比例或补贴系数hire_datedate入职时间statusint在职/离职状态下面是一段简化后的实体映射重点不是写全字段而是展示ManyToOne关系Entity Table(name employee) public class Employee { Id GeneratedValue(strategy GenerationType.IDENTITY) private Integer id; Column(name emp_no, length 20, unique true) private String empNo; Column(name name, length 50) private String name; ManyToOne(fetch FetchType.LAZY) JoinColumn(name dept_id) private Dept dept; Column(name base_salary, precision 10, scale 2) private Double baseSalary; Column(name hire_date) private Date hireDate; Column(name status) private Integer status; // 省略 getter/setter }ManyToOne(fetch FetchType.LAZY)表示多个员工对应一个部门查询员工列表时默认不马上把部门数据加载出来避免无谓的 join。如果 action 返回给 jsp 时已经不在 Hibernate 会话里取employee.getDept().getDeptName()会有懒加载异常常见解决办法是关联抓取或者把部门名称冗余到 employee 表。看懂 Employee.class 后再去看 dept、salary、training 表就顺了。3.2 BaseDao 泛型把 CRUD 抽成一套模板项目正文里的 BaseDaoImpl 负责把公共方法沉下去。常见设计是一个接口BaseDaoT提供save、update、delete、findById、findAll子接口继承后自动拥有这些方法。这样 EmployeeDao、DeptDao、AdminDao 都只写自己的查询逻辑不需要重复造轮子。public class BaseDaoImplT implements BaseDaoT { Autowired private SessionFactory sessionFactory; private ClassT clazz; public BaseDaoImpl() { // 通过泛型反射拿到运行时实际类型比如 EmployeeDaoImpl 里的 Employee ParameterizedType type (ParameterizedType) getClass().getGenericSuperclass(); this.clazz (ClassT) type.getActualTypeArguments()[0]; } protected Session getSession() { return sessionFactory.getCurrentSession(); } Override public void save(T entity) { getSession().save(entity); } Override public T findById(Integer id) { return getSession().get(clazz, id); } Override SuppressWarnings(unchecked) public ListT findAll() { String hql from clazz.getName(); return getSession().createQuery(hql).list(); } }代码里ParameterizedType反射这一行是为了在子类不写构造器的情况下拿到真实的泛型类型否则getSession().get(clazz, id)不知道要查哪张表。getCurrentSession()由 Spring 的事务管理器统一控制事务开启时获取会话事务提交时自动关闭手动去 close 反而会打乱生命周期。createQuery(hql).list()是 Hibernate 5 的写法旧项目如果用的find(hql)列表那就是 HibernateTemplate原理一致只是 API 不同。3.3 EmployeeDaoImpl 的条件分页工号、姓名、部门怎么拼查询在人力资源管理系统里员工列表页几乎都带多个筛选条件。最容易写错的是把外部参数直接拼进 HQL导致 or 11类型的注入风险。正确做法是用setParameter绑定参数。public MapString, Object findPage(String empNo, String name, Integer deptId, int page, int rows) { StringBuilder hql new StringBuilder(from Employee e where 11 ); StringBuilder countHql new StringBuilder(select count(*) from Employee e where 11 ); MapString, Object params new HashMap(); if (empNo ! null !empNo.trim().isEmpty()) { hql.append(and e.empNo like :empNo ); countHql.append(and e.empNo like :empNo ); params.put(empNo, % empNo %); } if (deptId ! null) { hql.append(and e.dept.id :deptId ); countHql.append(and e.dept.id :deptId ); params.put(deptId, deptId); } Query query getSession().createQuery(hql.toString()); Query countQuery getSession().createQuery(countHql.toString()); for (Map.Entry entry : params.entrySet()) { query.setParameter((String) entry.getKey(), entry.getValue()); countQuery.setParameter((String) entry.getKey(), entry.getValue()); } Number total (Number) countQuery.uniqueResult(); query.setFirstResult((page - 1) * rows).setMaxResults(rows); MapString, Object result new HashMap(); result.put(total, total); result.put(rows, query.list()); return result; }这里的where 11不是无意义的句式是为了后续条件都能直接拼and避免每次判断是否首个条件。统计总行数和查列表必须用两条 HQL不能先查 list 再取 size因为setMaxResults会截断数据总行数就不对了。page和rows来自前端表格组件page 从 1 开始rows 是每页行数底层的setFirstResult((page - 1) * rows)做偏移计算。如果分页后不带排序结果不稳定简单补一个order by e.id。3.4 工资福利的统计查询少在 Java 里做循环求和工资统计是人事资源管理系统里最容易被人诟病性能的地方。有些项目会把员工列表全部加载到内存再用 for 循环累加数据量到一万以上就明显卡顿。HQL 里也能做分组聚合直接在数据库计算只把结果传给页面。String hql select new map(e.dept.deptName as deptName, sum(e.baseSalary) as salaryTotal, avg(e.baseSalary) as salaryAvg) from Employee e where e.status 1 group by e.dept.deptName; ListMapString, Object list getSession().createQuery(hql).list();new map让 Hibernate 把每一行结果包装成Mapkey 是 as 后面的别名。这样做的好处是 service 层不需要为统计结果额外创建一个 VO 类jsp 用map.deptName也能直接取值。需要注意的是 HQL 的 group by 中尽量用实体的属性路径e.dept.deptName不要用deptId否则 Hibernate 可能会生成多余的字段导致分组结果和预期不一致。这里还有一个容易踩的细节如果要统计福利比例可以先在实体里把welfareRate字段设计成小数再用sum(e.baseSalary * e.welfareRate)求出福利总额而不必在 java 里逐个乘。分析报表时把运算下推到数据库就是要让人事薪资这种数据量大的模块跑得动。4. AdminAction、EmployeeAction 与 UserInterceptor 的请求流转和权限控制4.1 struts.xml 的 action 映射method 参数与 stream 结果Struts2 的分层思路是 URL - Filter - Action - Service - Dao。Action 类里通常是一个方法对应一个操作比如EmployeeAction.list()、save()、delete()、export()。struts.xml 用通配符把不同的 action 方法映射出来。package namehrms namespace/ extendsstruts-default action nameemployee_* classemployeeAction method{1} result namesuccess/WEB-INF/pages/employee_list.jsp/result result nameredirect_list typeredirectActionemployee_list/result result nameexport_excel typestream param namecontentTypeapplication/vnd.ms-excel/param param namecontentDispositionattachment;filenameemployee_export.xls/param param nameinputNameexportStream/param /result /action /packagenameemployee_*里的*被method{1}引用比如请求employee_list.action时执行list()请求employee_export.action时执行export()。classemployeeAction不是类的完整限定名而是 Spring 容器里的 bean id。抄代码时如果 action 类改过包名一定要回 applicationContext.xml 同步修改否则 struts2-spring-plugin 只会报一个 “bean id 不存在” 的错。action 名方法返回结果用途employee_listlist()success分页显示员工档案employee_savesave()redirect_list新增或修改员工employee_exportexport()export_excel导出 excel 报表admin_loginlogin()success管理员登录export_excel的 result type 是stream它不走 JSP 转发而是把 action 中某个 getter 返回的输入流直接写到响应输出流。inputName必须对应 action 的getExportStream()方法这里不是自由的字符串而是一个 bean 属性名写错了浏览器只会收到 0 字节文件。4.2 UserInterceptor登录态不放行方法名决定跳过哪些请求UserInterceptor 是整个系统安全的第一道闸。它拦截 action 请求后先看会话里有没有登录用户如果没有就跳到登录页。这里最容易踩的坑是登录请求也会被拦截所以必须做方法名排除。public class UserInterceptor extends AbstractInterceptor { private SetString excludeMethods new HashSet(); { excludeMethods.add(login); } Override public String intercept(ActionInvocation invocation) throws Exception { ActionProxy proxy invocation.getProxy(); String method proxy.getMethod(); // 允许登录动作直接通过 if (excludeMethods.contains(method)) { return invocation.invoke(); } MapString, Object session invocation.getInvocationContext().getSession(); Object loginUser session.get(loginUser); if (loginUser null) { // 全局 result 在 struts.xml 中配置成登录页面 return loginRedirect; } return invocation.invoke(); } }getMethod()拿到的是当前 action 的方法名也就是 struts.xml 里{1}匹配到的值。如果登录方法叫doLogin排除列表就要写doLogin而不是只写login。不要把所有以admin_开头的 action 都放行更不要只判断excludeMethods后就不调invocation.invoke()那样 action 永远执行不了。会话里存对象的 key 最好用常量比如UserConstants.LOGIN_USER避免页面和拦截器各写各的字符串导致错位。提示如果拦截器写在 Struts 的拦截器栈里要确认默认的defaultStack被放在前面。Struts2 默认拦截器栈负责参数绑定、文件上传、验证等动作直接把自己的拦截器放到首位会让很多功能失效。4.3 部门变动与职位调整的事务边界在 service 层AdminServiceImpl 里最常见的一个场景是调整员工部门。如果只employeeDao.update(employee)职位变动记录却没有写入库一段时间后人事盘点就对不上账。Spring 的事务管理就是把多条 dao 操作打包成一个事务。Service Transactional public class AdminServiceImpl implements AdminService { Resource private EmployeeDao employeeDao; Resource private DeptDao deptDao; Resource private PositionChangeDao positionChangeDao; Override public void changeDept(Integer empId, Integer newDeptId) { Employee emp employeeDao.findById(empId); Dept newDept deptDao.findById(newDeptId); PositionChange change new PositionChange(); change.setEmpId(empId); change.setOldPosition(emp.getPositionName()); change.setNewPosition(newDept.getDeptName()); positionChangeDao.save(change); emp.setDept(newDept); employeeDao.update(emp); } }Transactional的作用是把positionChangeDao.save()和employeeDao.update()放到同一个数据库事务中。前面那句如果成功后面那句失败事务会整体回滚不会出现“变动记录写了员工部门没改”的脏数据。注意Transactional放在 service 实现类上或接口上都可以但更推荐放实现类因为代理机制对实现类方法更直接。如果 action 里自己也做业务查询事务只负责 service 方法这一段不负责 action 中其他逻辑。4.4 JSP 用 struts 标签对接 action 数据与表单参数登录后的员工列表页通常是一张 table用s:iterator遍历 action 返回的 List。这里的list是 action 里的属性名struts2 会把 result 中的栈顶对象属性暴露给页面。s:iterator valuepageResult.rows varemp statuss tr tds:property value#emp.empNo //td tds:property value#emp.name //td tds:property value#emp.dept.deptName //td tds:property value#emp.baseSalary //td /tr /s:iterator#emp是迭代变量的引用写法取的是迭代对象的属性。表单提交时name 属性最好保持empNo、deptId这样的形式Struts2 的 params 拦截器会自动把请求参数绑定到 action 的同名属性或 getter/setter 上。如果 action 实现了 ModelDriven表单 name 可以写成emp.empNo的形式模型对象会被自动压入栈顶。这个差异在续写代码时容易混淆先分清当前 action 是普通属性模式还是 ModelDriven 模式再去接表单。5. 报表导出到 PDF/Excel 与上线前的小改造5.1 用 Apache POI 生成员工工资 Excel导出功能在人事资源管理系统中属于高频需求。常见做法是 action 里生成 workbook再通过 struts 的 stream 结果输出。不要自己去response.getOutputStream()写文件那样很难和 struts 的结果类型配合。public String export() throws Exception { ListEmployee employees employeeService.findAll(); try (HSSFWorkbook workbook new HSSFWorkbook()) { HSSFSheet sheet workbook.createSheet(员工工资); HSSFRow header sheet.createRow(0); header.createCell(0).setCellValue(工号); header.createCell(1).setCellValue(姓名); header.createCell(2).setCellValue(部门); header.createCell(3).setCellValue(基本工资); for (int i 0; i employees.size(); i) { Employee emp employees.get(i); HSSFRow row sheet.createRow(i 1); row.createCell(0).setCellValue(emp.getEmpNo()); row.createCell(1).setCellValue(emp.getName()); row.createCell(2).setCellValue(emp.getDept() null ? : emp.getDept().getDeptName()); row.createCell(3).setCellValue(emp.getBaseSalary() null ? 0 : emp.getBaseSalary()); } ByteArrayOutputStream out new ByteArrayOutputStream(); workbook.write(out); exportStream new ByteArrayInputStream(out.toByteArray()); } return export_excel; }exportStream是 action 中的一个字段必须有 getter与 struts.xml 里的inputName对应。用HSSFWorkbook生成的是 xls 格式文件较小兼容老版 excel如果导出量大换成XSSFWorkbook写 xlsx。最关键的是文件名编码如果 struts.xml 的contentDisposition写死成中文浏览器会乱码我一般传一个exportFileName参数在 action 里new String(fileName.getBytes(UTF-8), ISO-8859-1)转码后再放到contentDisposition。5.2 PDF 导出iText 还是打印样式PDF 导出在旧 SSH 项目里常见两个方向。一是用 iText 直接生成 pdf表格样式靠代码绘制灵活性高但改动成本高二是用 HTML 拼接报表再用工具转 pdf。如果这套人事资源管理系统源码没有引入 pdf 库尽量不要临时加大而全的开源组件先用浏览器打印功能导出或者用现成 excel 转 pdf避免给部署环境增加一堆 jar 包依赖。等确认业务方真的需要固定 pdf 版式再在导出方法里按报表模板重写。5.3 修改版权信息、默认密码与部署验证源码包里有些人会把登录页和页脚版权信息写死在 JSP 里有些写在 common 常量类中。改版权时不要只搜 jsp项目中可能还有 head.jsp、left.jsp、footer.jsp 这类公共片段。我用一个名字叫common/的目录统一放公共页脚只要改动一行所有页面同步生效。部署验证按这个顺序检查先导入 mysql 的 sql 脚本修改applicationContext.xml中的数据库账号密码再启动 Tomcat浏览器输入http://localhost:8080/hrms/login.action用默认管理员账号登录如果提示登录失败去数据库admin表重置密码最后点一次员工导出和部门列表确认 stream 结果不是 0 字节。这一套走通后再去看AdminAction和EmployeeAction中具体的业务方法改起来才有坐标系。本文还有配套的精品资源点击获取