自定义生成Excel报表文件还是有很多不尽如意的地方,特别是针对复杂报表头,单元格样式,字体等操作。手写这些代码不仅费时费力,有时候效果还不太理想。那怎么样才能更方便的对报表样式,报表头进行处理呢?答案是使用已经准备好的Excel模板,只需要关注模板中的数据即可。
/**
* 采用模板打印的形式完成报表生成
* 模板
* 参数:
* 年月-月(2018-02%)
*
*/
@RequestMapping(value = "/export/{month}", method = RequestMethod.GET)
public void export(@PathVariable String month) throws Exception {
//1.获取报表数据
List<EmployeeReportResult> list = userCompanyPersonalService.findByReport(companyId,month);
//2.加载模板
Resource resource = new ClassPathResource("excel-template/hr-demo.xlsx");
FileInputStream fis = new FileInputStream(resource.getFile());
//3.根据模板创建工作簿
Workbook wb = new XSSFWorkbook(fis);
//4.读取工作表
Sheet sheet = wb.getSheetAt(0);
//5.抽取公共样式
Row row = sheet.getRow(2);
CellStyle styles[] = new CellStyle[row.getLastCellNum()];
for(int i=0;i<row.getLastCellNum();i++) {
Cell cell = row.getCell(i);
styles[i] = cell.getCellStyle();
}
//6.构造单元格
int rowIndex = 2;
Cell cell = null;
for (EmployeeReportResult employeeReportResult : list) {
row = sheet.createRow(rowIndex++);
// 编号,
cell = row.createCell(0);
cell.setCellValue(employeeReportResult.getUserId());
cell.setCellStyle(styles[0]);
// 姓名,
cell = row.createCell(1);
cell.setCellValue(employeeReportResult.getUsername());
cell.setCellStyle(styles[1]);
// 手机,
cell = row.createCell(2);
cell.setCellValue(employeeReportResult.getMobile());
cell.setCellStyle(styles[2]);
// 最高学历,
cell = row.createCell(3);
cell.setCellValue(employeeReportResult.getTheHighestDegreeOfEducation());
cell.setCellStyle(styles[3]);
// 国家地区,
cell = row.createCell(4);
cell.setCellValue(employeeReportResult.getNationalArea());
cell.setCellStyle(styles[4]);
// 护照号,
cell = row.createCell(5);
cell.setCellValue(employeeReportResult.getPassportNo());
cell.setCellStyle(styles[5]);
// 籍贯,
cell = row.createCell(6);
cell.setCellValue(employeeReportResult.getNativePlace());
cell.setCellStyle(styles[6]);
// 生日,
cell = row.createCell(7);
cell.setCellValue(employeeReportResult.getBirthday());
cell.setCellStyle(styles[7]);
// 属相,
cell = row.createCell(8);
cell.setCellValue(employeeReportResult.getZodiac());
cell.setCellStyle(styles[8]);
// 入职时间,
cell = row.createCell(9);
cell.setCellValue(employeeReportResult.getTimeOfEntry());
cell.setCellStyle(styles[9]);
// 离职类型,
cell = row.createCell(10);
cell.setCellValue(employeeReportResult.getTypeOfTurnover());
cell.setCellStyle(styles[10]);
// 离职原因,
cell = row.createCell(11);
cell.setCellValue(employeeReportResult.getReasonsForLeaving());
cell.setCellStyle(styles[11]);
// 离职时间
cell = row.createCell(12);
cell.setCellValue(employeeReportResult.getResignationTime());
cell.setCellStyle(styles[12]);
}
//7.下载
ByteArrayOutputStream os = new ByteArrayOutputStream();
wb.write(os);
new DownloadUtils().download(os,response,month+"人事报表.xlsx");
}
1、在ihrm_common_model中自定义注解
package com.ihrm.domain.poi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelAttribute {
/** 对应的列名称 */
String name() default "";
/** excel列的索引 */
int sort();
/** 字段类型对应的格式 */
String format() default "";
}
2、在ihrm_common中编写导入工具类
package com.ihrm.common.poi;
import com.ihrm.domain.poi.ExcelAttribute;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class ExcelImportUtil<T> {
private Class clazz;
private Field fields[];
public ExcelImportUtil(Class clazz) {
this.clazz = clazz;
fields = clazz.getDeclaredFields();
}
/**
* 基于注解读取excel
* is:文件上传流信息
* rowIndex:读取数据的起始行
* cellIndex:读取数据的起始单元格位置
*/
public List<T> readExcel(InputStream is, int rowIndex,int cellIndex) {
List<T> list = new ArrayList<T>();
T entity = null;
try {
XSSFWorkbook workbook = new XSSFWorkbook(is);
Sheet sheet = workbook.getSheetAt(0);
// 不准确
int rowLength = sheet.getLastRowNum();
System.out.println(sheet.getLastRowNum());
for (int rowNum = rowIndex; rowNum <= sheet.getLastRowNum(); rowNum++) {
Row row = sheet.getRow(rowNum);
entity = (T) clazz.newInstance();
System.out.println(row.getLastCellNum());
for (int j = cellIndex; j < row.getLastCellNum(); j++) {
Cell cell = row.getCell(j);
for (Field field : fields) {
if(field.isAnnotationPresent(ExcelAttribute.class)){
field.setAccessible(true);
ExcelAttribute ea = field.getAnnotation(ExcelAttribute.class);
if(j == ea.sort()) {
field.set(entity, covertAttrType(field, cell));
}
}
}
}
list.add(entity);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
/**
* 类型转换 将cell 单元格格式转为 字段类型
*/
private Object covertAttrType(Field field, Cell cell) throws Exception {
String fieldType = field.getType().getSimpleName();
if ("String".equals(fieldType)) {
return getValue(cell);
}else if ("Date".equals(fieldType)) {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(getValue(cell)) ;
}else if ("int".equals(fieldType) || "Integer".equals(fieldType)) {
return Integer.parseInt(getValue(cell));
}else if ("double".equals(fieldType) || "Double".equals(fieldType)) {
return Double.parseDouble(getValue(cell));
}else {
return null;
}
}
/**
* 格式转为String
* @param cell
*/
public String getValue(Cell cell) {
if (cell == null) {
return "";
}
switch (cell.getCellType()) {
case STRING:
return cell.getRichStringCellValue().getString().trim();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
Date dt = DateUtil.getJavaDate(cell.getNumericCellValue());
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(dt);
} else {
// 防止数值变成科学计数法
String strCell = "";
Double num = cell.getNumericCellValue();
BigDecimal bd = new BigDecimal(num.toString());
if (bd != null) {
strCell = bd.toPlainString();
}
// 去除 浮点型 自动加的 .0
if (strCell.endsWith(".0")) {
strCell = strCell.substring(0, strCell.indexOf("."));
}
return strCell;
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
default:
return "";
}
}
}
3、在ihrm_common中编写导出工具类
package com.ihrm.common.poi;
import com.ihrm.domain.poi.ExcelAttribute;
import lombok.Getter;
import lombok.Setter;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 导出Excel工具类
* 基于模板打印的方式导出:
*/
@Getter
@Setter
public class ExcelExportUtil<T> {
private int rowIndex; //写入数据的起始行
private int styleIndex; //需要提取的样式所在的行号
private Class clazz; //对象的字节码
private Field fields[]; //对象中的所有属性
public ExcelExportUtil(Class clazz,int rowIndex,int styleIndex) {
this.clazz = clazz;
this.rowIndex = rowIndex;
this.styleIndex = styleIndex;
fields = clazz.getDeclaredFields();
}
/**
* 基于注解导出
参数:
response:
InputStream:模板的输入流
objs:数据
fileName:生成的文件名
*
*/
public void export(HttpServletResponse response,InputStream is, List<T> objs,String fileName) throws Exception {
//1.根据模板创建工作簿
XSSFWorkbook workbook = new XSSFWorkbook(is);
//2.读取工作表
Sheet sheet = workbook.getSheetAt(0);
//3.提取公共的样式
CellStyle[] styles = getTemplateStyles(sheet.getRow(styleIndex));
//4.根据数据创建每一行和每一个单元格的数据2
AtomicInteger datasAi = new AtomicInteger(rowIndex); //数字
for (T t : objs) {
//datasAi.getAndIncrement() :获取数字,并++ i++
Row row = sheet.createRow(datasAi.getAndIncrement());
for(int i=0;i<styles.length;i++) {
Cell cell = row.createCell(i);
cell.setCellStyle(styles[i]);
for (Field field : fields) {
if(field.isAnnotationPresent(ExcelAttribute.class)){
field.setAccessible(true);
ExcelAttribute ea = field.getAnnotation(ExcelAttribute.class);
if(i == ea.sort()) {
if(field.get(t) != null) {
cell.setCellValue(field.get(t).toString());
}
}
}
}
}
}
fileName = URLEncoder.encode(fileName, "UTF-8");
response.setContentType("application/octet-stream");
response.setHeader("content-disposition", "attachment;filename=" + new String(fileName.getBytes("ISO8859-1")));
response.setHeader("filename", fileName);
workbook.write(response.getOutputStream());
}
public CellStyle[] getTemplateStyles(Row row) {
CellStyle [] styles = new CellStyle[row.getLastCellNum()];
for(int i=0;i<row.getLastCellNum();i++) {
styles[i] = row.getCell(i).getCellStyle();
}
return styles;
}
}
1、实现导出数据之前要在EmployeeReportResult返回值类中加上相应的注解
package com.ihrm.domain.employee.response;
import com.ihrm.domain.employee.EmployeeResignation;
import com.ihrm.domain.employee.UserCompanyPersonal;
import com.ihrm.domain.poi.ExcelAttribute;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.beans.BeanUtils;
@Getter
@Setter
@NoArgsConstructor
@ToString
public class EmployeeReportResult {
@ExcelAttribute(sort = 0)
private String userId;
@ExcelAttribute(sort = 1)
private String username;
private String departmentName;
@ExcelAttribute(sort = 2)
private String mobile;
@ExcelAttribute(sort = 9)
private String timeOfEntry;
private String companyId;
private String sex;
/**
* 出生日期
*/
private String dateOfBirth;
/**
* 最高学历
*/
@ExcelAttribute(sort = 3)
private String theHighestDegreeOfEducation;
/**
* 国家地区
*/
@ExcelAttribute(sort = 4)
private String nationalArea;
/**
* 护照号
*/
@ExcelAttribute(sort = 5)
private String passportNo;
/**
* 身份证号
*/
private String idNumber;
/**
* 身份证照片-正面
*/
private String idCardPhotoPositive;
/**
* 身份证照片-背面
*/
private String idCardPhotoBack;
/**
* 籍贯
*/
@ExcelAttribute(sort = 6)
private String nativePlace;
/**
* 民族
*/
private String nation;
/**
* 英文名
*/
private String englishName;
/**
* 婚姻状况
*/
private String maritalStatus;
/**
* 员工照片
*/
private String staffPhoto;
/**
* 生日
*/
@ExcelAttribute(sort = 7)
private String birthday;
/**
* 属相
*/
@ExcelAttribute(sort = 8)
private String zodiac;
/**
* 年龄
*/
private String age;
/**
* 星座
*/
private String constellation;
/**
* 血型
*/
private String bloodType;
/**
* 户籍所在地
*/
private String domicile;
/**
* 政治面貌
*/
private String politicalOutlook;
/**
* 入党时间
*/
private String timeToJoinTheParty;
/**
* 存档机构
*/
private String archivingOrganization;
/**
* 子女状态
*/
private String stateOfChildren;
/**
* 子女有无商业保险
*/
private String doChildrenHaveCommercialInsurance;
/**
* 有无违法违纪行为
*/
private String isThereAnyViolationOfLawOrDiscipline;
/**
* 有无重大病史
*/
private String areThereAnyMajorMedicalHistories;
/**
* QQ
*/
private String qq;
/**
* 微信
*/
private String wechat;
/**
* 居住证城市
*/
private String residenceCardCity;
/**
* 居住证办理日期
*/
private String dateOfResidencePermit;
/**
* 居住证截止日期
*/
private String residencePermitDeadline;
/**
* 现居住地
*/
private String placeOfResidence;
/**
* 通讯地址
*/
private String postalAddress;
/**
* 联系手机
*/
private String contactTheMobilePhone;
/**
* 个人邮箱
*/
private String personalMailbox;
/**
* 紧急联系人
*/
private String emergencyContact;
/**
* 紧急联系电话
*/
private String emergencyContactNumber;
/**
* 社保电脑号
*/
private String socialSecurityComputerNumber;
/**
* 公积金账号
*/
private String providentFundAccount;
/**
* 银行卡号
*/
private String bankCardNumber;
/**
* 开户行
*/
private String openingBank;
/**
* 学历类型
*/
private String educationalType;
/**
* 毕业学校
*/
private String graduateSchool;
/**
* 入学时间
*/
private String enrolmentTime;
/**
* 毕业时间
*/
private String graduationTime;
/**
* 专业
*/
private String major;
/**
* 毕业证书
*/
private String graduationCertificate;
/**
* 学位证书
*/
private String certificateOfAcademicDegree;
/**
* 上家公司
*/
private String homeCompany;
/**
* 职称
*/
private String title;
/**
* 简历
*/
private String resume;
/**
* 有无竞业限制
*/
private String isThereAnyCompetitionRestriction;
/**
* 前公司离职证明
*/
private String proofOfDepartureOfFormerCompany;
/**
* 备注
*/
private String remarks;
/**
* 离职时间
*/
@ExcelAttribute(sort = 12)
private String resignationTime;
/**
* 离职类型
*/
@ExcelAttribute(sort = 10)
private String typeOfTurnover;
/**
* 申请离职原因
*/
@ExcelAttribute(sort = 11)
private String reasonsForLeaving;
public EmployeeReportResult(UserCompanyPersonal personal, EmployeeResignation resignation) {
BeanUtils.copyProperties(personal,this);
if(resignation != null) {
BeanUtils.copyProperties(resignation,this);
}
}
}
2、导出数据
修改ihrm_employee中的EmployeeController
@RequestMapping(value = "/export/{month}", method = RequestMethod.GET)
public void export(@PathVariable String month) throws Exception {
//1.获取报表数据
List<EmployeeReportResult> list = userCompanyPersonalService.findByReport(companyId,month);
//2.加载模板
Resource resource = new ClassPathResource("excel-template/hr-demo.xlsx");
FileInputStream fis = new FileInputStream(resource.getFile());
//3.通过工具类完成下载
new ExcelExportUtil(EmployeeReportResult.class,2,2).
export(response,fis,list,month+"人事报表.xlsx");
3、导入数据
修改ihrn_system中的UserController
List<User> list = new ExcelImportUtil(User.class).readExcel(is, 1, 2);
要想成功实现导入数据的操作,也需要在返回值对象中对在数据库中的相应字段加上相应的自定义的注解。
我们都知道Excel可以分为早期的Excel2003版本(使用POI的HSSF对象操作)和Excel2007版本(使用POI的XSSF操作),两者对百万数据的支持如下:
没有性能监控工具一切推论都只能停留在理论阶段,我们可以使用Java的性能监控工具来监视程序的运行情况,包括CUP,垃圾回收,内存的分配和使用情况,这让程序的运行阶段变得更加可控,也可以用来证明我们的推测。这里我们使用JDK提供的性能工具Jvisualvm来监控程序运行。
VisualVM 是Netbeans的profile子项目,已在JDK6.0 update 7 中自带,能够监控线程、内存情况、查看方法的CPU时间和内存中的对象、已被GC的对象、反向查看分配的堆栈
visualvm位于JAVA_HOME/bin目录下,直接双击就可以打开该程序。如果只是监控本地的java进程,是不需要配置参数的,直接打开就能够进行监控。首先我们需要在本地打开一个Java程序,例如我打开员工微服务进程,这时在jvisualvm界面就可以看到与IDEA相关的Java进程了。
Jvisualvm使用起来比较简单,双击点击当前运行的进程即可进入到程序的监控界面
对于百万数据量的Excel导入导出,只讨论基于Excel2007的解决方法。在ApachePoi 官方提供了对操作大数据量的导入导出的工具和解决办法,操作Excel2007使用XSSF对象,可以分为三种模式:
使用Apache POI完成百万数据量的Excel报表导出。
基于XSSFWork导出Excel报表,是通过将所有单元格对象保存到内存中,当所有的Excel单元格全部创建完成之后一次性写入到Excel并导出。当百万数据级别的Excel导出时,随着表格的不断创建,内存中的对象会越来越多,直至内存溢出。Apache POI提供了SXSSFWork对象,专门用于处理大数据量Excel报表导出。
在实例化SXSSFWork这个对象时,可以指定在内存中所产生的POI导出相关对象的数量(默认100),一旦内存中的对象的个数达到这个指定值时,就将内存中的这些对象的内容写入到磁盘中(XML的文件格式),就可以将这些对象从内存中销毁,以后只要达到这个值,就会以类似的处理方式处理,直至Excel导出完成。
SXSSFWork:通过以xml临时文件的方式,帮助存储临时的内存对象,当达到阈值之后将内存的对象写入到临时文件中,但是也是先写入内存在写入到临时文件中。写内存的时间肯定要比写入磁盘上的临时文件的时间要快,因此如果是长时间操作的话也会造成内存溢出的错误。操作大数据量的表格时减少内存的使用常使用的是尽量减少对象的产生,忽略样式对象和字体对象。
只需要在原有代码的基础上替换之前的XSSFWorkbook,使用SXSSFWorkbook完成创建过程即可
//Workbook wb = new XSSFWorkbook();
SXSSFWorkbook wb = new SXSSFWorkbook(100); //阈值,内存中的对象数量最大数量
注意:SXSSF对象不支持模板打印
使用POI基于事件模式解析案例提供的Excel文件
1、设置POI事件模式
2、Sax解析
我们都知道对于Excel2007的实质是一种特殊的XML存储数据,那就可以使用基于SAX的方式解析XML完成Excel的读取。SAX提供了一种从XML文档中读取数据的机制。它逐行扫描文档,一边扫描一边解析。由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势
package poi.test.handler;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
import org.apache.poi.xssf.usermodel.XSSFComment;
import poi.test.entity.PoiEntity;
/**
* 自定义的事件处理器
* 处理每一行数据读取
* 实现接口
*/
public class SheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler {
private PoiEntity poiEntity;
/**
* 开始解析某一行时候触发
* @param i 行索引
*/
@Override
public void startRow(int i) {
if (i > 0 ){
poiEntity = new PoiEntity();
}
}
/**
* 结束解析某一行时候触发
* @param i 行索引
*/
@Override
public void endRow(int i) {
System.out.println(poiEntity);
}
/**
* 对行中的每一个表格进行处理
* @param cellReference 单元格名称
* @param value 数据
* @param xssfComment 批注
*/
@Override
public void cell(String cellReference, String value, XSSFComment xssfComment) {
if (poiEntity != null){
String s = cellReference.substring(0, 1);
switch (s){
case "A":
poiEntity.setId(value);
break;
case "B":
poiEntity.setBreast(value);
break;
case "C":
poiEntity.setAdipocytes(value);
break;
case "D":
poiEntity.setNegative(value);
break;
case "E":
poiEntity.setStaining(value);
break;
case "F":
poiEntity.setSupportive(value);
break;
default:
break;
}
}
}
}
package poi.test;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.model.StylesTable;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import poi.test.handler.SheetHandler;
import java.io.InputStream;
/**
* 使用事件模型解析百万数据Excel报表
*/
public class PoiTest06 {
public static void main(String[] args) throws Exception{
String path = "/Users/xiaotongxin/IdeaProjects/hrm/poi-test/src/poi/demo.xlsx";
//1、根据Excel报表获取OPCPackage
OPCPackage opcPackage = OPCPackage.open(path);
//2、创建XSSFReader
XSSFReader xssfReader = new XSSFReader(opcPackage);
//3、获取SharedStringTable对象
SharedStringsTable sharedStringsTable = xssfReader.getSharedStringsTable();
//4、获取styleTable对象
StylesTable stylesTable = xssfReader.getStylesTable();
//5、创建Sax的xmlReader对象
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
//6、注册事件处理器
XSSFSheetXMLHandler xssfSheetXMLHandler = new XSSFSheetXMLHandler(stylesTable, sharedStringsTable,
new SheetHandler(), false);
xmlReader.setContentHandler(xssfSheetXMLHandler);
//7、逐行处理
XSSFReader.SheetIterator sheetIterator = (XSSFReader.SheetIterator)xssfReader.getSheetsData();
while (sheetIterator.hasNext()){
InputStream inputStream = sheetIterator.next();
InputSource inputSource = new InputSource(inputStream);
xmlReader.parse(inputSource);
}
}
}
通过简单的分析以及运行两种模式进行比较,可以看到用户模式下使用更简单的代码实现了Excel读取,但是在读取大文件时CPU和内存都不理想;而事件模式虽然代码写起来比较繁琐,但是在读取大文件时CPU和内存更占优势。