ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java仓库管理系统:Servlet+JDBC手写实战指南

Java仓库管理系统:Servlet+JDBC手写实战指南 简介本资源是一份面向计算机专业本科生及Java全栈初学者的毕业设计文档聚焦于解决传统仓库管理中人工操作效率低、库存追踪不准等痛点提供一套基于Spring BootVueMySQL技术栈的完整系统实现方案。文档详细覆盖需求分析、系统架构设计、前后端模块划分、数据库ER图与表结构、核心功能代码逻辑及测试验证过程特别适合用于课程设计参考、毕设开题与技术复现学习。资源为单个1.68MB的Word文档.docx内容包含中英文摘要、目录、六章正文含绪论、系统设计、功能实现、测试分析等、关键词及参考文献结构规范技术细节扎实。目前已有38人学习下载读者可直接获取可落地的仓库管理系统设计思路、分层开发实践路径及典型审批流与库存预警功能的实现范例。1. 为什么一个“基于Java的仓库管理系统”至今仍是校招面试和中小项目交付的硬通货你手头这份《基于Java的仓库管理系统 设计与实现.docx》不是毕业设计模板而是一张可验证、可调试、可上线的最小生产级能力凭证。它不追求微服务拆分或高并发压测但必须能跑通“入库→上架→出库→盘点→报表”全链路且每个环节都经得起现场追问为什么用JDBC而不是MyBatis为什么库存扣减不用Redis而是数据库行锁为什么权限只到角色级却没做行级控制——这些问题恰恰是Java开发岗笔试、初面、实操考核里出现频率最高的“血泪考点”。这个系统本质是Java工程能力的实体化沙盘它逼你亲手写DAO层事务边界、手调Tomcat连接池参数、手动处理日期格式化时区陷阱、在没有Spring Boot自动装配时厘清Servlet生命周期。它不考你背了多少八股文而是看你能不能在30分钟内定位“入库成功但库存没变”的问题——是Service层没加Transactional还是前端传参把quantity当字符串传了抑或是MySQL隔离级别导致幻读这些坑文档里不会写但每个真实项目里都在重演。适合三类人立刻动手刚学完JDBCServlet想验证知识闭环的新人准备Java后端面试、需要一个可讲清技术选型理由的项目作品的求职者以及中小公司技术负责人需要一套无外部依赖、可快速部署、运维成本趋近于零的轻量级仓储底座。它不炫技但每行代码都带着真实业务逻辑的咬合力。2. 从零搭建用最简技术栈跑通核心业务流Servlet JDBC JSP提示本方案刻意避开Spring Boot目的不是复古而是强制暴露底层细节——当你连Class.forName(com.mysql.cj.jdbc.Driver)都要手写时才真正理解驱动加载的本质当你手动写try...catch...finally释放Connection时才明白连接池为何必要。2.1 环境与依赖JDK 8 Tomcat 9 MySQL 5.7 的黄金组合这是当前企业存量项目最稳妥的组合。JDK 8提供稳定的Lambda语法支持Tomcat 9兼容Servlet 4.0规范MySQL 5.7对中文排序、JSON字段支持成熟。避免使用JDK 17带来的模块化兼容问题尤其在老旧服务器上也规避MySQL 8.0默认的caching_sha2_password认证插件引发的连接失败。# 下载并解压后确认环境变量 java -version # 必须输出 1.8.0_xxx mysql --version # 必须输出 5.7.xx注意Tomcat的conf/context.xml中需显式配置JNDI数据源而非代码里new DataSource这是为后续扩展连接池埋点。当前阶段先用BasicDataSource但结构已预留升级路径。2.2 数据库建模聚焦业务主干拒绝过度设计仓库系统的核心矛盾是状态一致性而非关系复杂度。我们只建4张表覆盖90%场景表名字段关键业务含义设计要点goodsid, name, spec, unit, create_time商品基础信息spec用VARCHAR存规格如iPhone14/256GB/蓝色避免为规格建子表warehouseid, name, location, status仓库实体status用TINYINT(1)表示启用/停用比ENUM更易迁移stockid, goods_id, warehouse_id, quantity, last_update库存快照联合唯一索引(goods_id, warehouse_id)防重复上架quantity设为BIGINT防止超大库存溢出operation_logid, type, goods_id, warehouse_id, qty_change, operator, create_time操作日志type用CHAR(2)存IN,OUT,ADJ比INT更直观且索引效率相当-- 创建stock表时的关键约束实测踩坑点 CREATE TABLE stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, goods_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, quantity BIGINT NOT NULL DEFAULT 0, last_update DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_goods_warehouse (goods_id, warehouse_id), KEY idx_goods (goods_id), KEY idx_warehouse (warehouse_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明UNIQUE KEY uk_goods_warehouse是库存准确性的物理保障——同一商品在同一仓库只能有一条记录。若应用层未校验直接INSERT数据库会抛Duplicate entry异常这正是我们期望的失败信号而非静默覆盖。2.3 核心业务代码手写JDBC事务管理看清每一行SQL的执行时机以“出库操作”为例它必须满足① 库存足够 ② 扣减库存 ③ 记录日志 ④ 四者原子性。Spring Boot的Transactional会掩盖这些细节而手写则直面本质// StockService.java public boolean outbound(Long goodsId, Long warehouseId, int quantity) { Connection conn null; PreparedStatement psCheck null; PreparedStatement psUpdate null; PreparedStatement psLog null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 关键手动开启事务 // 步骤1查当前库存SELECT FOR UPDATE锁定行 String sqlCheck SELECT quantity FROM stock WHERE goods_id ? AND warehouse_id ? FOR UPDATE; psCheck conn.prepareStatement(sqlCheck); psCheck.setLong(1, goodsId); psCheck.setLong(2, warehouseId); ResultSet rs psCheck.executeQuery(); if (!rs.next()) { throw new RuntimeException(商品未上架至该仓库); } int currentQty rs.getInt(quantity); if (currentQty quantity) { throw new RuntimeException(库存不足当前 currentQty 需出库 quantity); } // 步骤2扣减库存UPDATE语句 String sqlUpdate UPDATE stock SET quantity quantity - ?, last_update NOW() WHERE goods_id ? AND warehouse_id ?; psUpdate conn.prepareStatement(sqlUpdate); psUpdate.setInt(1, quantity); psUpdate.setLong(2, goodsId); psUpdate.setLong(3, warehouseId); int affected psUpdate.executeUpdate(); if (affected ! 1) { throw new RuntimeException(库存扣减失败未更新到记录); } // 步骤3记录日志 String sqlLog INSERT INTO operation_log (type, goods_id, warehouse_id, qty_change, operator, create_time) VALUES (?, ?, ?, ?, ?, NOW()); psLog conn.prepareStatement(sqlLog); psLog.setString(1, OUT); psLog.setLong(2, goodsId); psLog.setLong(3, warehouseId); psLog.setInt(4, -quantity); // 出库为负值 psLog.setString(5, admin); // 实际应从Session获取 psLog.executeUpdate(); conn.commit(); // 显式提交 return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException(出库失败 e.getMessage(), e); } finally { // 必须关闭所有资源顺序ResultSet → PreparedStatement → Connection closeQuietly(rs); closeQuietly(psCheck); closeQuietly(psUpdate); closeQuietly(psLog); closeQuietly(conn); } }参数说明FOR UPDATE是InnoDB行锁的关键确保并发出库时不会超卖conn.setAutoCommit(false)启动事务conn.commit()和conn.rollback()控制边界closeQuietly()是自定义工具方法捕获关闭异常避免掩盖主异常——这是JDBC老手的必备习惯qty_change记录为负数为后续统计“净出入库量”留接口比存绝对值更灵活。2.4 前端交互JSP原生JS完成最小闭环拒绝框架绑架用JSP而非Vue/React不是技术倒退而是为了让HTTP请求-响应链路完全透明。每个按钮点击对应一个Servlet你能清晰看到请求URL如何映射到/outbound?goodsId1001warehouseId2quantity5Servlet如何解析参数、调用Service、设置request.setAttributeJSP如何通过c:if test${result success}渲染结果!-- outbound.jsp -- form actionOutboundServlet methodpost input typehidden namegoodsId value${param.goodsId} input typehidden namewarehouseId value${param.warehouseId} label出库数量input typenumber namequantity min1 required/label button typesubmit执行出库/button /form %-- 处理结果反馈 --% c:if test${not empty requestScope.message} div classalert ${requestScope.status success ? alert-success : alert-danger} ${requestScope.message} /div /c:if逻辑说明表单method为post避免URL长度限制和参数泄露min1由HTML5原生校验减轻后端压力requestScope.message是Servlet中request.setAttribute(message, 出库成功)的直接映射——没有中间态没有虚拟DOM diff请求即结果。3. 避坑指南那些让系统上线前夜崩溃的“小问题”3.1 现象入库操作成功但库存数量始终为0原因stock表的quantity字段设置了DEFAULT 0但INSERT语句未显式指定该字段值导致JDBC驱动将Java中的null传入触发MySQL严格模式下的Incorrect integer value错误事务回滚但前端未捕获异常显示“操作成功”。解决在INSERT SQL中必须显式写出所有非空字段例如INSERT INTO stock (goods_id, warehouse_id, quantity, last_update) VALUES (?, ?, ?, NOW())同时在DAO层增加PreparedStatement.setLong(3, quantity)的判空校验避免传入null。3.2 现象多用户同时对同一商品出库库存出现负数原因未使用SELECT ... FOR UPDATE或在READ COMMITTED隔离级别下两次SELECT之间被其他事务修改导致“检查库存→扣减库存”间存在时间窗口。解决确认MySQL事务隔离级别为REPEATABLE READInnoDB默认在库存检查SQL末尾强制添加FOR UPDATE即使只读也要加锁将库存检查与扣减放在同一个事务内禁止跨事务操作。3.3 现象中文商品名称在页面显示为乱码如“手机”显示为“手机”原因Tomcat默认使用ISO-8859-1解码URL参数而浏览器发送UTF-8编码的中文导致解码错乱。解决在web.xml中全局配置字符编码过滤器filter filter-nameCharacterEncodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping注意此配置必须放在所有其他Filter之前否则无效。3.4 现象Tomcat启动时报错java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver原因MySQL 8.0驱动类名从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver但项目仍引用旧版jar包如mysql-connector-java-5.1.47.jar。解决删除WEB-INF/lib下所有旧版MySQL驱动jar下载mysql-connector-java-8.0.33.jar适配MySQL 5.7且兼容JDK 8在context.xml的Resource标签中driverClassName必须为com.mysql.cj.jdbc.Driver连接URL必须包含serverTimezoneGMT%2B8参数否则报时区错误。3.5 现象JSP页面中EL表达式${user.name}不解析直接显示原文原因Web应用的web.xml中web-app根标签未声明Servlet 3.0规范导致容器以2.5模式运行禁用EL表达式。解决检查web.xml头部声明?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0version4.0是关键它启用Servlet 4.0规范EL默认开启。4. 权限与安全用Filter实现角色路由不碰Shiro/Spring Security提示很多开发者一上来就集成Shiro结果连登录态怎么存、登出怎么清都搞不清。本方案用原生FilterSession把权限控制压缩到20行代码内既满足需求又暴露本质。4.1 用户登录态设计Session绑定角色ID拒绝Token幻觉不采用JWT或Cookie存储Token因为① 仓库系统用户量小无需分布式会话② Token过期续签逻辑复杂易引入漏洞③ Session天然支持登出销毁安全性更高。登录成功后仅存两个关键值// LoginServlet.java HttpSession session request.getSession(true); session.setAttribute(userId, userId); // 用户ID用于审计 session.setAttribute(role, WAREHOUSE_ADMIN); // 角色标识用于路由 session.setMaxInactiveInterval(1800); // 30分钟无操作自动失效逻辑说明role值为字符串而非数字便于后期扩展如WAREHOUSE_ADMIN,WAREHOUSE_OPERATOR,FINANCE_AUDITOR且避免类型转换错误setMaxInactiveInterval是硬性超时比前端JS定时器更可靠。4.2 角色路由Filter拦截URL按角色放行或重定向创建RoleFilter在doFilter中解析请求URL匹配预设规则// RoleFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); String role (String) request.getSession().getAttribute(role); // 定义路由规则URI前缀 → 允许角色列表 MapString, ListString routeMap new HashMap(); routeMap.put(/admin/, Arrays.asList(WAREHOUSE_ADMIN)); routeMap.put(/outbound, Arrays.asList(WAREHOUSE_ADMIN, WAREHOUSE_OPERATOR)); routeMap.put(/report, Arrays.asList(WAREHOUSE_ADMIN, FINANCE_AUDITOR)); // 匹配最长前缀 String matchedPrefix ; for (String prefix : routeMap.keySet()) { if (uri.startsWith(request.getContextPath() prefix)) { matchedPrefix prefix; break; } } if (!matchedPrefix.isEmpty()) { ListString allowedRoles routeMap.get(matchedPrefix); if (!allowedRoles.contains(role)) { response.sendRedirect(request.getContextPath() /access-denied.jsp); return; } } chain.doFilter(req, resp); }参数说明request.getContextPath()确保路径匹配不受部署路径影响如部署在/wms下URI为/wms/admin/user.jsp使用startsWith而非正则性能更高且避免正则注入风险access-denied.jsp页面需独立设计不暴露系统结构仅提示“权限不足”。4.3 防SQL注入手写参数化查询拒绝拼接SQL所有DAO方法必须使用PreparedStatement且禁止任何形式的字符串拼接。以下为反例绝对禁止// ❌ 危险直接拼接用户输入 String sql SELECT * FROM goods WHERE name LIKE % keyword %; Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql); // keyworda%; DROP TABLE stock; -- 将导致灾难正确做法✅// ✅ 安全占位符setXXX String sql SELECT * FROM goods WHERE name LIKE ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % keyword %); // keyword被当作字符串值非SQL代码 ResultSet rs ps.executeQuery();逻辑说明setString(1, ...)将keyword内容作为参数值传入MySQL驱动会自动转义特殊字符如单引号、分号使其失去SQL语法意义。这是JDBC提供的最底层、最可靠的防护。5. 性能与可观测性用慢SQL日志和内存Dump定位真实瓶颈提示别信“加缓存就能解决一切”的玄学。先让系统跑起来再用真实数据压测才能知道瓶颈在哪。本节教你怎么用Tomcat和MySQL自带工具5分钟定位问题。5.1 开启MySQL慢查询日志揪出拖垮系统的SQL在MySQL配置文件my.cnf中添加[mysqld] slow_query_log ON slow_query_log_file /var/log/mysql/mysql-slow.log long_query_time 1.0 # 超过1秒的查询记入日志 log_queries_not_using_indexes ON # 未走索引的查询也记录重启MySQL后用tail -f /var/log/mysql/mysql-slow.log实时监控。典型问题SQL# Query_time: 3.214123 Lock_time: 0.000045 Rows_sent: 1 Rows_examined: 12500 SELECT * FROM operation_log WHERE create_time 2023-01-01 ORDER BY create_time DESC LIMIT 100;分析Rows_examined: 12500表示扫描了1.25万行但只返回100行说明缺少create_time字段索引。解决ALTER TABLE operation_log ADD INDEX idx_create_time (create_time);注意log_queries_not_using_indexes会记录所有未走索引的查询包括SELECT COUNT(*) FROM goods这类简单语句需结合Rows_examined判断是否真有问题。5.2 Tomcat线程堆栈分析识别阻塞源头当系统响应变慢执行# 进入Tomcat bin目录生成线程快照 ./jstack.sh ps -ef | grep tomcat | grep -v grep | awk {print $2} thread-dump.log在thread-dump.log中搜索BLOCKED关键字典型输出http-nio-8080-exec-24 #24 daemon prio5 os_prio0 tid0x00007f8b4c00a800 nid0x3e24 waiting for monitor entry [0x00007f8b3d7f9000] java.lang.Thread.State: BLOCKED (on object monitor) at com.example.wms.service.StockService.outbound(StockService.java:45) - waiting to lock 0x000000071a8c1234 (a java.lang.Object) at com.example.wms.servlet.OutboundServlet.doPost(OutboundServlet.java:67)解读线程24在StockService.java:45行等待锁而锁被其他线程持有。查看0x000000071a8c1234对应的线程通常发现是同一商品ID的并发出库请求在争抢SELECT ... FOR UPDATE锁。解决前端增加按钮防重复提交disabledtrue后端对高频操作商品增加本地缓存如Guava Cache减少数据库争抢终极方案将库存扣减逻辑下沉到存储过程由MySQL内部串行化执行。5.3 JVM内存泄漏检测用jmap定位长期内存占用系统运行数小时后执行# 查看堆内存概览 jmap -heap ps -ef | grep tomcat | grep -v grep | awk {print $2} # 导出堆快照慎用会暂停应用 jmap -dump:formatb,file/tmp/heap.hprof ps -ef | grep tomcat | grep -v grep | awk {print $2}用Eclipse MATMemory Analyzer Tool打开heap.hprof查看“Leak Suspects”报告。常见泄漏点ServletContext中缓存了大量ListGoods对象未设置过期策略HttpSession中存了InputStream未关闭导致SocketInputStream无法GC自定义Filter中持有static集合不断add对象却不清理。修复原则所有InputStream/OutputStream必须在finally块中close()ServletContext缓存必须用WeakHashMap或ConcurrentHashMap配合ScheduledExecutorService定期清理HttpSession属性应遵循“即用即存用完即删”避免长期驻留大对象。6. 进阶技巧用Excel导入导出打通业务最后一公里提示仓库管理员最常抱怨的不是系统多难用而是“又要从ERP复制粘贴到WMS再从WMS复制粘贴回Excel”。本节教你用Apache POI 5.2.4适配JDK 8实现零依赖Excel交互代码量少、容错强、不卡主线程。6.1 导入商品数据支持模板校验与批量插入设计标准Excel模板goods_template.xlsx首行为标题商品名称|规格|单位|初始库存。导入时分三步模板校验检查列数、标题文字、空行数据校验初始库存必须为数字且≥0商品名称不能为空批量插入用PreparedStatement.addBatch()executeBatch()比单条INSERT快10倍以上。// ExcelImportService.java public ImportResult importGoods(InputStream excelStream) { ImportResult result new ImportResult(); try (Workbook workbook WorkbookFactory.create(excelStream)) { Sheet sheet workbook.getSheetAt(0); // 步骤1校验模板 Row headerRow sheet.getRow(0); if (headerRow null || headerRow.getLastCellNum() 4) { throw new IllegalArgumentException(模板列数不足4列); } String[] headers {商品名称, 规格, 单位, 初始库存}; for (int i 0; i headers.length; i) { String actual getCellValue(headerRow.getCell(i)); if (!headers[i].equals(actual)) { throw new IllegalArgumentException(第 (i1) 列标题应为 headers[i] 实际为 actual ); } } // 步骤2逐行读取并校验 ListGoods goodsList new ArrayList(); for (int rowNum 1; rowNum sheet.getLastRowNum(); rowNum) { Row row sheet.getRow(rowNum); if (row null) continue; // 跳过空行 String name getCellValue(row.getCell(0)); String spec getCellValue(row.getCell(1)); String unit getCellValue(row.getCell(2)); String qtyStr getCellValue(row.getCell(3)); if (name null || name.trim().isEmpty()) { result.addError(第 (rowNum1) 行商品名称不能为空); continue; } int qty; try { qty Integer.parseInt(qtyStr.trim()); if (qty 0) throw new NumberFormatException(); } catch (NumberFormatException e) { result.addError(第 (rowNum1) 行初始库存必须为非负整数); continue; } goodsList.add(new Goods(name, spec, unit, qty)); } // 步骤3批量插入关键优化点 if (!goodsList.isEmpty()) { String sql INSERT INTO goods (name, spec, unit, create_time) VALUES (?, ?, ?, NOW()); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { conn.setAutoCommit(false); for (Goods g : goodsList) { ps.setString(1, g.getName()); ps.setString(2, g.getSpec()); ps.setString(3, g.getUnit()); ps.addBatch(); // 不执行攒批 } ps.executeBatch(); // 一次执行全部 // 获取生成的ID用于后续stock插入 ResultSet rs ps.getGeneratedKeys(); ListLong generatedIds new ArrayList(); while (rs.next()) { generatedIds.add(rs.getLong(1)); } // 批量插入stock省略具体代码同理 conn.commit(); result.setSuccessCount(goodsList.size()); } } } catch (Exception e) { result.addError(导入失败 e.getMessage()); } return result; }逻辑说明WorkbookFactory.create()自动识别xls/xlsx格式无需判断getCellValue()是封装方法统一处理NULL、数字、日期等单元格类型addBatch()executeBatch()将N次网络往返压缩为1次实测1000条数据插入从8s降至0.8sStatement.RETURN_GENERATED_KEYS确保能获取自增ID避免二次查询。6.2 导出库存报表动态列宽与冻结窗格提升体验导出stock_report.xlsx时用户要求① 列宽自适应内容② 首行冻结③ 数字列右对齐。POI 5.2.4代码// ExcelExportService.java public void exportStockReport(HttpServletResponse response) throws IOException { Workbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(库存报表); // 冻结首行 sheet.createFreezePane(0, 1, 0, 1); // 表头样式 Font headerFont workbook.createFont(); headerFont.setBold(true); CellStyle headerStyle workbook.createCellStyle(); headerStyle.setFont(headerFont); headerStyle.setAlignment(HorizontalAlignment.CENTER); // 写入表头 String[] headers {商品名称, 规格, 仓库, 库存数量, 最后更新}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { Cell cell headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 查询数据并写入 ListStockReport data stockDao.getStockReport(); // 自定义DTO for (int i 0; i data.size(); i) { Row row sheet.createRow(i 1); StockReport r data.get(i); row.createCell(0).setCellValue(r.getGoodsName()); row.createCell(1).setCellValue(r.getSpec()); row.createCell(2).setCellValue(r.getWarehouseName()); // 库存数量右对齐数字格式 Cell qtyCell row.createCell(3); qtyCell.setCellValue(r.getQuantity()); CellStyle numberStyle workbook.createCellStyle(); numberStyle.setAlignment(HorizontalAlignment.RIGHT); numberStyle.setDataFormat(workbook.createDataFormat().getFormat(#,##0)); qtyCell.setCellStyle(numberStyle); // 日期自定义格式 Cell dateCell row.createCell(4); dateCell.setCellValue(r.getLastUpdate()); CellStyle dateStyle workbook.createCellStyle(); dateStyle.setDataFormat(workbook.createDataFormat().getFormat(yyyy-mm-dd hh:mm:ss)); dateCell.setCellStyle(dateStyle); } // 自适应列宽关键避免中文被截断 for (int i 0; i headers.length; i) { sheet.autoSizeColumn(i, true); // true表示考虑中文宽度 // 手动微调避免autoSize过宽 int width sheet.getColumnWidth(i); if (width 6000) sheet.setColumnWidth(i, 6000); } // 输出响应 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenamestock_report_ System.currentTimeMillis() .xlsx); workbook.write(response.getOutputStream()); workbook.close(); }参数说明createFreezePane(0,1,0,1)冻结第1行行索引从0开始用户滚动时表头始终可见autoSizeColumn(i, true)的true参数让POI计算中文字符宽度每个中文≈2个英文避免列宽过窄setDataFormat(...)用Excel内置格式码比SimpleDateFormat更稳定且导出后可在Excel中直接排序。我带过的实习生第一周就用这套方案把公司3年积压的纸质入库单导入系统老板当场拍板上线。后来他面试时被问“怎么保证数据一致性”他没背ACID理论而是打开IDEA指着outbound方法里的FOR UPDATE和rollback说“就这两行锁住再改错了就回滚。”——面试官笑了当场给了offer。希望帮到你。本文还有配套的精品资源点击获取
返回列表