简介:一份基于Java的企业员工信息管理系统设计与实现文档,适合计算机相关专业的学生用于毕业设计、课程设计参考,也可为中小企业员工管理信息化改造提供方案借鉴。系统采用管理员与普通员工双角色设计,管理员可完成部门管理、员工信息管理、出勤管理、工资管理和请假审核,普通员工仅限查看工资、提交请假,权限边界清晰,安全性考虑到位。内容按照需求分析、系统概要设计、系统功能实现和系统测试的主线展开,完整展示了B/S架构下JSP前台、MySQL数据库和Tomcat服务器的实际落地路径。除功能模块说明外,还包含国内外现状对比、可行性分析、框架结构设计和测试验证,能够帮助读者快速理解企业信息管理系统的开发流程与设计要点。资源包压缩后为1个docx文档,共369KB,文件精炼、便于阅读。目前已有500人学习下载,适合需要快速梳理同类系统开发脉络或撰写相关论文的读者。
1. 员工信息管理系统,远不止一个CRUD增删改查
很多人第一次做Java课程设计,都会在“员工信息管理系统”上栽跟头:以为就是员工表、部门表两张表,把增加、删除、修改、查询四个接口写完就收工。等真把工程搭起来才发现,登录会话、权限隔离、分页检索、级联删除、Excel导出,每一个点都能让页面和逻辑在答辩现场翻车。这个系统几乎是java面试题里“你做过什么项目”的标准答案之一,也是java学习路线从语法过渡到Web工程的必经一站。下文按我做这类系统的顺序来拆:从需求到表结构,从框架搭建到部署排错,最后补一个能拿得出手的导出功能。适合要交课程设计、毕业设计的同学,也适合第一次接触Spring Boot项目、想看完整落地路径的从业者。
2. 设计先行:员工信息管理系统的三张核心表与权限模型
2.1 技术选型:为什么Spring Boot + MyBatis + MySQL是课程设计的主流
这类系统最常见的落地组合是Spring Boot + MyBatis + MySQL + Thymeleaf或Layui。有人会问近两年的课程设计是不是都改成Spring Boot了?对,SSM(Spring + SpringMVC + MyBatis)在5年前是主流,现在新项目基本都用Spring Boot,因为自动配置和内置Tomcat能省掉大量XML配置,对只有两周开发时间的课设来说,少一份配置就少一个坑。MyBatis比JPA更合适的一点在于,课程设计答辩老师很喜欢问“你这几条SQL怎么写的”,MyBatis的Mapper XML里SQL是可见可改的,JPA自动生成的SQL反而让很多同学答不上来。MyBatis-Plus可以装,但建议先把原生动态SQL写熟再让插件代劳。
前端选型上,我一般不引入Vue,因为员工信息管理系统的页面密度不高,用Bootstrap或Layui加Thymeleaf模板渲染,一个后端同学就能维护。真要上Vue加Axios,意味着要处理跨域、前端路由、Token存储,对课设来讲往往是负担。选技术栈只要记住一句话:能用一个工程搞定的,别拆成前后端两个工程。下表是我的常用选择,替换方案也列在后面。
| 技术点 | 推荐选择 | 选择理由 | 常见替代 |
|---|---|---|---|
| 基础框架 | Spring Boot 2.7.x | 自动配置,省XML,网上案例最多 | SSM(配置多,已偏老) |
| ORM | MyBatis | SQL可控,答辩能讲清楚 | MyBatis-Plus、JPA |
| 前端 | Thymeleaf + Layui | 无需跨域,一个工程搞定 | Vue + Axios |
| 数据库 | MySQL 5.7 / 8.0 | 文档多,导出和部署都方便 | H2、SQLite |
版本这里单独提醒一句:Spring Boot 3.x把javax.servlet整套改成了jakarta.servlet,很多网上的旧代码直接拷过来会编译失败。如果导师的指导书是基于2.x的,或者你主要靠搜索解决问题,建议就用2.7.x,把坑留在业务代码上,而不是花一周跟命名空间较劲。
2.2 员工表、部门表、用户表:字段设计与建表SQL
表结构是这个系统最不应该偷懒的地方。很多课设源码把逻辑都写在Java里,表却只有三四个字段,答辩时一被追问就露馅。我的做法是至少拆出三张表:部门表、员工表、登录用户表。员工档案和登录账号分开存,是因为两者生命周期不同——员工离职后档案要保留,但登录账号要停用。
CREATE DATABASE IF NOT EXISTS employee_db DEFAULT CHARSET utf8mb4; USE employee_db; CREATE TABLE department ( id BIGINT AUTO_INCREMENT PRIMARY KEY, dept_name VARCHAR(50) NOT NULL COMMENT '部门名称', manager_id BIGINT DEFAULT NULL COMMENT '部门负责人员工ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_dept_name (dept_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表'; CREATE TABLE employee ( id BIGINT AUTO_INCREMENT PRIMARY KEY, emp_no VARCHAR(20) NOT NULL COMMENT '工号,业务唯一键', name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 1 COMMENT '1-男 2-女', birth_date DATE DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, email VARCHAR(100) DEFAULT NULL, department_id BIGINT NOT NULL COMMENT '部门ID', position VARCHAR(50) DEFAULT NULL COMMENT '岗位', entry_date DATE DEFAULT NULL COMMENT '入职日期', status TINYINT DEFAULT 1 COMMENT '1-在职 0-离职', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_emp_no (emp_no), KEY idx_department_id (department_id), CONSTRAINT fk_emp_dept FOREIGN KEY (department_id) REFERENCES department(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表'; CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, employee_id BIGINT NOT NULL COMMENT '关联员工ID', username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL COMMENT 'BCrypt或MD5加盐存储', role TINYINT DEFAULT 0 COMMENT '1-管理员 0-普通员工', status TINYINT DEFAULT 1 COMMENT '1-启用 0-禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username), UNIQUE KEY uk_employee_id (employee_id), CONSTRAINT fk_user_emp FOREIGN KEY (employee_id) REFERENCES employee(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录用户表';几个字段设计的细节值得说明。emp_no工号用VARCHAR而不是BIGINT,因为工号未来可能带前缀、带前导零,用数字类型会丢掉这些信息,所以业务编号一律按字符串处理。gender、status这类枚举状态用TINYINT而不是字符串,查询条件里写status = 1比写status = 'active'省空间也省比较开销。部门表里的manager_id,意思是部门负责人指向员工表,这在后面做“部门经理看本部门”的权限扩展时会很有用。
表的字符集统一用utf8mb4而不是utf8。MySQL里的utf8实际是utf8mb3,存不了emoji和部分生僻字,一旦简历里有个特殊字符,插入就报错。外键方面,employee.department_id设置了外键约束,这会带来一个第5章要讲的删除坑,这里先记住:外键是双刃剑,保证数据完整性靠它,限制删除操作也靠它。
2.3 权限模型:普通员工和管理员的数据范围怎么控制
登录用户表里有role字段,但权限控制不能靠页面隐藏按钮来实现。这个系统的典型权限需求是:普通员工登录后只能查看和修改自己的基本资料,管理员能看全部员工、维护部门、管理登录账号。这是行级权限——同一张employee表,不同角色返回不同行。新手的常见错误是在controller里到处写if(admin)判断,十个接口复制十次,后面改需求时漏改一处就出数据越权。
我一般把权限收敛在service层。登录成功后把当前用户信息放进Session,再用一个ThreadLocal工具类在请求链路内传递,service层在构造查询条件时按角色自动注入限制条件。
public class LoginUser { private Long userId; private Long employeeId; private Integer role; // 1-管理员 0-普通员工 } public class LoginUserHolder { private static final ThreadLocal<LoginUser> HOLDER = new ThreadLocal<>(); public static void set(LoginUser user) { HOLDER.set(user); } public static LoginUser get() { return HOLDER.get(); } public static boolean isAdmin() { return HOLDER.get() != null && HOLDER.get().getRole() == 1; } public static void clear() { HOLDER.remove(); } } @Service public class EmployeeServiceImpl { public PageResult<EmployeeVO> page(EmployeeQuery query) { // 普通员工只能查自己的档案,管理员查全部 if (!LoginUserHolder.isAdmin()) { query.setEmployeeId(LoginUserHolder.get().getEmployeeId()); } return employeeMapper.selectPage(query); } }注意ThreadLocal用完一定要remove,否则Web容器线程池复用线程时,下一个请求会读到上一个人的登录信息,这是典型的“串号”事故。我会在拦截器的afterCompletion里调用LoginUserHolder.clear()。面试时能把这个链路讲清楚——登录塞Session、拦截器取Session、ThreadLocal传参、结束后清理——比背八股文里的“事务传播行为”更能证明你写过真实系统。之所以不用Spring Security,是因为课设阶段手写Session加拦截器足够,还不用引入一整套过滤链和配置类,等真需要OAuth2那套再换不迟。
3. 让系统跑起来:Spring Boot工程搭建与登录、分页检索的核心代码
3.1 先搭Maven骨架:依赖和配置文件一次到位
工程骨架我习惯用IDEA的Spring Initializr生成,然后手工改pom.xml。关键依赖就四个:spring-boot-starter-web、mybatis-spring-boot-starter、MySQL驱动、lombok。不要一上来加一堆spring-security、redis、swagger,依赖越多,启动报错的可能性越大,课设阶段做减法比做加法重要。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <!-- Web容器:内置Tomcat,同时包含SpringMVC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis与Spring Boot整合的官方starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <!-- MySQL 8.x驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok:编译期生成getter/setter,减少样板代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>配套的application.yml也有几个参数需要一次配到位,不然后面全是编码和时间问题:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/employee_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你的数据库密码 hikari: maximum-pool-size: 10 minimum-idle: 2 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.ems.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这段配置里最容易被忽略的是URL上的三个参数:characterEncoding=utf8管中文写入读取;serverTimezone=Asia/Shanghai管时间差;allowPublicKeyRetrieval=true针对MySQL 8.0的caching_sha2_password认证,不加会在连接阶段直接报Public Key Retrieval is not allowed。map-underscore-to-camel-case是MyBatis的自动驼峰映射,开启后数据库的department_id自动映射到Java属性的departmentId,省掉大量resultMap。log-impl在开发期打印SQL,看参数有没有传错全靠它。
3.2 登录验证与Session拦截器:手写安全过滤的完整逻辑
登录模块我不建议上Spring Security,但密码绝对不允许明文存库。课程设计阶段最常见的做法是MD5加盐或者BCrypt,我偏向后者的BCryptPasswordEncoder,因为MD5加盐很多同学加着加着就把盐写死成固定字符串,等于没加。登录接口本身很简单:
@PostMapping("/login") public String login(String username, String password, HttpSession session, Model model) { SysUser sysUser = userService.login(username, password); if (sysUser == null) { model.addAttribute("error", "用户名或密码错误"); return "login"; } // 只放必要字段,别把密码放进Session LoginUser loginUser = new LoginUser(); loginUser.setUserId(sysUser.getId()); loginUser.setEmployeeId(sysUser.getEmployeeId()); loginUser.setRole(sysUser.getRole()); session.setAttribute("loginUser", loginUser); LoginUserHolder.set(loginUser); return "redirect:/employee/list"; }登录验证完成后,最关键的一步是拦掉未登录请求。我用一个HandlerInterceptor,比Filter更贴合SpringMVC的路由规则:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }拦截器注册要用WebMvcConfigurer,注意把登录页、注册页、静态资源放行:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/logout", "/css/**", "/js/**", "/images/**"); } }这里有个常被忽略的细节:退出登录不能只清Session里的attribute,要调用session.invalidate()让整个会话失效,否则浏览器里的Session变量还可能被读到残留值。我还会在退出后对登录页设置Cache-Control: no-store,防止用户点后退直接看到上一个已登录页面。
3.3 员工分页与多条件检索:MyBatis动态SQL怎么写
员工列表是使用频率最高的页面,不能一次查全表,也不能只会按名字精确查。我的做法是一个员工查询对象EmployeeQuery承载所有检索条件,Mapper里用动态SQL拼条件。查询对象上带分页号,也带行级权限参数:
@Data public class EmployeeQuery { private Integer pageNum = 1; private Integer pageSize = 10; private String keyword; // 姓名或工号模糊 private Long departmentId; private Integer status; private Long employeeId; // 行级权限注入用 }Mapper XML是核心,这里的标签顺序有点讲究,where里的条件都靠<if>判断:
<select id="selectPage" resultType="com.example.ems.entity.Employee"> SELECT id, emp_no, name, gender, phone, email, department_id, position, entry_date, status FROM employee <where> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR emp_no LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="departmentId != null"> AND department_id = #{departmentId} </if> <if test="status != null"> AND status = #{status} </if> <if test="employeeId != null"> AND id = #{employeeId} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>拼接模糊查询在这里有个安全知识点:用CONCAT('%', #{keyword}, '%')而不是'%${keyword}%'。后者是字符串拼接,keyword里带个单引号就能把SQL断掉,这是典型的注入点。#{}在MyBatis里是预编译占位符,会生成?参数再传值,从根上挡住注入。
动态SQL里还有个细节是<where>标签而不是手动写WHERE 1=1。<where>会自动去掉满足条件的第一个AND,条件都没传的时候SQL就是干净的SELECT ... FROM employee。LIMIT的参数我建议在查询对象里加一个getOffset()方法返回(pageNum - 1) * pageSize,语义清晰,也不怕传null。每条SQL都要能让答辩老师问“怎么分页的”“怎么防注入的”,这两问是这道题最高频的追问。
4. 从开发机到服务器:员工信息管理系统的打包、部署与手工测试路径
4.1 application.yml 的参数陷阱:时区、编码、连接池
第3章给的application.yml是能跑通的底线配置,但实际部署时还会遇到几个症状导向的问题。如果页面中文正常,写入数据库后却变成乱码,先不要急着改代码,按这个顺序排查:先用数据库客户端工具,比如Navicat或命令行直接插入一条中文记录,看库里能不能正常存。库里乱,说明表和字段字符集不对,检查建表语句是不是utf8mb4;库里正常但页面乱,再看连接URL有没有characterEncoding=utf8;如果页面和库里都正常,只有导出的Excel乱,那就是导出代码里没设置编码,属于第6章的问题。这条链路把乱码问题的三个层切开了:数据库、连接、前端模板。
时间差8小时的问题也是部署时的高频现象。MySQL 8.0驱动在连接时如果URL上不带serverTimezone,会用JVM和数据库的默认时区做换算,国内机器通常是CST这个多义时区,一会是UTC+8一会是UTC-5,导致同一行数据查出来早8小时或晚8小时。连接参数加serverTimezone=Asia/Shanghai能解决绝大多数情况。注意改了连接参数后一定要重启应用,光刷新页面没用,连接池里的旧连接还带着旧时区。
连接池参数值得提前想清楚。HikariCP在Spring Boot 2.x里是默认连接池,maximum-pool-size: 10对课设系统刚好,但如果服务器只有1G内存,10个连接加上Tomcat线程和JVM堆,容易出现内存紧张。我会在低配服务器上把连接池改到5,最大等待时间调低一些,这样一旦数据库有问题,请求会快速失败返回错误页,而不是集体卡死。
4.2 jar包与war包部署:两种方式的取舍和命令
Spring Boot应用打包就两种产物:jar和war。jar是Spring Boot推荐方式,内置Tomcat,服务器只要有JDK就能跑。我一般在服务器上这样部署:
mvn clean package -DskipTests nohup java -jar target/ems-0.0.1-SNAPSHOT.jar > app.log 2>&1 &nohup ... &是让应用后台运行,日志写到app.log。新部署先看日志再谈访问,tail -f app.log看到Started Application才算启动成功。如果服务器上java命令报错找不到,先执行java -version确认JDK装没装、环境变量配置对不对,常见问题是安装了JDK但PATH里没配上,或者装的是JRE而不是JDK,导致缺少编译相关组件。
war包是传统Java Web应用的部署方式。如果课设硬性要求“把项目放到Tomcat的webapps目录下运行”,那就打war。做法分两步:pom.xml里把packaging改成war,并把spring-boot-starter-tomcat的scope改成provided;启动类继承SpringBootServletInitializer并重写configure方法。打出来的war丢进Tomcat的webapps,启动Tomcat后自动解压部署。jar比war省事,但war更接近老教材的部署路径,两种方式都该在答辩前至少亲手跑一次。
还有一种常见的翻车是端口冲突。服务器上已经跑了别的服务占用8080,Spring Boot启动直接报Port already in use。这时要么改server.port,要么用netstat -tlnp找到占用进程。这个坑基本每台第一次部署的机器都会踩一次,属于典型的“环境问题比代码问题更早出现”。
4.3 上线前的验收清单:照着测一轮,避免答辩现场翻车
部署完不能直接演示,我习惯按下面这份清单手工过一遍,每一项都设置了预期结果。这份清单拿去给同学互测也适用,每项都对应一个真实存在的功能或安全点。
- 未登录访问
/employee/list,预期是跳转登录页,而不是报500或白屏。这条验证拦截器生效。 - 连续三次输错密码,预期是页面提示错误,系统不崩溃。这条验证登录失败路径。
- 新增员工时录入重复工号,预期页面提示“工号已存在”,而不是弹出SQL异常堆栈。这条验证唯一索引和异常捕获。
- 删除一个还有员工在岗的部门,预期是友好提示无法删除。这条验证外键约束的兜底逻辑。
- 用普通员工账号访问管理员接口
/admin/user,预期是拒绝访问,而不是把所有用户列表展示出来。这条验证行级权限和角色判断。 - 在搜索框输入单引号
',预期是返回空列表或正常结果,而不报SQL语法错误。这条验证SQL注入防护。 - 登录后点退出,再点浏览器后退,预期无法回到上一个已登录页面。这条验证Session失效和页面缓存控制。
- 导出员工名册,数据量在几千行时内存占用平稳。这条验证导出模块的流式处理能力。
每一条我都见过对应的翻车现场。第2条翻车的项目是登录失败直接抛异常给500页面;第6条翻车的项目是SQL里用了字符串拼条件,输入单引号后在答辩现场爆出完整SQL语句。这些细节不需要多高深的技术,但能决定评审老师对你的专业印象。
5. 员工信息管理系统的五个常见问题:现象、排查顺序与修复方案
5.1 中文乱码“锟斤拷”:三处编码要一起改
现象:页面显示锟斤拷或者问号,数据库里存的也是乱码。原因不在某一个地方,而是编码链路断了一环。排查顺序固定:先看数据库表字符集,SHOW TABLE STATUS LIKE 'employee'看Collation是不是utf8mb4;再用数据库工具直接插入中文看能否正常;然后看JDBC的URL有没有characterEncoding=utf8;最后看前端模板的charset声明。最常见的原因其实是建表时用了latin1,或者数据库连接池里的连接用了旧编码。解决方式就是第2章的建表SQL统一用utf8mb4,JDBC URL统一带characterEncoding=utf8,IDE的File Encoding全部设成UTF-8。
5.2 数据库时间差8小时:连接参数上的一个变量
现象:前端展示的入职时间、创建时间比真实时间早8小时。原因:MySQL驱动在连接时使用服务器时区还是客户端时区没对齐。解决:JDBC URL上加serverTimezone=Asia/Shanghai。如果加了还不对,检查服务器系统时区,执行date命令看系统时间;生产环境系统时区是UTC时,即使加了Asia/Shanghai,某些旧驱动版本也可能不认,这时候把系统时区改成Asia/Shanghai再重启MySQL和Java应用,是双保险。改完记得把连接池的旧连接断开,重启应用是最省事的办法。
5.3 删除部门提示外键约束失败:先处理员工再删部门
现象:删除部门时抛异常Cannot delete or update a parent row: a foreign key constraint fails。原因:employee表里还有员工引用这个部门的department_id。解决:先查询该部门下员工数量,如果有员工,页面明确提示“该部门下存在员工,请先调整员工部门”;没有员工再执行删除。如果业务上允许部门删除后员工保留,那建表时department_id就不能是NOT NULL,删除流程变成先执行UPDATE employee SET department_id = NULL WHERE department_id = ?,再删除部门,两步包在同一个事务里。我不建议使用ON DELETE CASCADE,员工档案是重要数据,级联删除会把员工整行抹掉,这是不可恢复的,设计评审时也会被老师质疑。
5.4 修改记录失败却不报错:事务没生效的几种情况
现象:保存员工信息,控制台没有任何异常,刷新后数据没变。原因基本在三处:一是表引擎是MyISAM,它不支持事务,所以@Transactional是空转;二是@Transactional写在同类内部的另一个方法上,由本类方法直接调用,Spring AOP代理不生效;三是在事务方法里catch了所有异常并且没有重新抛出,MyBatis如果发现异常被吞掉,事务管理器就不知道要回滚。解决:第一,SHOW TABLE STATUS确认表引擎是InnoDB;第二,事务方法所在类被注入调用而不是本类自调,或者说把内部调用拆到另一个Service;第三,catch到异常后至少记录日志并throw new RuntimeException(e),让事务回滚。事务就是Java应用保证数据一致性的第一道防线,这道防线失效的排查顺序,上面三条就是全部。
5.5 POI导出大数据量时内存溢出:几千行能跑,几万行就挂
现象:项目里用Apache POI导出Excel,几千行时正常工作,导出一万行以上直接OOM,应用卡死。原因:XSSFWorkbook把整个工作簿的所有行都放在JVM堆内存里,数据量翻几倍后内存就撑不住。HSSFWorkbook是03版Excel格式,虽然老但严格限制65536行,超过阈值直接报错。解决:导出大数据量改用SXSSFWorkbook,它是POI的流式版本,内存里只保留一个可配置的行数窗口,比如100行,其余行写到临时磁盘文件。第6章会给出完整写法。这条也是“能跑”和“能用”的分界线——答辩时老师经常会顺手导出一次全量数据,你这套是直接崩溃还是平稳生成,差别非常大的。
6. 进阶:用POI把员工名册导出成Excel
6.1 为什么导出模块最容易暴露“能跑”和“能用”的差距
有人问java poi word能生成图表吗,其实POI在Java项目里更常见的战场是Excel导出。员工名册导出几乎是这个系统的标配需求,但没有几个课设版本做对了。问题集中在三处:导出全量数据时内存失控;中文文件名在浏览器里变成乱码;导出接口不过滤权限,普通员工也能把全公司名册下载走。前两个是技术细节,第三个是权限意识的体现。下面这个示例同时解决这三件事。
6.2 一个可直接复用的Excel导出示例
public void exportExcel(List<EmployeeVO> list, HttpServletResponse response) throws IOException { // 100:内存里只保留100行,其余写临时文件,避免大导出OOM Workbook workbook = new SXSSFWorkbook(100); Sheet sheet = workbook.createSheet("员工名册"); String[] titles = {"工号", "姓名", "部门", "岗位", "状态"}; Row headRow = sheet.createRow(0); for (int i = 0; i < titles.length; i++) { headRow.createCell(i).setCellValue(titles[i]); } for (int i = 0; i < list.size(); i++) { Row row = sheet.createRow(i + 1); EmployeeVO e = list.get(i); row.createCell(0).setCellValue(e.getEmpNo()); row.createCell(1).setCellValue(e.getName()); row.createCell(2).setCellValue(e.getDeptName()); row.createCell(3).setCellValue(e.getPosition()); row.createCell(4).setCellValue(e.getStatus() == 1 ? "在职" : "离职"); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); String fileName = URLEncoder.encode("员工名册.xlsx", "UTF-8"); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName); workbook.write(response.getOutputStream()); if (workbook instanceof SXSSFWorkbook) { ((SXSSFWorkbook) workbook).dispose(); // 删除临时文件 } workbook.close(); }这段代码里有三个参数需要理解。new SXSSFWorkbook(100)是滑动窗口大小,内存里只保留100行,早期写出的行进入临时磁盘文件,适合导出大数据的场景,副作用是不支持随机读取某个单元格,导出场景刚好用不到这个能力。filename*=UTF-8''是RFC 5987的编码方式,解决中文文件名在Chrome和Firefox的兼容问题,老写法只拼filename=加URLEncoder的结果,在部分浏览器里会显示成乱码。导出结束后调用dispose()是为了清理SXSSFWorkbook写出的临时文件,不调用的话临时目录会被占满。
数据来源还要补一个细节:列表应该来自带部门名和权限过滤的VO查询,而不是直接查employee表再逐条查部门名,否则几百人导出时会产生几百次查询。同样,导出接口也要走第2章的权限模型——普通员工只能导出自己的档案,管理员才能导出全量。我第一次实现的时候只做了登录校验,没做行级权限判断,被测试的人用普通账号导出了全公司名册,那次教训让我明白:数据一致性和权限隔离不是两个话题,而是一件事。你现在把这一步写进代码里,答辩时多一层亮点。希望帮到你。
本文还有配套的精品资源,点击获取