章最后提供源码下载地址
市面上处理文字的的办公软件有很多,包括WPS、MSOffice、永中OFFICE,当然还有开源的openoffice、liboffice等。我们在项目开发过程中经常会遇到预览word文件,数据库中数据自动填充word模板等需求。现在能够满足以上需求的技术有很多,服务端可通过POI\aspose等处理,也可通过客户端调用OFFICE组件处理,本人曾经在这方便做了很多测试,最终发现兼容性最好的、接口对JAVA程序员最友好的就属永中OFFICE,因为它基本就是JAVA实现的,使用起来非常方便。
我的测试环境使用的是永中2016版本,它运行要求JRE1.6,且我发现它应该是对JRE进行过重构,按永中SDK要求编写代码通过自ORACAL官网下载的jdk1.6编译后运行是失败的,现在都2021年了,我们的项目绝大多数都JDK1.8以上版本了,那么怎么让SDK兼容我们的项目呢?怎么实现标题中提到的两个需求呢?下面我说说我的处理方法吧:
1、下载永中软件并安装(官网下载即可)
2、安装后打开安装路径可以看到如下图
永中软件安装目录
JRE:即永中软件的运行环境
Yozo_Office.jar: 即永中为开发者提供的SDK,可以将jar导入到工程中
3、编写WORD文件处理服务组件
处理word文件的代码片段,详细代码请在文后下载源码查阅
/**
* 将word文件转换为对应格式的文件的字节数组
* @param type 将word文件转换成的文件格式 pdf、html\ofd\txt\xml
* @return
* @throws IOException
*/
public byte[] convertFile(String type) throws IOException {
int typePdf = FileConstants.TYPE_PDF;
if("html".equals(type.toLowerCase())) {//此功能转换后乱码,后期可采用 this.workbook.saveAs("D:/2.html"); 方式存储html后,将字节返回
typePdf= FileConstants.FILETYPE_HTML;
}else if("ofd".equals(type.toLowerCase())) {
typePdf= FileConstants.TYPE_OFD; // 这个是不成功的,应该是版本太低
}else if("txt".equals(type.toLowerCase())) {
typePdf = FileConstants.TYPE_TXT;
}else if("xml".equals(type.toLowerCase())) {
typePdf = FileConstants.FILETYPE_XML;
}else if("doc".equals(type.toLowerCase())||"xls".equals(type.toLowerCase())||"ppt".equals(type.toLowerCase())) {
typePdf = FileConstants.TYPE_MS;
}else if("docx".equals(type.toLowerCase())||"xlsx".equals(type.toLowerCase())||"pptx".equals(type.toLowerCase())) {
typePdf = FileConstants.TYPE_MS_EX;
}
return this.workbooks.getWorkbookAsByteArray(workbook, typePdf);
}
/**
* 替换word模板中的书签
* @param jsonObject 数据内容 {“bookmarkname”:”test“}
*/
public void replaceBookMark(JSONObject jsonObject) {
BookMarks bookMarks = this.document.getBookMarks();
BookMark[] allBookmarks = bookMarks.getAllBookmarks();
for(BookMark bookMark:allBookmarks){
String name = bookMark.getName();
TextRange range = bookMark.getRange();
//if(name!=null)name=name.replace("PO_","");
String value = "";
Object o = jsonObject.get(name);
if(o!=null){
value=jsonObject.get(name).toString();
}
try {
range.insertText(value);
}catch (Exception e){
range.insertText(value);
}
}
}
/**
* 导出数据成excel文件
* @param jsonObject 数据内容 {“bookmarkname”:”test“}
*/
public byte[] exportData2File(JSONArray taskArray,int allrow) {
}
4、(重点)解决word文件处理组件与我们的项目文件交互问题
本人通过SOCKET即时通讯服务解决数据交互问题
/**
* 文件传输Server端<br>
* 功能说明:
* @Author 空中智囊
* @Date 2016年09月01日
* @version 1.0
*/
public class SocketService extends ServerSocket {
private static final int SERVER_PORT = 8899; // 服务端端口
private WordUtil wordUtil=null;
public SocketService() throws Exception {
super(SERVER_PORT);
this.wordUtil=new WordUtil();
}
/**
* 使用线程处理每个客户端传输的文件
* @throws Exception
*/
public void load() throws Exception {
System.out.println("服务端启动,监听端口为:"+SERVER_PORT);
while (true) {
// server尝试接收其他Socket的连接请求,server的accept方法是阻塞式的
Socket socket = this.accept();
socket.setSoTimeout(1200000);
/**
* 我们的服务端处理客户端的连接请求是同步进行的, 每次接收到来自客户端的连接请求后,
* 都要先跟当前的客户端通信完之后才能再处理下一个连接请求。 这在并发比较多的情况下会严重影响程序的性能,
* 为此,我们可以把它改为如下这种异步处理与客户端通信的方式
*/
// 每接收到一个Socket就建立一个新的线程来处理它
new Thread(new Task(socket,wordUtil)).start();
}
}
/**
* 入口
* @param args
*/
public static void main(String[] args) {
try {
SocketService server = new SocketService(); // 启动服务端
server.load();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 处理客户端传输过来的文件线程类
*/
public class Task implements Runnable {
@Override
public void run() {
System.out.println("===客户端连接成功=====");
System.out.println("****************************************************************");
SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 转换要求的格式
*/
try {
/********************************读取文件信息********************************/
dis = new DataInputStream(socket.getInputStream());
// 文件名和长度
String fileName = dis.readUTF();//1、文件名字
long fileLength = dis.readLong();//2、长度
String toext = dis.readUTF();//3、扩展名
String taskType=dis.readUTF();//4、文件操作类型
System.out.println("针对文件的操作类型====="+taskType);
String valueObject=dis.readUTF();//5、替换书签的值
System.out.println(format.format(new Date())+":开始接收文件");
ByteArrayOutputStream bos = new ByteArrayOutputStream((int)fileLength);
byte[] bytes = new byte[1024];
int length = 0;
while((length = dis.read(bytes, 0, bytes.length)) != -1) {
bos.write(bytes, 0, length);
}
byte[] filebytes = bos.toByteArray();
System.out.println("原始文件大小====="+fileLength+",实际接收文件大小="+filebytes.length);
/********************************读取文件信息结束********************************/
dos = new DataOutputStream(socket.getOutputStream());
/********************************校验文件信息********************************/
boolean process=true;
if(fileLength>0){
}else{
dos.writeUTF("error");
dos.flush();
dos.writeUTF("文件没有任何内容,请重新传送");
dos.flush();
process=false;
}
if(filebytes.length!=fileLength){
dos.writeUTF("error");
dos.flush();
dos.writeUTF("接受文件与实际文件大小不符合,请重新传送文件");
dos.flush();
process=false;
}
/********************************校验文件信息结束********************************/
/********************************处理文件********************************/
if(process){
byte[] fileBytes=null;
this.wordUtil.openFile(filebytes,fileName);//打开院文件
//workbook =workbooks.createWorkbookFromByteArray(filebytes,fileName);
String lowerExt = toext.toLowerCase();
if("convertFile".equals(taskType)){
System.out.println("开始将文件["+fileName+"]转换成===="+lowerExt);
fileBytes=this.wordUtil.convertFile(lowerExt);
System.out.println(format.format(new Date())+":转换"+toext+"完成");
}else if("replaceBookMark".equals(taskType)){
System.out.println("开始将文件["+fileName+"]书签进行替换====");
JSONObject jsonObject = JSONObject.fromObject(valueObject);
this.wordUtil.replaceBookMark(jsonObject);
fileBytes = this.wordUtil.convertFile(lowerExt);
System.out.println("===============替换书签完成============");
}else if("exportTask".equals(taskType)) {//处理业务数据 导出任务数据
System.out.println("开始导出业务数据===="+valueObject);
ServiceUtil serviceUtil = new ServiceUtil(this.wordUtil);
JSONObject jsonObject = JSONObject.fromObject(valueObject);
fileBytes = serviceUtil.exportData2File(jsonObject.getJSONArray("datalist"), jsonObject.getInt("size"));
System.out.println("===============导出业务数据完成============");
}
/********************************处理文件结束********************************/
if(fileBytes==null){
dos.writeUTF("error");
dos.flush();
dos.writeUTF("处理文件过程中错误");
dos.flush();
process=false;
}
/********************************返回处理过的文件********************************/
if(process){
dos.writeUTF("info");//文件处理完成,将信息返回到客户端
dos.flush();
int fileBytelength = fileBytes.length;//转换后的文件长度
System.out.println(format.format(new Date())+":======== 服务端开始发送文件流,文件大小("+getFormatFileSize(fileBytelength)+") ========");
dos.writeLong(fileBytelength);
dos.flush();
dos.write(fileBytes, 0, fileBytelength);//将文件一起写入到输出流发送
dos.flush();
System.out.println(format.format(new Date())+":======== 发送文件流成功 ========");
}
/********************************返回处理过的文件完成********************************/
}
} catch (Exception e) {
String error = e.toString();
System.out.println("error==================="+error);
StackTraceElement[] stackTrace = e.getStackTrace();
for(StackTraceElement s:stackTrace){
int lineNumber = s.getLineNumber();
String methodName = s.getMethodName();
String className = s.getClassName();
String filename = s.getFileName();
System.out.print("err:"+filename+" "+className+" "+methodName+" "+lineNumber);
System.out.println("");
}
try {
dos.writeUTF("error");
dos.flush();
dos.writeUTF("处理文件过程中错误=="+e.toString());
dos.flush();
}catch (Exception ex){
String exrror =ex.toString();
System.out.println("返回数据处理错误信息==================="+exrror);
}
}finally {
System.out.println("关闭资源");
try {
if(wordUtil!=null)wordUtil.close();
socket.close();
} catch (Exception e) {
String error = e.toString();
System.out.println(error);
e.printStackTrace();
}
System.out.println("****************************************************************");
}
}
/**
* 文件传输Clinet端<br>
* 功能说明:
* @Author 空中智囊
* @Date 2016年09月01日
* @version 1.0
*/
public class SocketClient extends Socket {
public static final Logger LOGGER = LoggerFactory.getLogger(SocketClient.class);
private static final String SERVER_IP = "127.0.0.1"; // word文件组件处理服务IP地址
private static final int SERVER_PORT = 8899; // word文件组件处理服务端口
private int soTimeout = 60000; // 服务链接超时时间 60s
private Socket client = this;
private FileInputStream fis;
private DataOutputStream dos;
private DataInputStream dis;
private FileOutputStream fos;
public SocketClient(String listenip, int listenport) throws Exception {
super(listenip, listenport);
this.setSoTimeout(this.soTimeout);
LOGGER.info("Cliect[port:" + this.client.getLocalPort() + "] 成功连接服务端");
}
public SocketClient() throws Exception {
super(SERVER_IP, SERVER_PORT);
this.setSoTimeout(this.soTimeout);
LOGGER.info("Cliect[port:" + this.client.getLocalPort() + "] 成功连接服务端");
}
public SocketClient(String listenip, int listenport, int soTimeout) throws Exception {
super(listenip, listenport);
this.setSoTimeout(soTimeout);
LOGGER.info("Cliect[port:" + this.client.getLocalPort() + "] 成功连接服务端");
}
/**
* 处理word文件
* @param srcRealPath 模板word文件路径绝对地址
* @param descRealPath 处理后的文件存放地址绝对路径
* @param taskType 处理文件的类型 convertFile/replaceBookMark/exportTask
* @param jsonObject 传给服务端的数据对象,这个参数可根据服务端需求进行调整
* @return 处理结果
*/
public JSONObject processOffice(String srcRealPath, String descRealPath, String taskType, JSONObject jsonObject) {
JSONObject rtnObject = new JSONObject();
String code = "200";
String message = "";
try {
File file = new File(srcRealPath);
if (!file.exists() || !file.canWrite()) {
code = "200";
message = "文件不存在,或已被占用";
rtnObject.element("code", code);
rtnObject.element("message", message);
JSONObject var41 = rtnObject;
return var41;
}
LOGGER.info(srcRealPath + "===>" + descRealPath);
if (file.exists() && file.canWrite()) {
String filename = file.getName();
this.fis = new FileInputStream(file);
this.dos = new DataOutputStream(this.client.getOutputStream());
this.dos.writeUTF(filename);//文件名字
this.dos.flush();
this.dos.writeLong(file.length());//文件长度
this.dos.flush();
String ext = descRealPath.substring(descRealPath.lastIndexOf(".") + 1, descRealPath.length());
this.dos.writeUTF(ext);//源文件后缀名字
this.dos.flush();
this.dos.writeUTF(taskType);//任务类型
this.dos.flush();
if (YOZOOfficeUtil.PROCESS_TYPE_CONVERTFILE.equals(taskType)) {
this.dos.writeUTF(jsonObject.toString());
this.dos.flush();
}
LOGGER.info("======== 开始向服务端传送源文件" + srcRealPath + " ========");
byte[] bytes = new byte[1024];
long progress = 0L;
int length;
while((length = this.fis.read(bytes, 0, bytes.length)) != -1) {
this.dos.write(bytes, 0, length);
this.dos.flush();
progress += (long)length;
LOGGER.info("| " + 100L * progress / file.length() + "% |");
}
LOGGER.info("======== 文件传输成功 (" + file.length() / 1048576L + ")M========");
this.client.shutdownOutput();
LOGGER.info("======== 开始转换" + ext + " ========");
InputStream inputStream = this.client.getInputStream();
this.dis = new DataInputStream(inputStream);
String result = this.dis.readUTF();
if ("error".equals(result)) {
String reason = this.dis.readUTF();
LOGGER.info(reason);
code = "500";
message = reason;
} else if ("info".equals(result)) {
long l = this.dis.readLong();
LOGGER.info("======== 转换" + ext + "完成,文件大小(" + l / 1048576L + ")M ========");
LOGGER.info("======== 开始接受" + ext + " ========");
File newFile = new File(descRealPath);
if (newFile.exists()) {
newFile.delete();
}
this.fos = new FileOutputStream(newFile);
progress = 0L;
bytes = new byte[1048576];
while((length = this.dis.read(bytes, 0, bytes.length)) != -1) {
this.fos.write(bytes, 0, length);
this.fos.flush();
}
LOGGER.info("======== 接受" + ext + "文件成功========");
this.dis.close();
} else {
code = "500";
message = "链接被强制关闭....";
}
} else {
code = "404";
message = "文件不存在,或已被占用:" + srcRealPath;
}
} catch (Exception e) {
code = "500";
message = "客户端报错:" + e.toString();
LOGGER.error("异常:",e);
} finally {
if (this.fis != null) {
try {
this.fis.close();
} catch (Exception var38) {
;
}
}
if (this.fos != null) {
try {
this.fos.close();
} catch (Exception var37) {
;
}
}
try {
this.client.close();
} catch (Exception var36) {
;
}
}
rtnObject.element("code", code);
rtnObject.element("message", message);
return rtnObject;
}
public static void main(String[] args) {
try {
SocketClient socketClient = new SocketClient();
// 将文档转换成pdf文件
socketClient.processOffice("D:/2.doc","D:/2.pdf",YOZOOfficeUtil.PROCESS_TYPE_CONVERTFILE,null);
// 将文档转换成pdf文件
JSONObject dataObject = new JSONObject();
dataObject.element("bookmarkname","这个是测试呢日哦那个");
socketClient.processOffice("D:/2.doc","D:/2.pdf",YOZOOfficeUtil.PROCESS_TYPE_REPLACEBOOKMARK,dataObject);
} catch (Exception e) {
LOGGER.error("异常:",e);
}
}
}
5、启动word文件处理组件服务端
组件启动脚本
nohup ./ofdServer.sh &
6、调用服务端对word文件处理
public static void main(String[] args) {
try {
SocketClient socketClient = new SocketClient();
// 将文档转换成pdf文件
socketClient.processOffice("D:/2.doc","D:/2.pdf",YOZOOfficeUtil.PROCESS_TYPE_CONVERTFILE,null);
// 替换模板中的书签值,word中插入书签自行百度
JSONObject dataObject = new JSONObject();
dataObject.element("bookmarkname","这个是测试呢日哦那个");
socketClient.processOffice("D:/2.doc","D:/3.doc",YOZOOfficeUtil.PROCESS_TYPE_REPLACEBOOKMARK,dataObject);
} catch (Exception e) {
LOGGER.error("异常:",e);
}
}
7、资源下载
word文件处理组件服务端(开箱即用):
链接: https://pan.baidu.com/s/1_ZgjoX_nuv3a7_SKkJ_D7w 提取码: hn2r
服务端资源内容
将文件复制到linux服务器,并解压,执行 ./ofdServer.sh ,输出:服务端启动,监听端口为:8899,即运行成功
word文件处理组件客户端(开箱即用processOffice):
链接: https://pan.baidu.com/s/1mtabGY87RuAGGkwKrBIvfQ 提取码: mqxf
客户端资源文件内容
将源文件复制到项目指定包名,运行SocketClient.java中的main方法,可查看运行结果。
最重要的一点:服务器要安装永中OFFICE客户端
数据报表是许多项目都有的模块,一般都是导出Excel或者PDF,这里记录下我在项目里用POI导出Excel。项目中,我需要根据页面jqgrid的机架查询条件导出对应的机架数据,jqgrid是分页的,但导出是要导出所有。
Apache POI - the Java API for Microsoft Documents,官网:http://poi.apache.org/
maven引入POI
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
或者
<dependency><!--Excel工具类(Easy POI)-->
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>3.2.0</version>
</dependency>
<dependency><!--Excel工具类(Easy POI)-->
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>3.2.0</version>
</dependency>
<dependency><!--Excel工具类(Easy POI)-->
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>3.2.0</version>
</dependency>
html、js调用
<dependency><!--Excel工具类(Easy POI)-->
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>3.2.0</version>
</dependency>
<dependency><!--Excel工具类(Easy POI)-->
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>3.2.0</version>
</dependency>
<dependency><!--Excel工具类(Easy POI)-->
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>3.2.0</version>
</dependency>
//导出excel
function exportRackExcel() {
//获取当前jqGrid分页参数
var postData = $("#rack").jqGrid("getGridParam", "postData");
postData.page = 1;
postData.rows = 999999999;//设置每页9亿条记录(相当于无穷大,查询所有)
//ajax不支持Excel类型,使用location.href或者表单提交
//window.location.href,get提交,数据会暴露在URL,相对不安全
//创建临时的、隐藏的form表单,post提交,数据在请求体里,相对安全
var $form = $(document.createElement('form')).css({display: 'none'}).attr("method", "POST").attr("action", ctx + "/excel");
for (var key in postData) {
var $input = $(document.createElement('input')).attr('name', key).val(postData[key]);
$form.append($input);
}
$("body").append($form);
$form.submit();
//过河拆桥,提交完成后remove掉
$form.remove();
}
纯js写法
//其他操作,同上
let $form = document.createElement('form');
$form.style.display="none";
$form.method="POST";
$form.action=ctx + "/excel";
for (let key in postData) {
if(postData[key]){
let $input = document.createElement('input');
$input.name=key;
$input.value=postData[key];
$form.appendChild($input);
}
}
document.body.appendChild($form);
$form.submit();
//过河拆桥,提交完成后remove掉
$form.remove();
controller
/**
* 根据当前jqGrid分页情况,创建并导出Excel文件
*
* @param entity 机架实体,用来接收查询条件
* @return ResponseEntity
*/
@PostMapping("/excel")
public ResponseEntity createExcel(RackVo entity) {
//Excel对应的columnNames列名集合 { key,label }
String[][] excelMap = {
{"no", "Rack Code"},
{"rackName", "Rack Name"},
{"roomName", "Room"},
{"idc", "IDC Center"},
{"clientName", "Customer"},
{"rackTypeName", "Type"},
{"existentialMode", "Existential Mode"},
{"maxPower", "Maximum Power(KVA)"},
{"status", "Status"},
{"administrate", "Administrate"},
};
return DownloadUtil.download(ExportExcelUtil.createExcel("Rack Management", excelMap, rackService.createExcel(entity).getData()).getData(), "机架数据报表");
}
两个工具类:导出Excel工具类 ExportExcelUtil,下载工具类 DownloadUtil
/**
* java POI 导出Excel表工具类
*/
public class ExportExcelUtil {
//禁止实例化
private ExportExcelUtil() {
}
/**
* 只支持一级表头
*
* @param titleName 表标题
* @param columnNames 列名集合,key是用来设置填充数据时对应单元格的值,label就是对应的列名,生成Excel表时,
* 第一维数组下标0对应值为Excel表最左边的列的列名 例:{ { key,label },{ key,label } }
* @param dataLists 数据集合,key对应的是列名集合的key,value是要填充到单元格的值 例:ArrayList<HashMap<String key, String vaule>>
* @return ResultModel<Workbook>
*/
public static ResultModel<Workbook> createExcel(String titleName, String[][] columnNames, ArrayList<HashMap<String, String>> dataLists) {
//创建HSSFWorkbook对象(excel的文档对象)
HSSFWorkbook wb = new HSSFWorkbook();
//建立新的sheet对象(excel的表单)
HSSFSheet sheet = wb.createSheet(titleName);//设置表单名
//1、标题名
//创建标题行,参数为行索引(excel的行),可以是0~65535之间的任何一个
HSSFRow row1 = sheet.createRow(0);
//标题的字体
HSSFFont font1 = wb.createFont();
font1.setFontHeightInPoints((short) 12);
font1.setFontName("黑体");
//标题的样式
HSSFCellStyle style1 = wb.createCellStyle();
style1.setAlignment(HSSFCellStyle.ALIGN_CENTER);//水平居中
style1.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);//垂直居中
// 把字体 应用到当前样式
style1.setFont(font1);
//自动换行
style1.setWrapText(true);
//自定义填充颜色(天空蓝)
style1.setFillPattern(FillPatternType.SOLID_FOREGROUND);
style1.setFillForegroundColor(IndexedColors.SKY_BLUE.getIndex());
// 设置边框
style1.setBorderBottom(HSSFCellStyle.BORDER_THIN);
style1.setBorderLeft(HSSFCellStyle.BORDER_THIN);
style1.setBorderRight(HSSFCellStyle.BORDER_THIN);
style1.setBorderTop(HSSFCellStyle.BORDER_THIN);
createCell(row1, 0, style1, titleName);
//合并单元格CellRangeAddress构造参数依次表示起始行,截至行,起始列, 截至列
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, columnNames.length - 1));
//2、列名
//创建列名行
//列名的字体
HSSFFont font2 = wb.createFont();
font2.setFontHeightInPoints((short) 12);
font2.setFontName("新宋体");
//列名的样式
HSSFCellStyle style2 = wb.createCellStyle();
style2.setAlignment(HSSFCellStyle.ALIGN_CENTER);//水平居中
style2.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);//垂直居中
// 把字体 应用到当前样式
style2.setFont(font2);
//自动换行
style2.setWrapText(true);
//自定义填充颜色(浅蓝色)
style2.setFillPattern(FillPatternType.SOLID_FOREGROUND);
style2.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex());
// 设置边框
style2.setBorderBottom(HSSFCellStyle.BORDER_THIN);
style2.setBorderLeft(HSSFCellStyle.BORDER_THIN);
style2.setBorderRight(HSSFCellStyle.BORDER_THIN);
style2.setBorderTop(HSSFCellStyle.BORDER_THIN);
HSSFRow row2 = sheet.createRow(1);
for (int i = 0; i < columnNames.length; i++) {
//单元格宽度
sheet.setColumnWidth(i, 20 * 256);
createCell(row2, i, style2, columnNames[i][1]);//例:[[key,label],[key,label]] 取label
}
//3、填充数据
//内容的字体
HSSFFont font3 = wb.createFont();
font3.setFontHeightInPoints((short) 12);
font3.setFontName("新宋体");
//内容的样式
HSSFCellStyle style3 = wb.createCellStyle();
style3.setAlignment(HSSFCellStyle.ALIGN_CENTER);//水平居中
style3.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);//垂直居中
// 把字体 应用到当前样式
style3.setFont(font3);
//自动换行
style3.setWrapText(true);
//默认无填充
style3.setFillPattern(FillPatternType.NO_FILL);
style3.setFillForegroundColor(IndexedColors.RED.getIndex());
// 设置边框
style3.setBorderBottom(HSSFCellStyle.BORDER_THIN);
style3.setBorderLeft(HSSFCellStyle.BORDER_THIN);
style3.setBorderRight(HSSFCellStyle.BORDER_THIN);
style3.setBorderTop(HSSFCellStyle.BORDER_THIN);
int index = 2;//标题行、列名行,所以数据行默认从第三行开始
for (HashMap<String, String> map : dataLists) {
//创建内容行
HSSFRow row3 = sheet.createRow(index);
for (int i = 0; i < columnNames.length; i++) {
String val = map.get(columnNames[i][0]);
createCell(row3, i, style3, val == null ? "" : val);//例:[[key,label],[key,label]] 取key
}
index++;
}
return ResultModel.of(wb);
}
/**
* 创建一个单元格
*
* @param row 行
* @param column 列
* @param cellStyle 单元格样式
* @param text 值
*/
private static void createCell(Row row, int column, CellStyle cellStyle, String text) {
Cell cell = row.createCell(column); // 创建单元格
cell.setCellValue(text); // 设置值
cell.setCellStyle(cellStyle); // 设置单元格样式
}
}
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 文件下载工具类
*/
public class DownloadUtil{
/**
* 快速下载
*/
public static ResponseEntity download(byte[] fileBytes, String fileName) {
//设置文件
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", new String(fileName.getBytes(StandardCharsets.UTF_8),StandardCharsets.ISO_8859_1));
//下载文件
return new ResponseEntity<>(fileBytes, headers, HttpStatus.CREATED);
}
/**
* 快速下载
*/
public static ResponseEntity download(File file) {
return download(getByteArray(file), file.getName());
}
/**
* 快速下载
*/
public static ResponseEntity download(Workbook workbook, String fileName) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
fileName = fileName + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".xls";
workbook.write(outputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
return download(outputStream.toByteArray(), fileName);
}
//获取文件的字节数组
private static byte[] getByteArray(File file) {
if (!file.exists()) {
throw new RuntimeException("File Not Found:" + file.getPath());
}
ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length());
BufferedInputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(file));
int buf_size = 1024;
byte[] buffer = new byte[buf_size];
int len;
while (-1 != (len = in.read(buffer, 0, buf_size))) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
try {
assert in != null;
in.close();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//获取文件名后缀
private static String getSuffix(String fileName) {
int lastPointIndex = fileName.lastIndexOf(".");
if (StringUtils.isEmpty(fileName) || lastPointIndex == -1) {
return null;
}
return fileName.substring(lastPointIndex + 1);
}
}
获取封装数据的service层 createExcel,直接到取page分页方法,遍历机架数据集合,设置Map<key,value>,add到list<Map>中,最后将封装好的数据return回controller,传入工具类,最后下载。
/**
* 根据当前jqGrid分页情况,创建并导出Excel文件
*
* @param entity 查询条件
* @return 封装好的数据集合
*/
@Override
public ResultModel<ArrayList<HashMap<String, String>>> createExcel(RackVo entity) {
ArrayList<HashMap<String, String>> dataLists = new ArrayList<HashMap<String, String>>();
//直接调page分页方法,获取当前jqGrid分页条件对应的数据集合,
ResultModel<PageInfo<RackVo>> rm = page(entity);
if (rm.isFlag()) {
List<RackVo> rackVoList = rm.getData().getRows();
for (RackVo rackVo : rackVoList) {
HashMap<String, String> map = new HashMap<String, String>(16);
map.put("no", rackVo.getNo() != null ? rackVo.getNo() : "");
map.put("rackName", rackVo.getName() != null ? rackVo.getName() : "");
map.put("roomName", rackVo.getRoom() != null ? rackVo.getRoom().getRoomname() : "");
map.put("idc", rackVo.getOrg() != null ? rackVo.getOrg().getOrgName() : "");
map.put("clientName", rackVo.getCustomer() != null ? rackVo.getCustomer().getClientname() : "");
map.put("rackTypeName", rackVo.getRacktype() != null ? rackVo.getRacktype().getName() : "");
map.put("existentialMode", "1".equals(rackVo.getExistentialMode()) ? "Physical" : "Virtual");
map.put("maxPower", rackVo.getMaxpower() != null ? rackVo.getMaxpower() : "");
String status = rackVo.getServiceStatus();
switch (status != null ? status : "") {
case "1":
status = "Idle";
break;
case "2":
status = "Reserved";
break;
case "3":
status = "Occupied";
break;
default:
status = "";
break;
}
map.put("status", status);
String administrate = rackVo.getAdministrate();
switch (administrate != null ? administrate : "") {
case "R":
administrate = "Cust Own";
break;
case "U":
administrate = "CTG Own";
break;
default:
administrate = "";
break;
}
map.put("administrate", administrate);
dataLists.add(map);
}
}
return ResultModel.of(dataLists);
}
从开发阶段到测试阶段,导了无数次,没毛病
excelMap,Excel对应的columnNames列名集合 { key,label },可以不用再controller设置了,直接从页面jqgrid抓取,传入controller就行(滑稽脸~)
//获取jqgrid头部标题tr,有多少个tr就有多少级标题
var thead_tr = $(".ui-jqgrid-htable").find("tr.ui-jqgrid-labels");
//遍历thead_tr找出每一个标题,并保存到对象中
var titles = [];
thead_tr.each(function(index_tr,element_tr){
titles.push([]);
$(element_tr).find("th").each(function(index_th,element_th){
//内容
var label = $(element_th).text();
//所占行 rowspan 默认1
var rowspan = $(element_th).attr("rowspan") || 1;
//所占列 colspan 默认1
var colspan = $(element_th).attr("colspan") || 1;
//键
var key = $(element_th).attr("id");
key = key.substring(key.lastIndexOf("_")+1,key.length);
if(label){
titles[index_tr].push({
label:label,
key:key,
rowspan:rowspan,
colspan:colspan,
});
}
});
});
//JSON.stringify(titles)
console.log(titles);
2020-10-20更新
直接构造form表单提交,我们不能设置请求头信息,有些需求不能满足(例如在前后端分离的项目中,需要在请求头传递token令牌),当我们导出Excel功能需要设置请求头信息时应该如何操作呢?封装原生Ajax,利用responseType: 'blob'属性,接收二进制数据,构建Blob对象,将二进制数据转成文件,利用a标签下载文件
//封装原生Ajax
var Ajax={
get: function(options) {
let xhr = new XMLHttpRequest();
xhr.open('GET', options.url, true);
//设置请求头
xhr.setRequestHeader("Authorization", 'Bearer ' + store.getters.token);
xhr.onload = function() {
let response = null;
// responseType="" / "text"时,响应的结果从xhr.responseText获取
if(xhr.responseType === "" || xhr.responseType === "text"){
response = xhr.responseText;
}
//200 请求成功
if (xhr.status === 200) {
options.success.call(response);
}
//其他情况,请求失败
if(options.error){
options.error.call(xhr.error);
}
};
xhr.send();
},
post: function (options) {
let xhr = new XMLHttpRequest();
xhr.open("POST", options.url, true);
//设置请求头
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Authorization", 'Bearer ' + store.getters.token);
//设置响应内容类型、超时时间
options.responseType ? xhr.responseType = options.responseType : xhr.responseType = "text";
options.timeout ? xhr.timeout = options.timeout : xhr.timeout = 30000;
xhr.onload = function() {
let response = null;
// responseType="" / "text"时,响应的结果从xhr.responseText获取
if(xhr.responseType === "" || xhr.responseType === "text"){
response = xhr.responseText;
}
//200 请求成功
if (xhr.status === 200) {
options.success.call(response);
}
// responseType = "blob"时,响应的是Blob二进制数据,直接调用下载
if(xhr.status === 201){
download(xhr,options.success)
}
//其他情况,请求失败
if(options.error){
options.error.call(xhr.error);
}
};
xhr.send(JSON.stringify(options.data));
}
};
//Blob响应,转成文件下载
function download(response,callback) {
//创建一个隐藏的下载a标签
let url = window.URL.createObjectURL(new Blob([response.response]));
let link = document.createElement("a");
link.style.display = "none";
link.href = url;
//设置文件名,文件名从响应头中获取(PS:可能会存在中文乱码、文件后缀多个下划线等问题)
let fileName = response.getAllResponseHeaders().split("\n")[4].split(":")[1].split(";")[2].split("=")[1].replace(/"/g,"");
fileName = decodeURIComponent(escape(fileName));
console.log("文件名:" + fileName);
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
//过河拆桥
link.remove();
if(callback){
callback();
}
}
使用
//获取当前分页参数
let postData = vue.getPageParameter();
postData.page = 1;
postData.pageSize = 999999999;//设置每页9亿条记录(相当于无穷大,查询所有)
console.log("开始导出...");
Ajax.post({
url:vue.excelUrl,
data:postData,
timeout: 30000,
responseType: 'blob',
success:function () {
console.log("导出完成,请您注意浏览器的下载管理器!");
}
});
效果
后缀多了个下划线,很奇怪...,删除下划线文件能正常打开,数据、单元格背景等正常
作者:huanzi-qch
出处:https://www.cnblogs.com/huanzi-qch
若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.
产品期望实现【公文管理】其中发文拟文一块内容,用户上传正文(word),再选择不同套红模板,最后拼接为一个对外发文,的公文格式。
基于上次使用vue实现在线编辑功能,产品不太满意,重新学习java如何操作word文档,之前地址:(vue使用Tinymce富文本模拟在线word文档)juejin.cn/post/723665…
有两个文件test1.docx (作为红头模板)和test2.docx(作为正文);期望实现效果:用户选择红头模板加上自己写的正文内容,最终生成可编辑的word文档。
create_table.docx 合并之后最终效果“
test1.docx 红头模板:
test2.docx 正文(用户撰写上传的内容):
如标题所示,最终采用Apache POI来实现这个功能主要原因:
主要是对于我这种初学者来说是非常友好的,太复杂玩不转。Apache POI 的使用非常简单,只需添加它的 jar 包到项目中,并调用相应的 API 即可实现文档读写。同时,Apache POI 提供了丰富的文档和官方网站上的文档也很详细。
Apache POI,分别为 Word、Excel、PowerPoint 等各种格式提供不同的类方法,我们需要操作Word文档的功能,所以使用(Java API for Microsoft Documents)中的XWPFDocument类,实现文档合并功能。
整理不同格式文档操作类
注意:word文档目前有两种不同格式,一种是以doc结尾的,另一种以docx结尾,本次功能主要讲解docx格式文档操作,doc格式文档调用的类和函数HWPF开头。
两者区别:doc是Word2007及以下版的文件扩展名,而docx是Word2007及以上版本的文件扩展名,docx版本兼容性较高,而且比doc文件所占用空间更小。
在pom.xml文件中引入maven依赖,
<!-- WordToHtml .doc .odcx poi -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>4.1.2</version>
</dependency>
<!-- 操作excel的库 注意版本保持一致 poi poi-ooxml poi-scratchpad -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<!--poi-ooxml和*poi-ooxml-schemas*是poi对2007及以上版本的扩充。-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
简化整体流程,创建两个word文件test1.docx和test2.docx。将下列file对应的文件路径换成自己创建的文件路径创,建单个java文件(带main),直接运行main方法输出creat_table.docx文件。 先上代码,再进行讲解:
package org.ssssssss.magicboot;
import org.apache.poi.xwpf.usermodel.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.List;
public class WordDocumentTest {
public static void main(String[] args) throws Exception
{
//获取文件转io流
File file1 = new File("D:/IDM/test1.docx");
File file2 = new File("D:/IDM/test2.docx");
FileInputStream fis1 = new FileInputStream(file1);
FileInputStream fis2 = new FileInputStream(file2);
//最终文件输出的路径
FileOutputStream out = new FileOutputStream(new File("D:/IDM/create_table.docx"));
//转为word文档元素
XWPFDocument dcx1 = new XWPFDocument(fis1);
XWPFDocument dcx2 = new XWPFDocument(fis2);
//创建一个新的文档
XWPFDocument document= new XWPFDocument();
//将第一个文档元素复制新创建的文档中;
document = dcx1;
//换行 document.createParagraph().createRun().addBreak();
//将第二个docx内容添加到新创建的文档中
mergeParagraphs(dcx2.getParagraphs(), document);
//结束关闭io流
document.write(out);
out.close();
fis1.close();
fis2.close();
System.out.println("create_table document written success.");
}
// 合并文本段落
private static void mergeParagraphs(List<XWPFParagraph> paragraphs, XWPFDocument outDoc) {
for (XWPFParagraph para : paragraphs) {
XWPFParagraph newPara = outDoc.createParagraph();
newPara.getCTP().setPPr(para.getCTP().getPPr());
//判断是否是文本、段落、图片 //.getRPr() para.tables !=null ,iruns 获取图片
for (XWPFRun run : para.getRuns()) {
XWPFRun newRun = newPara.createRun();
newRun.getCTR().setRPr(run.getCTR().getRPr());
newRun.setText(run.getText(0));
}
}
}
实现流程主要看代码中的注释,包含以下几个步骤
核心重点将第二个docx内容添加到新创建的文档中封装mergeParagraphs方法,在这个方法中传入了两个参数(List paragraphs, XWPFDocument outDoc) 其中List<XWPFParagraph> paragraphs = dcx2.getParagraphs() 意思是将dcx2文档所有段落取出来用一个数组存放,再进行循环段落;通过XWPFParagraph newPara = outDoc.createParagraph();给新的文档创建一个新的段落;给新的段落添加对应的样式newPara.getCTP().setPPr(para.getCTP().getPPr());最后由于段落中会划分不同的XWPFRun再进行循环设置文本的字体、大小、颜色、加粗、斜体、下划线等格式。 官方api介绍
刚才对XWPFRun没有进行很好的解释,这里重新举例说明下,例如以下标红的段落
按照正常理解这一个段落内容应该是一个东西,其实在XWPF中会划分为不同的XWPFRun,使用idea打断点查看数据
可以看出它将一段文字划分为不同模块,为什么会这样,在一段文字中也存在不同的区别,例如文字的字体,像下图中“根据”“2023”属于不同的字体,所以会划分为不同的XWPFRun,理解这个概念后同理明白为什么一个段落会划分为29模块
在前面其实已经实现第一个模块最终效果的docx文档合并的功能,所以在这个模块讲解在实现这个过程中记录有意思的内容。
接着上面讲XWPFRun这个函数,XWPFRun用于在 Word 文档中添加或修改单个本 Run 或 Run 中的文字格式。它是文本段落(XWPFParagraph)中的最小单元,用于精细控制文本的格式和样式。可以使用 XWPFRun 类的各种方法来设置文本的字体、大小、颜色、加粗、斜体、下划线等格式。 下列是在使用过程中记录的一些属性,以及这些属性对应能够设置的格式注释。
XWPFRun run = firstParagraph.createRun();
XWPFRun tempRun = xwpfRuns.get(i);
//默认:宋体(wps)/等线(office2016) 5号 两端对齐 单倍间距
run.setText(tempRun.text());
//加粗
run.setBold(tempRun.isBold());
//我也不知道这个属性做啥的
run.setCapitalized(tempRun.isCapitalized());
//设置颜色--十六进制
run.setColor(tempRun.getColor());
//这个属性报错
run.setCharacterSpacing(tempRun.getCharacterSpacing());
//浮雕字体----效果和印记(悬浮阴影)类似
run.setEmbossed(tempRun.isEmbossed());
//双删除线
run.setDoubleStrikethrough(tempRun.isDoubleStrikeThrough());
run.setEmphasisMark(tempRun.getEmphasisMark().toString());
//字体,//字体,范围----效果不详
run.setFontFamily(tempRun.getFontFamily());
//字体大小,没有设置默认是-1,
if(tempRun.getFontSize() != -1){
run.setFontSize(tempRun.getFontSize());
}
//印迹(悬浮阴影)---效果和浮雕类似
run.setImprinted(tempRun.isImprinted());
//斜体(字体倾斜)
run.setItalic(tempRun.isItalic());
//字距调整----这个好像没有效果
run.setKerning(tempRun.getKerning());
//阴影---稍微有点效果(阴影不明显)
run.setShadow(tempRun.isShadowed());
//小型股------效果不清楚
run.setSmallCaps(tempRun.isSmallCaps());
//单删除线(废弃)
run.setStrike(tempRun.isStrike());
//单删除线(新的替换Strike)
run.setStrikeThrough(tempRun.isStrikeThrough());
//下标(吧当前这个run变成下标)---枚举
run.setSubscript(tempRun.getSubscript());
//设置两行之间的行间距
run.setTextPosition(tempRun.getTextPosition());
//各种类型的下划线(枚举)
run.setUnderline(tempRun.getUnderline());
run.setVerticalAlignment(tempRun.getVerticalAlignment().toString());
run.setVanish(tempRun.isVanish());
run.setUnderlineThemeColor(tempRun.getUnderlineColor());
run.setUnderlineColor(tempRun.getUnderlineColor());
run.setTextScale(tempRun.getTextScale());
run.setTextPosition(tempRun.getTextPosition());
run.setTextHighlightColor(tempRun.getTextHightlightColor().toString());
// run.setStyle(tempRun.gets); 没找到这个属性
run.setLang(tempRun.getLang());
XWPFParagraph 是 Apache POI 库中 XWPF 模块的一部分,用于创建或修改 Word 文档中的段落。它可以添加不同的文本格式,并且可以添加图片、表格、超链接等内容。XWPFParagraph 类可以控制段落的样式和格式,包括字体、字号、行距、首行缩进、对齐方式等。可以使用 XWPFParagraph 类的各种方法来设置段落的格式和样式。
常用方法:
//创建一个新的 XWPFRun 对象,用于在段落中添加文本或修改文本格式。
createRun()
//设置段落的对齐方式,align 参数可以是 LEFT、CENTER、RIGHT、JUSTIFY 等值。
setAlignment(ParagraphAlignment align)
//设置段落的行距和行距规则,lineSpacing 参数是行距大小(以磅为单位),lineSpacingRule 参数可以是 EXACT、AT_LEAST、AUTO 等值。
setSpacingBetween(int lineSpacing, LineSpacingRule lineSpacingRule)
//设置段落的左缩进大小(以磅为单位)。
setIndentationLeft(int indentation)
//设置段落的右缩进大小(以磅为单位)。
setIndentationRight(int indentation)
//设置段落的编号 ID。
setNumID(BigInteger numId)
//设置段落的编号格式,numFmt 参数可以是 DECIMAL、LOWERCASE_LETTER、UPPERCASE_LETTER 等值。
setNumFmt(NumberFormat numFmt)
//在段落中添加图片,pictureType 参数是图片类型,filename 参数是图片文件名,width 和 height 参数是图片宽度和高度。
createPicture(XWPFRun run, int pictureType, String filename, int width, int height)
其他方法:
//指定应显示在左边页面指定段周围的边界。
setBorderBottom(Borders.APPLES);
//指定应显示在下边页面指定段周围的边界。
setBorderLeft(Borders.APPLES);
//指定应显示在右侧的页面指定段周围的边界。
setBorderRight(Borders.ARCHED_SCALLOPS);
//指定应显示上方一组有相同的一组段边界设置的段落的边界。这几个是对段落之间的格式的统一,相当于格式刷
setBorderTop(Borders.ARCHED_SCALLOPS);
//---正文宽度会稍微变窄
p1.setFirstLineIndent(99);
//---段落的对齐方式 1左 2中 3右 4往上 左 不可写0和负数
p1.setFontAlignment(1);
//---首行缩进,指定额外的缩进,应适用于父段的第一行。
p1.setIndentationFirstLine(400);
//---首行前进,指定的缩进量,应通过第一行回到开始的文本流的方向上移动缩进从父段的第一行中删除。
p1.setIndentationHanging(400);
//---整段右移
p1.setIndentFromLeft(400);
//--此方法提供了样式的段落,这非常有用。
p1.setStyle("");
//--此元素指定是否消费者应中断超过一行的文本范围,通过打破这个词 (打破人物等级) 的两行或通过移动到下一行 (在词汇层面上打破) 这个词的拉丁文字。
p1.setWordWrapped(true);
//---指定的文本的垂直对齐方式将应用于此段落中的文本
p1.setVerticalAlignment(TextAlignment.CENTER);
//--指定行之间的间距如何计算存储在行属性中。
p1.setSpacingLineRule(LineSpacingRule.AT_LEAST);
//--指定应添加在此线单位在文档中的段落的第一行之前的间距。
p1.setSpacingBeforeLines(6);
//--指定应添加上面这一段文档中绝对单位中的第一行的间距。
p1.setSpacingBefore(6);
//--指定应添加在此线单位在文档中的段落的最后一行之后的间距。
p1.setSpacingAfterLines(6);
//--指定应添加在文档中绝对单位这一段的最后一行之后的间距。
p1.setSpacingAfter(6);
//--指定当渲染此分页视图中的文档,这一段的内容都呈现在文档中的新页的开始。
p1.setPageBreak(true);
刚在展示活动内容都是根据自身的需求写小demo,实际项目远远不止这些内容,其中还是存在不足之处,例如word中的表格、图片都是需要单独处理,表格有个专门类XWPFTable;图片也有XWPFPictureData、 XWPFPicture,
Apache POI 官方网站提供了完整的 API 文档:poi.apache.org/apidocs/dev…
Apache POI 的 GitHub 仓库中查看示例代码和文档:github.com/apache/poi
Java POI 生成Word文档:blog.csdn.net/qq_34755766…
Apache POI 中文版download.csdn.net/download/qq…
作者:沐游虞
转自:https://juejin.cn/post/7237487091554730021
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
*请认真填写需求信息,我们会在24小时内与您取得联系。