阅读完需:约 11 分钟
因为业务的需要,要解决报表样式的复杂度问题,每一个中国式报表的复杂度都是极其变态的,所以可以通过模版的方式来导出Excel样式,而不是通过代码来构造Excel的样式,这就是不采用原生的POI与EasyExcel等框架的原因。
官方文档:https://jxls.sourceforge.net/index.html
maven中添加依赖
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls-poi</artifactId>
<version>2.12.0</version>
</dependency>
<!-- 用来导出excel用的-->
<dependency>
<groupId>net.sf.jxls</groupId>
<artifactId>jxls-core</artifactId>
<version>1.0.6</version>
</dependency>
<!-- 用于读取内容 xml读取用的依赖 -->
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls-reader</artifactId>
<version>2.0.6</version>
</dependency>
除了对核心 Jxls 模块的依赖之外,我们需要添加对 Jxls 转换器引擎实现的依赖,该引擎将执行所有底层 Java 到 Excel 操作。
Jxls 核心模块不依赖于任何特定的 Java-Excel 库,而是通过预定义的接口专门与 Excel 一起工作。目前,Jxls 基于众所周知的Apache POI和Java Excel API 库,在不同的模块中提供了该接口的两种实现。
其中最关键的是Transformers
接口,是通过这个来转换获取不同的实现。
基础介绍
- 使用批注的模式编写指令
- 需在excel的第一个单元格指定整个模板的范围
- 单元格中取值与el表达式类似使用${}来访问传入的变量
- 指令中需要访问对象则不需要使用${} 直接访问即可,需用””包裹
在JXLS中最关键的是要制定填充的 XLS 区域,一共有三种方式来指定 XLS 区域
XLS 区域:分顶级区域,填充区域
有 3 种方法来构建 XLS 区域
- 使用 Excel 标记
- 使用 XML 配置
- 使用 Java API
这里更只说明Excel 标记与使用 Java API,更倾向于Java API
常用指令说明
jx:each 循环
jx:each((items="employees" var="employee" lastCell="D4" area="[A4:D4]" select="employee.payment > 2000"))
Java API
// creating a transformer and departments collection
...
// creating department XlsArea
XlsArea departmentArea = new XlsArea("Template!A2:G13", transformer);
// creating Each Command to iterate departments collection and attach to it "departmentArea"
EachCommand departmentEachCommand = new EachCommand("department", "departments", departmentArea);
每个命令具有以下属性:
- items是包含要迭代的集合 (Iterable<?>) 或数组的上下文变量的名称
- var是 Jxls 上下文中变量的名称,用于在迭代时放置每个新的集合项
- varIndex是 Jxls 上下文中的变量名称,它保存当前迭代索引,从零开始
- direction是Direction枚举的值,它可能具有值DOWN或RIGHT以指示如何重复命令主体 – 按行或按列。默认值为向下。
- select是一个表达式选择器,用于在迭代过程中过滤掉集合项
- groupBy是一个进行分组的属性(在 var 名称前加上“.”)
- groupOrder指示组的排序(’desc’ 或 ‘asc’)
- orderBy包含以逗号分隔的属性名称,每个属性名称都有一个可选的后缀“ASC”(默认)或“DESC”用于排序顺序。您应该在 var 名称前加上“.” 在每个属性名称之前。
- multisheet是上下文变量的名称,其中包含要输出集合的工作表名称列表
- cellRefGenerator是用于创建目标单元格引用的自定义策略
- area是对用作每个命令主体的 XLS 区域的引用
- lastCell是指向命令区域最后一个单元格的任何命令的公共属性
var和items属性是必需的,而其他属性可以跳过。
循环变量var和varIndex的值将使用特殊方法 Context.getRunVar() 保存。这允许您在值不可用时单独做出反应。
jx:if 条件判断
jx:if(condition="employee.payment <= 2000", lastCell="F9", areas=["A9:F9","A18:F18"])
Java API
// ...
// creating 'if' and 'else' areas
XlsArea ifArea = new XlsArea("Template!A18:F18", transformer);
XlsArea elseArea = new XlsArea("Template!A9:F9", transformer);
// creating 'if' command
IfCommand ifCommand = new IfCommand("employee.payment <= 2000", ifArea, elseArea);
If -Command具有以下属性
- condition是要测试的条件表达式
- ifArea是当此命令条件计算为真时要输出的区域的引用
- elseArea是当此命令条件计算为false时要输出的区域的引用
- lastCell是指向命令区域最后一个单元格的任何命令的公共属性
ifArea和condition属性是必需的。
jx:updateCell 动态输出公式
主要用于动态修改模板中被循环包裹的的公式
jx:updateCell(lastCell="E4" updater="totalCellUpdater")
UpdateCell -Command具有以下属性
- updater是包含CellDataUpdater实现的上下文中的键的名称
- lastCell是指向命令区域最后一个单元格的任何命令的公共属性
static class TotalCellUpdater implements CellDataUpdater{
/**
* cellData 批注对应的单元格
* targetCell 输出的单元格
* context 模板中的上下文通过getVar(变量名)来获取传入的对象
*/
@Override
public void updateCellData(CellData cellData, CellRef targetCell, Context context) {
if( cellData.isFormulaCell() && cellData.getFormula().equals("SUM(E2)")){
String resultFormula = String.format("SUM(E2:E%d)", targetCell.getRow());
cellData.setEvaluationResult(resultFormula);
}
}
}
jx:grid 输出一个表格
一次性输出一个表格包含表头表体
Grid-Command具有以下属性
- headers – 包含标题集合的上下文变量的名称
- data – 包含数据集合的上下文变量的名称
- 道具- 每个网格行的逗号分隔的对象属性列表(仅当每个网格行都是对象时才需要)
- formatCells – 类型格式地图单元格的逗号分隔列表,例如 formatCells=“Double:E1, Date:F1”
- headerArea – 标题的源 xls 区域
- bodyArea – 正文的源 xls 区域
- lastCell是指向命令区域最后一个单元格的任何命令的公共属性
数据变量可以是以下类型
- Collection<Collection<Object>> – 这里每个内部集合都包含对应行的单元格值
- Collection<Object> – 这里每个集合项都是一个包含相应行数据的对象。在这种情况下,您必须指定props属性来定义应该使用哪些对象属性来设置特定单元格的数据。
try(InputStream is = GridCommandDemo.class.getResourceAsStream("grid_template.xls")) {
try(OutputStream os = new FileOutputStream("grid_output2.xls")) {
Context context = new Context();
context.putVar("headers", Arrays.asList("Name", "Birthday", "Payment"));
context.putVar("data", employees);
//当循环的表体为javabean时指定读取的属性,Sheet2!A1表示输出开始的位置
JxlsHelper.getInstance().processGridTemplateAtCell(is, os, context, "name,birthDate,payment,bonus", "Sheet2!A1");
}
}
jx:image 输出图片
输出一张图片
jx:image(lastCell="D10" src="image" imageType="PNG")
参数名称 | 示例 | 必填 | 说明 |
src | src=”image” | 必填 | 输出的图片数据源byte[] |
imageType | imageType=”PNG” | 输出的图片格式可不填 |
public static void execute2() throws IOException {
try(InputStream is = ImageDemo.class.getResourceAsStream(template2)) {
try (OutputStream os = new FileOutputStream(output2)) {
Context context = new Context();
InputStream imageInputStream = ImageDemo.class.getResourceAsStream("business.png");
byte[] imageBytes = Util.toByteArray(imageInputStream);
Department department = new Department("Test Department");
department.setImage(imageBytes);
context.putVar("dep", department);
JxlsHelper.getInstance().processTemplate(is, os, context);
}
}
}
自定义 Jexl 处理
如果需要自定义 Jexl 处理,您可以从Transformer获取对JexlEngine的引用并应用必要的配置。
例如下面的代码在demo命名空间下注册了一个自定义的 Jexl 函数
public Integer mySum(Integer x, Integer y){
return x + y;
}
Transformer transformer = TransformerFactory.createTransformer(is, os);
...
JexlExpressionEvaluator evaluator = (JexlExpressionEvaluator) transformer.getTransformationConfig().getExpressionEvaluator();
Map<String, Object> functionMap = new HashMap<>();
functionMap.put("demo", new JexlCustomFunctionDemo());
evaluator.getJexlEngine().setFunctions(functionMap);
${demo:mySum(x,y)}
案例
execl标识
@Test
public void jxls2() throws IOException {
ArrayList<MultipartData> multipartData = selectSQL.selectResultSet(selectSQL1 -> "SELECT * FROM htpk.report_forms_input_output_one WHERE enterprise_id=57 and year=2022 and month=3");
try(InputStream is = new FileInputStream("/Users/xxxx/Me/IDEA/Necrozma/Zacian-888/src/main/java/com/enmalvi/zacian888/controller/JxlsExcel/one2.xls")){
try (OutputStream os = new FileOutputStream("target/one2-02.xls")) {
Context context = new Context();
context.putVar("tests", multipartData);
JxlsHelper.getInstance().processTemplateAtCell(is, os, context,"Result!A1");
}
}
}
这种方式是通过Jxls的transformer
接口来访问实现这个transformer
接口的转换类,比如如果底层是POI那么后面实际调用的是PoiTransformer transformer = createTransformer(is);
或者可以直接调用POI的转换类来进行
// getting input stream for our report template file from classpath
InputStream is = ObjectCollectionDemo.class.getResourceAsStream("object_collection_template.xls");
// creating POI Workbook
Workbook workbook = WorkbookFactory.create(is);
// creating JxlsPlus transformer for the workbook
PoiTransformer transformer = PoiTransformer.createTransformer(workbook);
// creating XlsCommentAreaBuilder instance
AreaBuilder areaBuilder = new XlsCommentAreaBuilder(transformer);
// using area builder to construct a list of processing areas
List<Area> xlsAreaList = areaBuilder.build();
// getting the main area from the list
Area xlsArea = xlsAreaList.get(0);
关键在于
PoiTransformer transformer = PoiTransformer.createTransformer(workbook);
AreaBuilder areaBuilder = new XlsCommentAreaBuilder(transformer);
List<Area> xlsAreaList = areaBuilder.build();
Java API 填充
@Test
public void jxlsApiME() throws ParseException, IOException {
ArrayList<MultipartData> employees = selectSQL.selectResultSet(selectSQL1 -> "SELECT * FROM htpk.report_forms_input_output_one WHERE enterprise_id=57 and year=2022 and month=3");
log.info("Running Object Collection JavaAPI demo");
try (InputStream is = new FileInputStream("/Users/xxxxx/Me/IDEA/Necrozma/Zacian-888/src/main/java/com/enmalvi/zacian888/controller/JxlsExcel/one.xls")) {
try (OutputStream os = new FileOutputStream("target/reportFormsInputOutputOne.xls")) {
// 转换地址
Transformer transformer = TransformerFactory.createTransformer(is, os);
// 创建一个新的 XlsArea,指定模板文件的区域(顶级区域)
XlsArea xlsArea = new XlsArea("Template!A1:I44", transformer);
// 数据填充区域
XlsArea employeeArea = new XlsArea("Template!A2:I43", transformer);
// 数据循环填充
EachCommand employeeEachCommand = new EachCommand("reportFormsInputOutputOne", "employees", employeeArea);
// 添加区域到顶级区域中
xlsArea.addCommand("A2:I43", employeeEachCommand);
Context context = new Context();
// 设置数据集
context.putVar("employees", employees);
// 最后需要输出到那个标签页面里,从哪个单元格开始写入
xlsArea.applyAt(new CellRef("Result!A1"), context);
transformer.write();
}
}
}
最后结果
API 工具封装
@RestController
public class ExcelUtil {
@Autowired
SelectSQL selectSQL;
/**
* 出口
*
* @param templatePath 模板路径
* @param map 地图
* @param topArea 顶部区域
* @param specifiedArea 指定区域
* @param variateContext 变量上下文
* @param os 操作系统
* @throws IOException ioexception
*/
public void export(String templatePath,
List<MultipartData> map,
String topArea,
String specifiedArea,
String variateContext,
OutputStream os
)throws IOException {
try (InputStream is = Files.newInputStream(Paths.get(templatePath))) {
Transformer transformer = TransformerFactory.createTransformer(is, os);
XlsArea xlsArea = new XlsArea("Template!"+topArea, transformer);
XlsArea employeeArea = new XlsArea("Template!"+specifiedArea, transformer);
EachCommand employeeEachCommand = new EachCommand(variateContext, "items", employeeArea);
xlsArea.addCommand(specifiedArea, employeeEachCommand);
Context context = new Context();
context.putVar("items", map);
xlsArea.applyAt(new CellRef("Result!A1"), context);
transformer.write();
}
}
/**
* 报告前端下载
*
* @param response 响应
* @throws IOException ioexception
*/
@GetMapping("/jxls")
public void report(String reportName,HttpServletResponse response) throws IOException {
OutputStream os=response.getOutputStream();
//如果想下载试自动填好文件名,需要设置Content-Disposition响应头
response.setHeader("Content-Disposition", "attachment;filename=" + reportName);
response.setContentType("application/vnd.ms-excel");
List<MultipartData> employees = selectSQL.selectResultSet(selectSQL1 -> "SELECT * FROM htpk.report_forms_input_output_one WHERE enterprise_id=57 and year=2022 and month=3");
export("/Users/xxxx/Me/IDEA/Necrozma/Zacian-888/src/main/java/com/enmalvi/zacian888/controller/JxlsExcel/one.xls",
employees,"A1:I44","A2:I43","reportFormsInputOutputOne",os);
}
}
导出工具可以通过jxls-core
依赖来实现,但是上面的例子没有用,下面的例子是用了
public static ResponseEntity<byte[]> downLoadExcel(String sourcePath, Map<String, Object> beanParams)
throws ParsePropertyException, InvalidFormatException, IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
//读取模板
InputStream is =ExcelUtil.class.getClassLoader().getResourceAsStream(sourcePath);
XLSTransformer transformer = new XLSTransformer();
//向模板中写入内容
Workbook workbook = transformer.transformXLS(is, beanParams);
//写入成功后转化为输出流
workbook.write(os);
//配置Response信息
HttpHeaders headers = new HttpHeaders();
String downloadFileName = UUID.randomUUID().toString() + ".xlsx";
//防止中文名乱码
downloadFileName = new String(downloadFileName.getBytes("UTF-8"), "ISO-8859-1");
headers.setContentDispositionFormData("attachment", downloadFileName);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
//返回
return new ResponseEntity<byte[]>(os.toByteArray(), headers, HttpStatus.CREATED);
}
@RequestMapping("/export")
//返回ResponseEntity<byte[]>使浏览器下载
public ResponseEntity<byte[]> exportExcel(HttpServletRequest request, HttpServletResponse response) throws Exception {
//查询参数
Map<String, Object> params = new HashMap<String, Object>();
//结果集
List<Aip_std> list = stdService.selectAllForExportExcel(params);
Map<String, Object> beanParams = new HashMap<String, Object>();
beanParams.put("list", list);
//下载表格
return ExcelUtil.downLoadExcel("static/excel/aaa.xlsx",beanParams);
}