JXL封装Java导入导出Excel工具类:从入门到实战
2026/9/14 12:17:58 网站建设 项目流程

简介:这套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工具类,先想清楚为什么不用POI

JXL(Java Excel API)是一个纯Java的Excel读写库,主打轻量,只处理xls格式。很多维护了几年的系统里,用的还是JXL而不是Apache POI,原因很现实:现有代码已经跑了好多年,没必要为了新特性重写。如果你接手的是这类项目,现学现卖封装一个工具类,把Workbook、Sheet、Cell的细节收进去,后面所有业务模块都只跟List 打交道,这是最省事的做法。这篇文章就围绕这个目标展开,顺便把Java面试题里常问的JXL和POI区别、日期单元格乱码、合并单元格读取这几个坑也一并说透。面向的读者是Java后端开发,以及准备面试时需要补一轮Excel读写知识的同学。

2. JXL的读写模型与三个关键对象:Workbook、Sheet、Cell

2.1 JXL与POI的取舍:轻量但只支持xls

JXL的核心对象只有三个:Workbook对应一个Excel文件,Sheet对应文件里的一张工作表,Cell对应一个单元格。所有读写操作都是在这三层之间移动。理解了这个模型,后面工具类的封装就是一层包装而已。

选JXL而不是POI,常见判断标准如下。

维度JXLApache POI
支持格式仅xls(Excel 97-2003)xls和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)说明只能标在字段上,不能标类或方法。nameorder是我们在导出表头和排列字段时的依据。

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(List<T> dataList, Class<T> clazz, OutputStream out) { try { WritableWorkbook wwb = Workbook.createWorkbook(out); WritableSheet sheet = wwb.createSheet("Sheet1", 0); // 1. 反射收集带@ExcelColumn注解的字段 List<Field> 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> List<T> importFromStream(InputStream in, Class<T> clazz) { List<T> result = new ArrayList<>(); try { Workbook workbook = Workbook.getWorkbook(in); Sheet sheet = workbook.getSheet(0); List<Field> 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 处理表头映射与单元格样式

上面的导入方法里提到列顺序问题。常见做法是先用一个Map<String, Integer> headerMap保存表头,下面是把第0行读取成Map的代码。

import java.util.HashMap; import java.util.Map; Map<String, 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,但实际生产环境需要把错误行号反馈给用户。可以在工具类里维护一个Map<Integer, 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字段用了Label,Excel里显示的是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。

定位方法很简单:先输出行列对比,看看数据是不是自动右移了。解决方式是导入时手动做“填充值扩散”,把上一行或者上一列的值补过来。下面是一个对行内合并做处理的片段。

Map<Integer, 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(List<YourData> 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, (s+1)*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没有列名。读取端则需要遍历所有Sheet,workbook.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(List<T> dataList, Class<T> clazz, OutputStream out, List<String> headerNames) { // 不传headerNames时沿用@ExcelColumn.name() List<Field> 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上硬抠。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询