1.2 任务要求
要求在前台能够下载word报告,报告中包含图片和表格。
在网上搜索了很多解决方案,大部分是利用freemark模板生成word,作者也试过这种方法,有几个缺点比较明显:
需要在word模板中写
${name}
这种变量用于freemark替换,但是在word模板转换为
.xml
格式后
${name}
可能不会连在一起,而是中间有xml中的代码,需要手动解决。
word模板转换为
.xml
后代码量很大,动辄上千行,比较容易出错,一旦出错就是文档打不开的结果。
如果生成的模板中带有图片,通常解决方案是在springboot后台将图片转换为base64格式然后替换掉
.xml
模板中的图片代码,但是这对图片格式以及base64图片数据中的空格等等都有很严格的要求,需要非常仔细,同时也非常麻烦。作者遇到最常见的就是图片代码被替换掉,但是图片显示不出来,需要一步一步的测试,非常麻烦,可能也与作者经验不足有关。
最后作者决定使用poi在后端生成word文档,然后前台进行下载的方法解决。
1.3 涉及到的知识点
动态创建form表单并且向后端传参。
form表单如何传递对象中的数组。
后端接收form表单传递的对象。
java中使用poi生成word文档。
word文档中创建图片。
word文档的下载。
该文档下载接口的权限校验。
echarts中获取base64格式的图片。
二、数据结构
2.1 后端接收的数据结构
使用VO类接收数据
import lombok.Data;
import java.util.List;
* 接受数据的Vo类
@Data
public class TestVo {
* key
private String key;
* value
private String value;
* chList
private List<TestChildrenVo> chList;
import lombok.Data;
* 子类型Vo
@Data
public class TestChildrenVo {
* 图片Base64
private String img;
* 分页大小
private Integer pageSize;
* 当前页
private Integer pageNo;
2.2 前端的数据结构
let params = {
key: '111',
value: '222',
chList: [{
img: 'base64图片',
pageSize: 10,
pageNo: 1
img: 'base64图片2',
pageSize: 10,
pageNo: 2
三、前端数据发送
首先为什么要用js创建form表单的方法请求,因为我们要下载word文件,如果使用ajax则下载下的是文件流,需要进行转换,作者曾使用ajax然后下载转blob类型下载word文件,但是这种方式下载后的word文件内容错误,无法打开,所以使用form表单post的方式下载。
然后要把数据组装为2.2.2的那种数据结构
3.1 base64图片的获取
作者生成图片使用的是echarts,具体方法为:
* 生成图片
getImageBase64() {
return this.$refs.myChart.getImageBase64()
其中myChart
为echarts的实例
3.2 组建参数
这个就是postForm
方法的第二个接收参数
let params = {
key: '111',
value: '222',
chList: [{
img: 'base64图片',
pageSize: 10,
pageNo: 1
img: 'base64图片2',
pageSize: 10,
pageNo: 2
3.3 创建form表单
* 模拟post请求
* url: 请求地址
* params:参数
postForm(url, params) {
let temp_form = document.createElement('form')
temp_form.action = url
temp_form.target = '_self'
temp_form.method = 'post'
temp_form.style.display = 'none'
for (let x in params) {
if (params.hasOwnProperty(x)) {
if (x === 'chList') {
for (let i = 0; i < params[x].length; i++) {
for (let y in params[x][i]) {
let opt = document.createElement('textarea')
opt.name = 'chList[' + i + '].' + y
opt.value = params[x][i][y] ? params[x][i][y] : ''
temp_form.appendChild(opt)
} else {
let opt = document.createElement('textarea')
opt.name = x
opt.value = params[x] ? params[x] : ''
temp_form.appendChild(opt)
document.body.appendChild(temp_form)
temp_form.submit()
document.body.removeChild(temp_form)
以上代码组建的form大致为:
<form action="xxx" method="post">
<input name="key"/>
<input name="value"/>
<input name="chList[0].img"/>
<input name="chList[0].pageSize"/>
<input name="chList[0].pageNo"/>
<input name="chList[1].img"/>
<input name="chList[1].pageSize"/>
<input name="chList[1].pageNo"/>
<input name="chList[2].img"/>
<input name="chList[2].pageSize"/>
<input name="chList[2].pageNo"/>
</form>
关于form中如何提交数组的知识点:数组属性名称[索引].属性值
其中数组中的属性和TestChildrenVo
中的属性相对应
注意,如果某一个属性为undefined,后台接收到的值为字符串的undefined,所以所有的参数不能为null或者undefined,可以为空字符串。
四、后端处理
POI版本
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>3.17</version>
</dependency>
4.1 数据接收
TestVo
详见2.2.1数据结构
关于form提交数据的一些看法:本案例因为参数数据量较大,无法使用get请求,如果请求接口传递的参数不多并且数据量不是很大,则完全可以使用get
请求传递参数,具体方式为:
location.href = url + '?token=' + this.token + '&参数key=参数value'
其中token用于该接口的权限验证,需要后台手动验证,如果不需要验证则可以忽略。
后端数据接收方法:
* word报告导出
@PostMapping(value = "/export-word")
public void export(TestVo testVo,
HttpServletResponse response) throws Exception {
System.out.println(testVo.toString());
4.2 创建word文件
XWPFDocument doc = new XWPFDocument();
XWPFParagraph p = doc.createParagraph();
p.setAlignment(ParagraphAlignment.CENTER);
XWPFRun r = p.createRun();
r.setText("我是标题");
r.setBold(true);
r.setColor("000000");
r.setFontSize(21);
r.addCarriageReturn();
createParagraph(doc, "段落一");
createParagraph(doc, "段落一");
createParagraph(doc, "段落二");
List<TestChildrenVo> imgs = testVo.getChList();
for (TestChildrenVo img : imgs) {
XWPFParagraph pImg = doc.createParagraph();
pImg.setAlignment(ParagraphAlignment.CENTER);
XWPFRun rImg = pImg.createRun();
rImg.addCarriageReturn();
String imgData = img.getImg().substring(img.getImg().indexOf(",") + 1);
byte[] bytes = new BASE64Decoder().decodeBuffer(imgData);
rImg.addPicture(new ByteArrayInputStream(bytes), Document.PICTURE_TYPE_PNG, "123.png", Units.toEMU(400), Units.toEMU(180));
XWPFTable table = doc.createTable(imgs.size() + 1, 2);
CTTblWidth infoTableWidth = table.getCTTbl().addNewTblPr().addNewTblW();
infoTableWidth.setType(STTblWidth.DXA);
infoTableWidth.setW(BigInteger.valueOf(9072));
setTableFonts(table.getRow(0).getCell(0), "当前页");
setTableFonts(table.getRow(0).getCell(1), "分页大小");
for (int i = 1; i <= imgs.size(); i++) {
setTableFonts(table.getRow(i).getCell(0), imgs.get(i - 1).getPageNo() + "");
setTableFonts(table.getRow(i).getCell(1), imgs.get(i - 1).getPageSize() + "");
其中 String imgData = img.getImg().substring(img.getImg().indexOf(",") + 1);
这段代码为了截取base64的数据后半段,data:image/png;base64,
这些数据被截取掉,看下图:
4.3 文件下载
String fileName = "word导出测试_" + System.currentTimeMillis() + ".doc";
String fileNameURL = URLEncoder.encode(fileName, "UTF-8");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileNameURL + ";" + "filename*=utf-8''" + fileNameURL);
response.setContentType("application/octet-stream");
response.flushBuffer();
OutputStream ouputStream = response.getOutputStream();
doc.write(ouputStream);
ouputStream.flush();
ouputStream.close();
4.4 word文件展示
4.5 代码合并
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.misc.BASE64Decoder;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/test")
public class WordExportTest {
* word报告导出
@PostMapping(value = "/export-word")
public void export(TestVo testVo,
HttpServletResponse response) throws Exception {
System.out.println(testVo.toString());
XWPFDocument doc = new XWPFDocument();
XWPFParagraph p = doc.createParagraph();
p.setAlignment(ParagraphAlignment.CENTER);
XWPFRun r = p.createRun();
r.setText("我是标题");
r.setBold(true);
r.setColor("000000");
r.setFontSize(21);
r.addCarriageReturn();
createParagraph(doc, "段落一");
createParagraph(doc, "段落一");
createParagraph(doc, "段落二");
List<TestChildrenVo> imgs = testVo.getChList();
for (TestChildrenVo img : imgs) {
XWPFParagraph pImg = doc.createParagraph();
pImg.setAlignment(ParagraphAlignment.CENTER);
XWPFRun rImg = pImg.createRun();
rImg.addCarriageReturn();
String imgData = img.getImg().substring(img.getImg().indexOf(",") + 1);
byte[] bytes = new BASE64Decoder().decodeBuffer(imgData);
rImg.addPicture(new ByteArrayInputStream(bytes), Document.PICTURE_TYPE_PNG, "123.png", Units.toEMU(400), Units.toEMU(180));
XWPFTable table = doc.createTable(imgs.size() + 1, 2);
CTTblWidth infoTableWidth = table.getCTTbl().addNewTblPr().addNewTblW();
infoTableWidth.setType(STTblWidth.DXA);
infoTableWidth.setW(BigInteger.valueOf(9072));
setTableFonts(table.getRow(0).getCell(0), "当前页");
setTableFonts(table.getRow(0).getCell(1), "分页大小");
for (int i = 1; i <= imgs.size(); i++) {
setTableFonts(table.getRow(i).getCell(0), imgs.get(i - 1).getPageNo() + "");
setTableFonts(table.getRow(i).getCell(1), imgs.get(i - 1).getPageSize() + "");
String fileName = "word导出测试_" + System.currentTimeMillis() + ".doc";
String fileNameURL = URLEncoder.encode(fileName, "UTF-8");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileNameURL + ";" + "filename*=utf-8''" + fileNameURL);
response.setContentType("application/octet-stream");
response.flushBuffer();
OutputStream ouputStream = response.getOutputStream();
doc.write(ouputStream);
ouputStream.flush();
ouputStream.close();
* 创建段落
* @param text
private void createParagraph(XWPFDocument doc, String text) {
XWPFParagraph paragraph = doc.createParagraph();
paragraph.setFontAlignment(1);
XWPFRun run = paragraph.createRun();
run.setText(text);
run.setColor("000000");
run.setFontSize(14);
run.addCarriageReturn();
* 设置表格中字体
* @param cell
* @param cellText
private static void setTableFonts(XWPFTableCell cell, String cellText) {
CTP ctp = CTP.Factory.newInstance();
XWPFParagraph p = new XWPFParagraph(ctp, cell);
p.setAlignment(ParagraphAlignment.CENTER);
XWPFRun run = p.createRun();
run.setFontSize(14);
run.setText(cellText);
CTRPr rpr = run.getCTR().isSetRPr() ? run.getCTR().getRPr() : run.getCTR().addNewRPr();
CTFonts fonts = rpr.isSetRFonts() ? rpr.getRFonts() : rpr.addNewRFonts();
fonts.setAscii("仿宋");
fonts.setEastAsia("仿宋");
fonts.setHAnsi("仿宋");
cell.setParagraph(p);
五、接口权限校验
SpringBoot后端权限校验使用的是jwt,需要在请求头中添加token;但是form表单无法添加请求头,则token要添加到post的参数中,然后在后端接收token进行手动校验用户是否登陆或者其他权限。如果该接口不需要进行认证,则可以直接放行。