ARTICLE DETAIL

资讯详情

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

JXL封装Java导入导出Excel工具类:从入门到实战

JXL封装Java导入导出Excel工具类:从入门到实战 简介这套Java工具类基于开源类库JXL实现面向需要在项目中读写旧版Excel工作簿.xls的开发者能够减少直接操作底层API的重复劳动快速完成数据导入导出。压缩包共9个文件整体约1.47MB包含4个常用依赖jar含jxl核心库及commons等辅助包、3个Java源码、1份说明文档和1个许可证结构清晰方便直接查看与引入项目。目前已有111人学习下载。工具类覆盖了JXL的关键用法创建工作簿与工作表、设置单元格字体、颜色、边框和背景处理日期数字及文本格式还兼顾了大数据量下的性能优化和异常处理。内部封装了导入导出的静态方法外部只需传入文件与数据即可调用随包附带的README说明能帮助快速上手。对希望理解Excel二进制结构、掌握Java读写Excel中高级细节的开发者来说是一份具备实践参考价值的代码。1. 用JXL封装Java导入导出Excel工具类先想清楚为什么不用POIJXLJava Excel API是一个纯Java的Excel读写库主打轻量只处理xls格式。很多维护了几年的系统里用的还是JXL而不是Apache POI原因很现实现有代码已经跑了好多年没必要为了新特性重写。如果你接手的是这类项目现学现卖封装一个工具类把Workbook、Sheet、Cell的细节收进去后面所有业务模块都只跟List 打交道这是最省事的做法。这篇文章就围绕这个目标展开顺便把Java面试题里常问的JXL和POI区别、日期单元格乱码、合并单元格读取这几个坑也一并说透。面向的读者是Java后端开发以及准备面试时需要补一轮Excel读写知识的同学。2. JXL的读写模型与三个关键对象Workbook、Sheet、Cell2.1 JXL与POI的取舍轻量但只支持xlsJXL的核心对象只有三个Workbook对应一个Excel文件Sheet对应文件里的一张工作表Cell对应一个单元格。所有读写操作都是在这三层之间移动。理解了这个模型后面工具类的封装就是一层包装而已。选JXL而不是POI常见判断标准如下。维度JXLApache POI支持格式仅xlsExcel 97-2003xls和xlsx都支持API风格简单行列直接定位体系大HSSF/XSSF/SXSSF分层内存占用整体写入数据量过大会OOMSXSSF可以流式写内存友好依赖jar单包1MB左右poi、poi-ooxml一堆依赖适合场景老项目、简单模板、内部工具新项目、复杂报表、大数据量JXL的发布年代很早很多Java基础面试题里会把它和POI放在一起考。实际决策时如果项目已经用了JXL没有必要因为POI功能多就立刻重写重点看你的Excel需求是否只停留在xls。注意JXL不支持xlsx也不支持公式计算导出的文件在WPS和Excel里打开没问题但别用它做统计报表写公式这种工作应该交给POI或模板引擎。2.2 用JXL导出Excel的最小代码创建Workbook与写入Cell动手写第一个导出程序先不封装直接感受API。下面的代码创建一个user.xls写入一行表头、一行数据。import jxl.Workbook; import jxl.write.Label; import jxl.write.Number; import jxl.write.DateTime; import jxl.write.WritableSheet; import jxl.write.WritableWorkbook; import java.io.FileOutputStream; import java.io.OutputStream; public class MinimalExport { public static void main(String[] args) throws Exception { OutputStream out new FileOutputStream(user.xls); // 1. 创建可写工作簿 WritableWorkbook wwb Workbook.createWorkbook(out); // 2. 在工作簿中创建sheet名称“用户表”索引0 WritableSheet sheet wwb.createSheet(用户表, 0); // 3. 写表头Label表示文本单元格 sheet.addCell(new Label(0, 0, 姓名)); sheet.addCell(new Label(1, 0, 年龄)); sheet.addCell(new Label(2, 0, 入职日期)); // 4. 写数据姓名用Label年龄用Number日期用DateTime sheet.addCell(new Label(0, 1, 张三)); sheet.addCell(new Number(1, 1, 28)); sheet.addCell(new DateTime(2, 1, new java.util.Date())); // 5. 写入文件并关闭 wwb.write(); wwb.close(); out.close(); } }代码里addCell的参数是列、行、内容行列都从0开始。注意不要记反new Label(col, row, value)中第一个参数是列号第二个是行号很多新手会把行号写到第一位。这个顺序和Excel本身的“先列后行”不一样是JXL的API习惯。关于类型JXL把单元格内容分成Label字符串、Number数字、DateTime日期三种主要类型另外还有Boolean和Formula。字符串和日期不能用混否则Excel里会出现“无法粘贴数据”或者显示乱码。后面封装工具类的时候我们会在反射里按Java字段的类型自动选择写入类型。2.3 用JXL读取Excel的最小代码遍历Sheet与Cell读取比写入更像遍历一个二维数组。下面是最小的读取程序把上一节生成的文件读回来并打印所有单元格内容。import jxl.Cell; import jxl.Sheet; import jxl.Workbook; import java.io.FileInputStream; import java.io.InputStream; public class MinimalImport { public static void main(String[] args) throws Exception { InputStream in new FileInputStream(user.xls); // 1. 创建只读工作簿 Workbook workbook Workbook.getWorkbook(in); // 2. 取第一张sheet Sheet sheet workbook.getSheet(0); // 3. 双重循环遍历所有单元格 for (int row 0; row sheet.getRows(); row) { for (int col 0; col sheet.getColumns(); col) { Cell cell sheet.getCell(col, row); System.out.print(cell.getContents() \t); } System.out.println(); } // 4. 关闭 workbook.close(); in.close(); } }这里getCell(col, row)同样是先列后行getRows返回总行数getColumns返回总列数。所有单元格都能用getContents()转成字符串这是最简单的取值方式但对日期字段会丢精度。因为JXL的getContents返回的是Excel显示值比如2025/04/01这个字符串而不是java.util.Date对象。如果你需要日期对象必须把Cell强转为DateCell再调用getDate()这一点在第四章会展开讲。从这两个最小示例可以看出JXL的核心就是“创建Workbook → 拿Sheet → addCell/readCell”剩下的都是业务映射问题。3. 封装一个可复用的Java Excel导入导出工具类有了最小读写基础现在开始写一个通用工具类。目标很明确业务代码只需要定义实体类和注解导入导出时直接调一个方法不用再碰Cell、Sheet这些底层对象。3.1 工具类的设计目标反射映射、类型转换、错误收集这个工具类要解决三件事第一表头到Java字段的映射用注解定义列名和顺序比硬编码下标好维护第二单元格类型到Java类型的转换比如Excel里的数字文本要转成Integer还是String日期要转成Date第三导入时的错误收集某一行某个字段转换失败不能整个文件失败应该记录行号继续往下处理。先说注解。定义一个ExcelColumn放在实体类字段上。import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Retention(RetentionPolicy.RUNTIME) Target(ElementType.FIELD) public interface ExcelColumn { // 表头显示名称 String name() default ; // 列顺序数字小的靠前 int order() default 0; }注解的RetentionPolicy.RUNTIME是为了可以在运行期用反射读取。Target(ElementType.FIELD)说明只能标在字段上不能标类或方法。name和order是我们在导出表头和排列字段时的依据。3.2 导出工具方法List 转Workbook并输出用反射拿到实体类的所有字段按order排序然后写入表头和数据行。下面是一个可以直接用的导出方法放进ExcelUtil类里。import jxl.Workbook; import jxl.write.DateTime; import jxl.write.Label; import jxl.write.Number; import jxl.write.WritableSheet; import jxl.write.WritableWorkbook; import java.io.OutputStream; import java.lang.reflect.Field; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.List; public static T void exportToStream(ListT dataList, ClassT clazz, OutputStream out) { try { WritableWorkbook wwb Workbook.createWorkbook(out); WritableSheet sheet wwb.createSheet(Sheet1, 0); // 1. 反射收集带ExcelColumn注解的字段 ListField fields new ArrayList(); for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(ExcelColumn.class)) { fields.add(field); } } // 2. 按order排序order一样就保持声明顺序 fields.sort(Comparator.comparingInt( field - field.getAnnotation(ExcelColumn.class).order())); // 3. 写入表头 for (int col 0; col fields.size(); col) { Field field fields.get(col); String name field.getAnnotation(ExcelColumn.class).name(); sheet.addCell(new Label(col, 0, name)); } // 4. 写入数据从第1行开始 for (int row 0; row dataList.size(); row) { T item dataList.get(row); int excelRow row 1; for (int col 0; col fields.size(); col) { Field field fields.get(col); field.setAccessible(true); Object value field.get(item); if (value null) { sheet.addCell(new Label(col, excelRow, )); } else if (value instanceof Double || value instanceof Integer || value instanceof BigDecimal || value instanceof Long) { sheet.addCell(new Number(col, excelRow, Double.parseDouble(value.toString()))); } else if (value instanceof Date) { sheet.addCell(new DateTime(col, excelRow, (Date) value)); } else { sheet.addCell(new Label(col, excelRow, value.toString())); } } } wwb.write(); wwb.close(); } catch (Exception e) { throw new RuntimeException(导出Excel失败, e); } }逻辑说明第1步用isAnnotationPresent判断字段是否标了注解把候选字段收进List。第2步对字段按order排序保证表头顺序和实体字段声明顺序无关。第4步通过field.setAccessible(true)访问private字段然后用instanceof判断Java类型选择合适的JXL单元格写入类。参数说明dataList是要导出的数据集合clazz是实体类型out是输出流。这个方法没有做分页如果dataList特别大建议外部按批次调用每次生成一个文件或者自己拆成多个Sheet。3.3 导入工具方法Workbook转List导入是导出的逆向过程读取表头然后从第1行开始把每一行反射成实体对象。import jxl.Cell; import jxl.DateCell; import jxl.Sheet; import jxl.Workbook; import java.io.InputStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.List; public static T ListT importFromStream(InputStream in, ClassT clazz) { ListT result new ArrayList(); try { Workbook workbook Workbook.getWorkbook(in); Sheet sheet workbook.getSheet(0); ListField fields new ArrayList(); for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(ExcelColumn.class)) { fields.add(field); } } // 这里简化默认表头顺序和字段排序一致可靠做法是按表头名称匹配后面补 fields.sort(Comparator.comparingInt( field - field.getAnnotation(ExcelColumn.class).order())); // 从第1行开始跳过表头 for (int row 1; row sheet.getRows(); row) { T obj clazz.newInstance(); for (int col 0; col sheet.getColumns() col fields.size(); col) { Field field fields.get(col); field.setAccessible(true); Cell cell sheet.getCell(col, row); Class? type field.getType(); try { if (type String.class) { field.set(obj, cell.getContents()); } else if (type int.class || type Integer.class) { field.set(obj, Double.valueOf(cell.getContents()).intValue()); } else if (type long.class || type Long.class) { field.set(obj, Double.valueOf(cell.getContents()).longValue()); } else if (type double.class || type Double.class) { field.set(obj, Double.valueOf(cell.getContents())); } else if (type boolean.class || type Boolean.class) { field.set(obj, Boolean.valueOf(cell.getContents())); } else if (type Date.class) { if (cell instanceof DateCell) { field.set(obj, ((DateCell) cell).getDate()); } else { field.set(obj, null); // 文本类型的日期需要解析 } } else { field.set(obj, cell.getContents()); } } catch (Exception e) { // 单行单列解析失败保留一个空值继续处理 field.set(obj, null); } } result.add(obj); } workbook.close(); } catch (Exception e) { throw new RuntimeException(导入Excel失败, e); } return result; }逻辑说明sheet.getColumns()返回Excel里的列数我们要么按列数循环要么按字段数循环取二者较小值。这样当Excel比实体多出无关列时不会越界。cell.getContents()对数字单元格返回的是纯数字字符串所以可以用Double.valueOf解析。注意这里有两个简化处理。第一个是日期如果单元格是JXL的DateCell直接拿getDate()如果用户填的是文本日期getContents()返回类似“2024-01-01”的字符串这段代码会置null实际项目中需要准备日期解析器。第二个简化是表头映射这里按字段的order顺序去对应Excel列一旦表头顺序变化就会错位。更稳的写法是先读第0行表头建立一个“列名→列索引”的Map再按字段上的ExcelColumn.name()去取值这一步建议在生产代码里补齐。3.4 处理表头映射与单元格样式上面的导入方法里提到列顺序问题。常见做法是先用一个MapString, Integer headerMap保存表头下面是把第0行读取成Map的代码。import java.util.HashMap; import java.util.Map; MapString, Integer headerMap new HashMap(); for (int col 0; col sheet.getColumns(); col) { String header sheet.getCell(col, 0).getContents(); headerMap.put(header, col); }得到headerMap后遍历实体字段时根据ExcelColumn.name()找到对应的列索引再用这个索引去读行数据。这样就算用户把Excel的列顺序调乱只要表头名称不变导入就能正确对位。样式方面JXL支持用WritableCellFormat控制字体、背景色、边框、对齐等。导出时可以为表头行单独设置一个格式让导出的文件看起来更正式。常见做法是给表头加粗并加背景色。import jxl.write.WritableCellFormat; import jxl.write.WritableFont; import jxl.write.Colour; WritableFont headerFont new WritableFont(WritableFont.ARIAL, 11, WritableFont.BOLD); WritableCellFormat headerFormat new WritableCellFormat(headerFont); headerFormat.setBackground(Colour.GRAY_50); headerFormat.setAlignment(jxl.format.Alignment.CENTRE);在导出代码里表头addCell时可以用这个headerFormatsheet.addCell(new Label(col, 0, name, headerFormat))。这样设置的是整行Excel单元格样式不会影响数据行的内容。工具类里建议把headerFormat作为可配置项而不是写死避免不同业务模块的导出样式互相干扰。导入时单行解析失败目前是置null但实际生产环境需要把错误行号反馈给用户。可以在工具类里维护一个MapInteger, String errorMap记录某一行的某个字段转换失败原因在返回结果时附带这个Map。这样前端可以把错误行标红而不是让用户看着一个全空列表发呆。不过这个属于工具类的增强项不影响核心的导入导出链路建议先把表头映射和类型转换跑通再加。4. JXL导入导出的常见坑与参数调优日期、合并单元格、内存JXL用起来简单但到真实业务里就会碰到几个绕不开的坑。这里选四个最常见的每一个都给出了定位方法和解决套路。4.1 xlsx无法读取JXL的边界与兼容方案JXL只认BIFF8格式的xls文件遇到xlsx直接报NullPointerException或FileFormatException。代码里要先判断文件名后缀。常见做法是在导入接口入口处做一次拦截。public static boolean isXlsFile(String fileName) { if (fileName null) { return false; } return fileName.toLowerCase().endsWith(.xls); }注意这里没有判断xlsx因为JXL本身不支持。如果用户上传的是xlsx处理方案有两种一是提示“请另存为xls格式再上传”二是在上传接口里用Apache POI把xlsx转成xls。第二种做法在一个上传服务里会遇到一个点POI和JXL都要处理Excel两套API都要维护。我一般建议老项目保持JXL新接口改成POI不要在中间层写混用的转换器。很多Java面试题里问“POI和JXL区别”本质也是在考这种边界识别能力。4.2 日期格式与单元格类型转换JXL日期是最容易出问题的地方。写入时如果对Date字段用了LabelExcel里显示的是Mon Apr 01 00:00:00 CST 2024这种Java的默认toString用户会以为程序写错了。必须用DateTime类型去写。DateTime的构造函数可以带日期格式参数。import jxl.write.DateFormat; import jxl.write.DateTime; import jxl.write.WritableCellFormat; DateFormat dateFormat new DateFormat(yyyy-MM-dd HH:mm:ss); WritableCellFormat dateCellFormat new WritableCellFormat(dateFormat); DateTime dt new DateTime(col, row, date, dateCellFormat); sheet.addCell(dt);DateFormat里的格式字符串和SimpleDateFormat的pattern一致支持yyyy-MM-dd HH:mm:ss。如果不指定格式JXL会用Excel默认格式通常只显示日期部分时分秒会丢。读取时也有个隐藏坑如果时间是Excel的日期单元格cell.getContents()按区域设置返回字符串但如果是文本单元格写着“2024-04-01”JXL不会自动识别成日期。正确的判断是先看cell.getType()是不是CellType.DATE再强转成DateCell。这里给一个工具方法import jxl.Cell; import jxl.CellType; import jxl.DateCell; public static java.util.Date parseDateCell(Cell cell) { if (cell.getType() CellType.DATE cell instanceof DateCell) { return ((DateCell) cell).getDate(); } // 兼容文本日期 String content cell.getContents().trim(); if (content.matches(\\d{4}-\\d{2}-\\d{2}.*)) { try { return new java.text.SimpleDateFormat(yyyy-MM-dd).parse(content); } catch (Exception e) { return null; } } return null; }这个工具方法里用了CellType.DATE做判断再降级兼容常见的文本日期避免用户从别处复制粘贴后格式变成文本导致读不到值。4.3 合并单元格与样式丢失JXL读取合并单元格时getCell(col, row)只会返回合并区域左上角的真实单元格其它位置返回一个空Cell内容为空字符串。如果你的导入模板里有“基本信息”这样的跨列标题遍历时会在右侧单元格读到空导致实体里对应字段为null。定位方法很简单先输出行列对比看看数据是不是自动右移了。解决方式是导入时手动做“填充值扩散”把上一行或者上一列的值补过来。下面是一个对行内合并做处理的片段。MapInteger, String lastValuePerCol new HashMap(); for (int row 1; row sheet.getRows(); row) { for (int col 0; col sheet.getColumns(); col) { String content sheet.getCell(col, row).getContents(); if (content null || content.isEmpty()) { content lastValuePerCol.getOrDefault(col, ); } else { lastValuePerCol.put(col, content); } System.out.print(content \t); } System.out.println(); }这里用lastValuePerCol缓存每一列最近一次非空值。遇到空单元格就取缓存的值这是处理纵向合并单元格的常见套路。如果是横向合并则需要按行维护类似的map。注意这种补值可能导致原本就是空白的单元格被误补所以只应针对模板里明确有合并的表头区域使用不要在纯数据表上无脑扩散。4.4 大数据量导出的内存控制JXL不是流式写入Workbook.createWorkbook之后所有单元格数据都缓存在内存里最终wwb.write()一次性写出。导出几万行通常没问题但几十万行就会看到内存上涨明显。而且JXL单Sheet最大行数是65536超过会抛Sheet.MaxRowsExceededException。应对策略是拆分成多个Sheet每个Sheet写6万行左右。下面是一个按行数分Sheet的写法。import jxl.write.WritableSheet; import jxl.write.WritableWorkbook; public static void exportBySheets(ListYourData dataList, OutputStream out) throws Exception { WritableWorkbook wwb Workbook.createWorkbook(out); int sheetSize 60000; int sheetCount (dataList.size() sheetSize - 1) / sheetSize; for (int s 0; s sheetCount; s) { WritableSheet sheet wwb.createSheet(数据 (s 1), s); // 先写表头然后从dataList中截取[s*sheetSize, (s1)*sheetSize)这段写数据 int start s * sheetSize; int end Math.min(start sheetSize, dataList.size()); for (int row start; row end; row) { // addCell逻辑同3.2 } } wwb.write(); wwb.close(); }这个方案比硬扛单Sheet靠谱因为分Sheet还能让数据量超过6万时不报错。注意每个Sheet都要单独写表头否则其它Sheet没有列名。读取端则需要遍历所有Sheetworkbook.getNumberOfSheets()可以得到sheet数量。内存问题最彻底的解法是换POI的SXSSF但如果项目已经定型分Sheet是改动最小的方案。分Sheet的个数也不是越多越好。每个Sheet都带一个表头行数太少会产生大量重复表头行数太多又会逼近65536上限。经验值是把单Sheet数据量控制在3万到6万之间如果内存充足可放宽到6万如果JDK堆内存只有256MB建议降到3万。导出时可以用Runtime.getRuntime().maxMemory()估算可用内存再动态决定sheetSize但老项目里写死一个安全值往往更省心。5. 给工具类加一个模板导出功能动态表头与下拉框单元格工具类能做到能导入导出还差最后一步让导出的Excel更像一个正式模板。比如在导出的表单里加上可下拉的选项列或者给表头套用品牌色。JXL的WritableCellFeatures支持在单元格上设置下拉数据验证实现起来很简单。5.1 用WritableCellFeatures给单元格加下拉框下面代码在“部门”这一列给第1行到第10行的每个单元格设置下拉列表选项来自数组。import jxl.write.Label; import jxl.write.WritableCellFeatures; import jxl.write.WritableSheet; WritableCellFeatures features new WritableCellFeatures(); features.setDataValidationList(new String[]{研发部, 市场部, 运营部}); for (int row 1; row 10; row) { Label cell new Label(3, row, ); // 第3列是部门 cell.setCellFeatures(features); sheet.addCell(cell); }setDataValidationList生成的Excel下拉框点击单元格会出现箭头。对用户来说这比手填文本可靠得多还能从源头避免“Excel无法粘贴数据”这类问题——因为单元格限制了合法值从外部粘贴不规范内容会被Excel拦住。注意features是一个对象在多行复用没问题但每次setCellFeatures都要重新调用不能偷懒用同一个Label对象。这里列出WritableCellFeatures的几个常用方法方便后面扩展。方法作用备注setDataValidationList设置下拉选项数组内字符串即为选项setDescription设置单元格批注不是每个Excel版本都显示setFormula设置公式JXL支持有限不推荐下拉框在Excel里有效但WPS里可能不完全兼容。如果目标用户大量使用WPS建议导出后用WPS打开验证一次。5.2 动态表头把注解里的name改成可覆盖的模板路径模板导出的常见需求是表头不固定。比如导出当月考勤列名根据月份生成。做法是在exportToStream上增加一个headerNames参数用它覆盖注解里的name。public static T void exportToStream(ListT dataList, ClassT clazz, OutputStream out, ListString headerNames) { // 不传headerNames时沿用ExcelColumn.name() ListField fields new ArrayList(); for (Field f : clazz.getDeclaredFields()) { if (f.isAnnotationPresent(ExcelColumn.class)) { fields.add(f); } } fields.sort(Comparator.comparingInt( f - f.getAnnotation(ExcelColumn.class).order())); // 写入表头时优先用headerNames.get(col) for (int col 0; col fields.size(); col) { String name (headerNames ! null col headerNames.size()) ? headerNames.get(col) : fields.get(col).getAnnotation(ExcelColumn.class).name(); sheet.addCell(new Label(col, 0, name)); } // 数据写入逻辑与3.2一致 }这个重载方法给自己的工具类加了模板能力调用方只需要按顺序传入新的表头名称实体类的字段顺序和类型映射完全不变。业务代码里配合一个导出配置表就能实现不同部门导出不同列名的效果。5.3 验证工具类的完整流程最后给一个自测步骤用代码生成一个带下拉框和动态表头的xls文件然后用上一章的导入方法读回来。重点验证三个点表头顺序变了导入是否还能对齐字段下拉框的合法值能否正常读入非法值会不会导致解析中断日期列经过一次导入导出后时分秒是否还在。只要这三个点通过工具类在真实项目里就足够撑起大多数导出场景了。至于更复杂的多级表头、图片导出、公式重算JXL支持有限建议直接上POI没必要在JXL上硬抠。本文还有配套的精品资源点击获取
返回列表