类似这样的一个表格
我们需要从数据库中查询对应的数据,将其汇总进该表格,并且可能还需要复制表格项,我这个案例中没有,只是一个动态列表,这时候我们可以分如下几个步骤。
我们以面向对象的思想来看, 编号一行 其实是一个列表,而剩余部分,则是表格的主题,这样构建对象的对象组成就是对象中包含一些属性,一个列表,其中一个列表再包含一些属性。
之前我对于这种固定模板,再套动态列表的导出,其实是有点懵的,但工作中碰到了,就要迎难而上,这篇文章就是来做一个梳理总结。
生成的对象如下所示
Invoice类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Invoice {
// 客户名称
private String customerName;
// 客户编码
private String customerCode;
// 日期
private Date date;
// 总合计大写
private String totalAmountInWords;
// 合计
private BigDecimal totalAmount;
// 主管
private String supervisor;
// 财务
private String finance;
// 保管员
private String custodian;
// 经手人
private String handler;
List<InvoiceItem> invoiceItemList;
}
InvoiceItem类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class InvoiceItem {
// 编号
private String itemNumber;
// 名称及规格
private String itemNameAndSpecs;
// 单位
private String unit;
// 数量
private int quantity;
// 单价
private BigDecimal unitPrice;
// 金额
private BigDecimal amount;
// 备注
private String comments;
}
然后先不要将表格转化为xml格式,我们可以先写freemarker表达式,在空位插值,否则等xml已转化,就很不直观了。
插值如下。
可以看到我把中间的空行删了,因为我们会遍历列表,所以那些空行是没有用的。
重要的事!此刻先确定好表格的样式,否则等你转换为ftl,写好循环什么的,再改样式将会变得很困难,特别是更复杂的表格。
接着将对应的表格另存为xml。
xml格式化
重点!
格式化后一定要再打开文件看一下样式!
格式化后一定要再打开文件看一下样式!
格式化后一定要再打开文件看一下样式!
我不知道是格式化网站的问题还是什么,格式化后的样式竟然会变,上面的这个就很靠谱。
我们可以将格式化后的代码复制到该文件 /templates/demo.ftl
你们随意命名即可。
我们找到worksheet这一行,下面的就是我们要更改的地方了。
因为我们只需要遍历列表即可,所以只需要在编号填充的上面引入列表即可,不要忘了在对应的地方结束循环。
service代码如下
@Service
@Slf4j
public class ExcelService {
/**
* 1.查询对象
* 2.封装map
* 3.指定模板与导出文件名称
* 4.响应设置 不设置会导致乱码 文件格式出错等问题
* 5.模板配置
* 6.变量替换
* 7.文件输出
* @param id 商品id
*/
public void export(Integer id, HttpServletResponse response) {
//这里假装查询对应的对象 我处于方便直接让ai生成了
Invoice invoice = getById(1);
Map<String, Object> model = new HashMap<>();
//要与ftl中的对象一致
model.put("invoice", invoice);
// 指定FreeMarker模板文件的位置
String templateFilePath = "demo.ftl";
String excelName = "商品出库单" + ".xlsx";
FileInputStream inputStream = null;
ServletOutputStream outputStream = null;
File file = null;
// 使用FreeMarker加载模板文件
try {
response.reset();
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(excelName, "UTF8"));
response.setCharacterEncoding("utf-8");
response.setContentType("application/vnd.ms-excel;charset=utf-8");
Configuration cfg = new Configuration(Configuration.getVersion());
// 指定FreeMarker模板文件的位置
String path = "/templates/";
cfg.setClassForTemplateLoading(getClass(), path);
Template template = cfg.getTemplate(templateFilePath);
file = new File(excelName);
FileWriter out = new FileWriter(excelName);
// 变量替换
template.process(model, out);
// 将文件输出到response,返回给客户端
inputStream = new FileInputStream(file);
byte[] buffer = new byte[inputStream.available()];
inputStream.read(buffer);
inputStream.close();
outputStream = response.getOutputStream();
outputStream.write(buffer);
outputStream.flush();
outputStream.close();
} catch (Exception e) {
log.error("出库单导出失败", e);
throw new RuntimeException(e);
}
}
public static Invoice getById(Integer id) {
// 创建一个 Invoice 对象
Invoice invoice = new Invoice();
invoice.setCustomerName("客户名称示例");
invoice.setCustomerCode("客户编码示例");
invoice.setDate(new Date());
invoice.setTotalAmountInWords("总合计大写示例");
invoice.setTotalAmount(new BigDecimal("1000.00"));
invoice.setSupervisor("主管示例");
invoice.setFinance("财务示例");
invoice.setCustodian("保管员示例");
invoice.setHandler("经手人示例");
// 创建并填充 5 个 InvoiceItem 对象,然后添加到 Invoice 的列表中
List<InvoiceItem> invoiceItems = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
InvoiceItem item = new InvoiceItem();
item.setItemNumber("编 号 " + i);
item.setItemNameAndSpecs("名称及规格示例 " + i);
item.setUnit("单位示例");
item.setQuantity(10 * i);
item.setUnitPrice(new BigDecimal("10.00"));
item.setAmount(new BigDecimal("100.00"));
item.setComments("备注示例 " + i);
invoiceItems.add(item);
}
invoice.setInvoiceItemList(invoiceItems);
return invoice;
}
}
controller 调用
@RestController
public class ExportController {
@Autowired
private ExcelService excelService;
@GetMapping("/export")
public void export(Integer id, HttpServletResponse response) {
excelService.export(1, response);
}
}
生成的表格如下所示,可以看到,效果还不错的。
但是!!!!!
你以为这就完了吗? 我之前做的表格比这复杂很多,导致遇到了很多错误。
比如文件路径错误,模板错误excel格式不支持错误。样式不整齐,乱码,读出错误。
现在一一列举下我犯的错,所以这不单单是一个教学贴,也是对我错误的一个总结。
1.文件路径错误。
在写这段代码的时候,由于路径藏的很深,且我以为第二个参数不能直接这么用,要填写全路径,导致文件一致读取失败,现在明白了,只需要直接填充根路径往下的内容即可。
// 指定FreeMarker模板文件的位置
String path = "/templates/";
cfg.setClassForTemplateLoading(getClass(), path);
2.对异常的不关注。
之前读取文件失败,还有个原因是ftl语法错误,但我打的异常并不明显,导致我一直没看到这个错误,计算把文件路径修复了,还是会报错,其实是找错了方向,提醒大家要注意异常,当时是日志太多了,确实忽略了这一点。
3.excel文件转化打不开。
其中一点,我们打开之前的xml文件,找到这两个字段
建议将这两个值改的大一点,否则文件格式会出问题,我这里是因为数据量少没出问题,但真实使用就不一定了,改个几百几千都可以。
还有一点,之前xml进行了格式化,但到了idea中,头文件内容错乱了,这点才导致文件无法打开,建议从之前的xml模板中复制头文件,在原基础上修改即可。
4.乱码
特别是中文命名的文件,一定要设置utf8格式,之前的response的设置可不是白加的。
5.freemarker空值问题
一旦某一个地方,比如日期,列表之类的为空,整个渲染就报错了,所以一定要提前判断某些地方会不会为null,提前设置默认值,一般来讲在表达式后面加!即可,当然只针对于字符串什么的。
6.遇到bug时的盲目
那时因为需求急,就算碰到了问题,也是一个劲的百度,cv大法,其实那时候静下心来,稍微放松一下,理清思路,也许很快就能解决问题,但知易行难,真正做起来才发现很有难度。
虽然我以前诟病freemarker很垃圾,只要一个语法出问题,就生成不了html页面了,但相比于用poi将复杂的excel导出,freemarker其实更好用,并且复杂的excel其实就是在原基础上做了更多的填充,字段复制罢了,原理不复杂,但对于没做过的人来说,可能会稍微有点恐惧,我的案例告诉你,只要咬牙迈过去就好了。
一个人最大的幸福莫过于在人生的中途、富有创造力的壮年,发现自己此生的使命。