POI生成Word文档
使用POI XWPF生成Word文档,引入POI:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.0.0</version>
</dependency>
模板替换
项目中经常从Word模板生成文档,下面提供了替换文档内容的工具类。模版中要替换的内容以${key}标识,两个${key}间至少要有一个字符分隔。
package com.ezp.resumes.util;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class XWPFDocumentUtils {
private static final String KEG_REGEX = "\\$\\{(\\w+)}";
private static final Pattern KEY_PATTERN = Pattern.compile(KEG_REGEX);
private XWPFDocumentUtils() {
}
/**
* 从Word模板生成文档,替换其中的${key}
*
* @param templatePath 模板文件路径
* @param fields 替换的键值对
* @return byte[]
* @throws IOException
*/
public static byte[] generateDocument(String templatePath, Map<String, String> fields) throws IOException {
try (XWPFDocument doc = new XWPFDocument(new FileInputStream(path))) {
replaceDoc(doc, fields);
return output(doc);
}
}
private static void replaceDoc(XWPFDocument doc, Map<String, String> fields) {
for (XWPFParagraph paragraph : doc.getParagraphs()) {
if (!paragraph.getText().contains("${")) {
continue;
}
replaceText(paragraph, fields);
}
}
private static void replaceText(XWPFParagraph paragraph, Map<String, String> fields) {
List<XWPFRun> runs = paragraph.getRuns();
for (int i = 0; i < runs.size(); i++) {
XWPFRun run = runs.get(i);
String text = run.text();
if (isContainsKey(text, runs, i)) {
// 删除${之后的XWPFRun, 并将${部分和删除内容保存到StringBuilder中
StringBuilder builder = new StringBuilder(text);
while (!text.contains("}")) {
text = runs.get(i + 1).text();
builder.append(text);
paragraph.removeRun(i + 1);
}
text = builder.toString();
// 替换内容
String key = getKey(text);
if (key != null && fields.containsKey(key)) {
String value = fields.get(key);
text = text.replaceFirst(KEG_REGEX, value == null ? "" : value);
}
run.setText(text, 0);
}
}
}
private static boolean isContainsKey(String text, List<XWPFRun> runs, int i) {
return text.contains("${") || (text.contains("$") && runs.get(i + 1).text().startsWith("{"));
}
private static byte[] output(XWPFDocument doc) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
doc.write(out);
return out.toByteArray();
}
}
private static String getKey(String text) {
Matcher matcher = KEY_PATTERN.matcher(text);
return matcher.find() ? matcher.group(1) : null;
}
}
POI常用参数
使用POI创建Word文档的常用参数:
XWPFParagraph paragraph = doc.createParagraph();
// 设置段落样式
paragraph.setAlignment(ParagraphAlignment.LEFT); // 左对齐
paragraph.setIndentationFirstLine(55); // 首行缩进0.1厘米
paragraph.setSpacingBeforeLines(50); // 段前0.5行
paragraph.setSpacingAfterLines(50); // 段后0.5行
paragraph.setSpacingBetween(1.25, LineSpacingRule.AUTO); // 1.25倍行距
CTP ctp = paragraph.getCTP();
CTPPr ctpPr = ctp.addNewPPr();
// 设置大纲级别2级
ctpPr.addNewOutlineLvl().setVal(BigInteger.valueOf(1));
// 设置底纹样式
CTShd shd = ctpPr.addNewShd();
shd.setFill("dbe5f1"); // 填充色
shd.setVal(STShd.CLEAR); // 图案样式:清除
shd.setColor("auto"); // 图案颜色:自动
// 设置段落文本
XWPFRun run = paragraph.createRun();
run.setText("Hello POI");
run.setFontFamily("宋体"); // 字体
run.setFontSize(10); // 字号
run.setCharacterSpacing(15); // 加宽字体,0.75磅
run.setCapitalized(true); // 大写
run.setBold(true); // 粗体
Spring Boot Rest API
Rest API
下载文档API使用@GetMapping,并指定MediaType为APPLICATION_OCTET_STREAM_VALUE。调用generateDocument()方法生成word文档,若要指定文件名称,返回类型要使用ResponseEntity并增加"Content-Disposition" header,否则可以直接返回byte[]。
@GetMapping(value ="/api/doc/{heroName}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<byte[]> downloadDocument(@PathVariable String heroName) {
try {
Map<String, String> fields = new HashMap<>();
fields.put("hero_name", heroName);
fields.put("create_date", "2019年6月");
byte[] bytes = XWPFDocumentUtils.generateDocument("template/hero.docx", fields);
HttpHeaders headers = new HttpHeaders();
String filename = "你好POI.docx";
headers.add("Content-Disposition", "attachment; filename=" + encode(filename));
return ResponseEntity.ok().headers(headers).body(bytes);
} catch (Exception e) {
throw new XWPFDocumentException(e.getMessage());
}
}
// 中文文件名称需要使用URL编码
private String encode(String filename) {
try {
return URLEncoder.encode(filename, "UTF-8");
} catch (UnsupportedEncodingException e) {
return filename;
}
}
CORS
配置CORS的ExposedHeaders,否则前台不能读取"Content-Disposition":
/**
* CORS配置
*/
@Configuration
@Slf4j
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("Accept", "Accept-Encoding", "Accept-Language", "Authorization", "Connection", "Content-Type", "Host", "Origin", "Referer", "User-Agent", "X-Requested-With")
.exposedHeaders("Content-Disposition");
}
};
}
}
Test
测试使用exchange方法,设置header APPLICATION_OCTET_STREAM:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HeroesApplicationTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void getDocumentSuccess() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM));
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<byte[]> response = restTemplate.exchange("/api/doc/jason", HttpMethod.GET, entity, byte[].class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
Angular下载文档
可以直接使用链接访问REST URL下载文档,若项目启用了JWT Token验证,则必须使用HttpClient的get方法。 本文使用了FileSaver.js保存文档,开始之前先安装:
npm install --save file-saver
然后在tsconfig.json中添加:
"paths": {
"file-saver": [
"node_modules/file-saver/dist/FileSaver.js"
]
}
下载文档,使用API提供的文件名称:
import * as fs from 'file-saver';
downloadDocument() {
this.httpClient.get('yourUrl', {observe: 'response', responseType: 'blob'}).subscribe(response => {
fs.saveAs(response.body, this.getFilename(response.headers));
});
}
private getFilename(headers: HttpHeaders): string {
const disposition = headers.get('Content-Disposition');
if (!disposition || disposition.indexOf('filename=') < 0) {
return '';
}
const filename = disposition.split('=')[1];
return decodeURI(filename);
}
或前台自定义名称:
downloadDocument() {
this.httpClient.get('yourUrl', {responseType: 'blob'}).subscribe(data => {
fs.saveAs(data, 'yourFilename');
});
}
参考文档
Excel File – Download from SpringBoot RestAPI + Apache POI + MySQL Apache POI Word Tutorial