220907更新

项目UAT期间,用户反映了一个问题,数据量稍大的情况下,会出现“从某一行开始,往下所有行设置的字体和字体大小不生效”的BUG。
经过排查,发现原因是:POI的XSSFCellStyle不能创建次数过多,否则超过的部分设置无法生效,根据客户的情况估算大约这个次数限制在500-1000次之间。我原本的代码中,每格都重新创建了一次XSSFCellStyle,显然创建次数过多,解决方法也很简单,设置相同的单元格只创建一次XSSFCellStyle就OK
步骤三工具类代码已经更新。


写在前面:

近期在做的项目中需要有Excel导出需求,因为多处都涉及该功能且数据字段较多,来源不唯一。因为导出操作里有大量重复的逻辑,因此想提取出一套通用的模板,最终通过自定义注解+自己编写工具类的方式来简化代码。

开发环境JAVA11(没有涉及其他新内容,仅部分地方使用了Lambda表达式,理论上JAVA8以上即可以运行)
excel基础操作涉及的外部依赖仅使用了poi-ooxml 4.1.1版本(详见步骤三)

实现步骤:

步骤一:编写自定义注解@ExportExcelSheet.java

package com.poiexcel;import java.lang.annotation.*;/*** @Description: 注解-标识一个类是用于导出excel的类* @Author : wqin* @DateTime : yyyy/MM/dd HH:mm*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExportExcelSheet {// value值即为sheet名String value();
}

解释说明:该注解用在希望导出的实体类的类名上方,用以标注该类需要被导出,且能够被我们接下来会编写的自定义工具类识别然后正确导出。其中value字段记录了sheet名,因为我的项目需求中单个导出的Excel文件只存在一个sheet页的情况,因此这次开发过程中不需要考虑单文件多sheet的适配情况。

步骤二:编写自定义注解@ExportExcelField.java

package com.poiexcel;import java.lang.annotation.*;/*** @Description: 注解-标识一个字段是需要导出excel的字段* @Author : wqin* @DateTime : yyyy/MM/dd HH:mm*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExportExcelField {// 记录字段名String title() default "未知列名";// 预设列宽int width() default 10;// 预设允许的最大列宽int maxWidth() default 30;// 列顺序,越小越靠前int order() default 65535;// 该列是否自适应列宽boolean autoSized() default true;
}

解释说明:该注解用于实体类中希望导出的字段上,用以标注该字段能够也将会被导出。字段上的注解生效的前提是该类被步骤一里的注解@ExportExcelSheet标记。该注解中的字段就有很多属性可以设置了,例如:

  • title字段将来作为导出时的字段名
  • width字段为预设的列宽,如果没有数据或者数据最大长度都短于该值那么列宽为设定值
  • maxWidth字段为预设的最大列宽,若数据长度大于最大列宽,也不会超过该值
  • order字段为导出的时候列的顺序,越小越靠前a
  • autoSized字段为布尔值,用以标明该列列宽是否自适应,若为false则根据width的值设置列宽,否则由该列数据最长的长度和maxWidth两者较小值决定

步骤三:编写工具类ExportExcelUtil.java

package com.poiexcel;import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.xssf.usermodel.*;import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.*;/*** @Description: 基于poi的excel报表导出工具类* @Author : wqin* @DateTime : yyyy/MM/dd HH:mm*/
public class ExportExcelUtil {// 字体private static final String FONT_STYLE_CALIBRI = "Calibri";// 边框类型private static final int BORD_MID= 0;private static final int BORD_LEFT = 1;private static final int BORD_RIGHT = 2;private static final int BORD_ALL = 3;// 默认参数private static final short DEFAULT_COLUMN_WIDTH = 10*1;private static final short DEFAULT_ROW_HEIGHT = (short) (15*20);// 自动调整列宽private static final boolean AOTU_CLOUMN_WIDTH = true;/*** 通过数据列表和类对象,获取一个简单表头的XSSFworkbook对象(单sheet,且无递归情况)* @param dataList 数据列表* @param annotatedClass 有ExportExcelSheet注解* @return XSSFWorkbook对象*/public static XSSFWorkbook getSimpleXSSFWorkbook(List dataList, Class annotatedClass) {// 1. 一些基础操作Map cglab=new HashMap();//创建一个ExcelXSSFWorkbook wb = new XSSFWorkbook();//ExportExcelSheet注解里存放了sheet名ExportExcelSheet excelSheet= (ExportExcelSheet)annotatedClass.getAnnotation(ExportExcelSheet.class);//excelSheet.value()获取sheet名然后设置sheet名XSSFSheet sheet;if (excelSheet != null){sheet = wb.createSheet(excelSheet.value());}else {return null;}//设置默认的列宽和行高(注意该方法传的值不要*256)sheet.setDefaultColumnWidth(DEFAULT_COLUMN_WIDTH);// 设置前两行冻结sheet.createFreezePane(0,2,0,2);// 定义一个存放类的栈List<Class> classList=new ArrayList();// 定义一个存放每个类开始位置List<Integer> classListHeadIndex=new ArrayList();classList.add(annotatedClass);classListHeadIndex.add(0);// 2. 获取所有需要导出的列// 2.1 获取要导出列的所有字段Field[] fields = annotatedClass.getDeclaredFields();List<Field> exportFields = new LinkedList<>();// 2.2 将其中有ExportExcelField注解的字段取出并按order顺序从小到大排序for (Field field : fields) {ExportExcelField excelField = field.getAnnotation(ExportExcelField.class);if (excelField != null) {exportFields.add(field);}}if (exportFields.size() > 0){exportFields.sort((o1, o2) -> {ExportExcelField excelField1 = o1.getAnnotation(ExportExcelField.class);ExportExcelField excelField2 = o2.getAnnotation(ExportExcelField.class);return excelField1.order() - excelField2.order();});}else {return null;}// 2.3 创建一个记录每列记录的最大长度的列表,从表头开始记录长度List<Integer> widthList = new ArrayList();// 2.4 注解里每列允许的最大列宽List<Integer> maxWidthList = new ArrayList();// 2.5 注解里每列是否允许自动适应列宽List<Boolean> autoSizedList = new ArrayList<>();// 3. 构造表// 3.1 创建第一行XSSFRow firstRow = sheet.createRow(0);firstRow.setHeight(DEFAULT_ROW_HEIGHT);for (int i = 0; i < exportFields.size(); i++){XSSFCell cell = firstRow.createCell(i);int bordType = BORD_MID;if (1 == exportFields.size()){bordType = BORD_ALL;}else if (i == 0){bordType = BORD_LEFT;}else if(i == exportFields.size()-1){bordType = BORD_RIGHT;}setFirstRowStyle(cell,wb,bordType);}// 3.2 创建表头(第二行)XSSFRow headRow = sheet.createRow(1);headRow.setHeight(DEFAULT_ROW_HEIGHT);XSSFCellStyle headStyle = createHeadStyle(wb);for (int i = 0; i < exportFields.size(); i++) {XSSFCell cell = headRow.createCell(i);ExportExcelField exportExcelField = exportFields.get(i).getAnnotation(ExportExcelField.class);cell.setCellValue(exportExcelField.title());maxWidthList.add(exportExcelField.maxWidth());autoSizedList.add(exportExcelField.autoSized());widthList.add(Math.max(exportExcelField.title().getBytes().length, DEFAULT_COLUMN_WIDTH));setCellStyle(cell, headStyle);}// 3.3 创建主体内容(第三-N行)XSSFCellStyle bodyStyle = createBodyStyle(wb);for (int i = 0; null != dataList && i < dataList.size(); i++) {XSSFRow row = sheet.createRow(i + 2);row.getCTRow().setCustomHeight(false);Object dataObject = dataList.get(i);for (int j = 0; j < exportFields.size(); j++) {XSSFCell cell = row.createCell(j);Object value = getFieldValueByFieldName(exportFields.get(j).getName(), dataObject);int valueLength = setValue(cell, value);assert value != null;setCellStyle(cell, bodyStyle);if (valueLength > widthList.get(j)){widthList.set(j, Math.min(valueLength, maxWidthList.get(j)));}}}if (AOTU_CLOUMN_WIDTH){setAutoSizeColumn(sheet, widthList, autoSizedList);}return wb;}/*** 设置第一行单元格样式* 边框:整体第一行的外边缘有边框* 填充: 灰色* @param bordType 边框类型,0-中间,1-最左,2-最右*/private static void setFirstRowStyle(XSSFCell xssfCell, XSSFWorkbook workBook, Integer bordType){XSSFCellStyle style = workBook.createCellStyle();// 设置背景颜色(灰色)byte[] colorRgb = { (byte)219, (byte)219, (byte)219 };XSSFColor xssfColor = new XSSFColor(colorRgb, new DefaultIndexedColorMap());xssfColor.setRGB(colorRgb);style.setFillForegroundColor(xssfColor);style.setFillPattern(FillPatternType.SOLID_FOREGROUND);switch (bordType){case 0:// 中间位置的单元格,上下有边框,左右无边框style.setBorderTop(BorderStyle.THIN);style.setBorderBottom(BorderStyle.THIN);style.setBorderLeft(BorderStyle.NONE);style.setBorderRight(BorderStyle.NONE);break;case 1:// 最左位置的单元格,右侧无边框,其他三侧有边框style.setBorderTop(BorderStyle.THIN);style.setBorderBottom(BorderStyle.THIN);style.setBorderLeft(BorderStyle.THIN);style.setBorderRight(BorderStyle.NONE);break;case 2:// 最右位置的单元格,左侧无边框,其他三侧有边框style.setBorderTop(BorderStyle.THIN);style.setBorderBottom(BorderStyle.THIN);style.setBorderLeft(BorderStyle.NONE);style.setBorderRight(BorderStyle.THIN);break;default:// 极端情况全边框style.setBorderTop(BorderStyle.THIN);style.setBorderBottom(BorderStyle.THIN);style.setBorderLeft(BorderStyle.THIN);style.setBorderRight(BorderStyle.THIN);}xssfCell.setCellStyle(style);}/*** 产生表头单元格样式* 字体:Calibri 10号 加粗 水平垂直居中* 边框:全边框* 填充: 60% 淡蓝色*/private static XSSFCellStyle createHeadStyle(XSSFWorkbook workBook) {XSSFCellStyle style = workBook.createCellStyle();// 创建字体对象XSSFFont font = workBook.createFont();// 设置字体大小font.setFontHeightInPoints((short) 10);// 设置粗体font.setBold(true);// 设置为字体font.setFontName(FONT_STYLE_CALIBRI);// 将字体加入到样式对象style.setFont(font);// 设置背景颜色(60% 淡蓝色)byte[] colorRgb = { (byte)189, (byte)215, (byte)238 };XSSFColor xssfColor = new XSSFColor(colorRgb, new DefaultIndexedColorMap());style.setFillForegroundColor(xssfColor);style.setFillPattern(FillPatternType.SOLID_FOREGROUND);// 设置对齐方式// 水平居中style.setAlignment(HorizontalAlignment.CENTER);// 垂直居中style.setVerticalAlignment(VerticalAlignment.CENTER);// 设置边框(上下左右全边框)style.setBorderTop(BorderStyle.THIN);style.setBorderBottom(BorderStyle.THIN);style.setBorderLeft(BorderStyle.THIN);style.setBorderRight(BorderStyle.THIN);return style;}/*** 产生主体数据单元格样式* 字体:Calibri 10号 水平垂直居中* 边框:每格四周都有边框* 填充:无*/private static XSSFCellStyle createBodyStyle(XSSFWorkbook workBook) {XSSFCellStyle style = workBook.createCellStyle();// 创建字体对象XSSFFont font = workBook.createFont();// 设置字体大小font.setFontHeightInPoints((short) 10);// 设置粗体font.setBold(false);// 设置字体font.setFontName(FONT_STYLE_CALIBRI);// 将字体加入到样式对象style.setFont(font);// 设置对齐方式// 水平居中style.setAlignment(HorizontalAlignment.CENTER);// 垂直居中style.setVerticalAlignment(VerticalAlignment.CENTER);// 设置边框(上下左右全边框)style.setBorderTop(BorderStyle.THIN);style.setBorderBottom(BorderStyle.THIN);style.setBorderLeft(BorderStyle.THIN);style.setBorderRight(BorderStyle.THIN);// 自动换行style.setWrapText(true);return style;}/*** 设置单元格样式* @param xssfCell 单元格对象* @param style 样式对象*/private static void setCellStyle(XSSFCell xssfCell, XSSFCellStyle style){xssfCell.setCellStyle(style);}/*** 通过属性名字,调用相应的Get方法获取属性值* @param object 目标对象* @param fieldName 需要获取的属性名字* @return 通过getXxxx(fieldName)获取到的对象属性*/private static Object getFieldValueByFieldName(String fieldName, Object object) {Class c=object.getClass();try {String s=fieldName.substring(0,1).toUpperCase()+fieldName.substring(1);Method method=c.getMethod("get"+s);return method.invoke(object);} catch (Exception e) {return null;}}/*** 设置表格内容的值* @param xssfCell 单元格对象* @param value 填充单元格的值对象* @return valueLength 返回数据的预期长度(区别中英文长度)*/private static int setValue(XSSFCell xssfCell,Object value){int valueLength = 0;if (value instanceof String) {xssfCell.setCellValue(value.toString());valueLength = value.toString().getBytes().length;}else if (value instanceof Integer) {xssfCell.setCellValue((Integer) value);valueLength = value.toString().getBytes().length;} else if (value instanceof Double) {xssfCell.setCellValue((Double) value);valueLength = value.toString().getBytes().length;} else if (value instanceof Boolean) {xssfCell.setCellValue((Boolean) value);valueLength = value.toString().getBytes().length;} else if (value instanceof Float) {xssfCell.setCellValue((Float) value);valueLength = value.toString().getBytes().length;} else if (value instanceof Short) {xssfCell.setCellValue((Short) value);valueLength = value.toString().getBytes().length;}else if (value instanceof Long) {xssfCell.setCellValue((Long) value);valueLength = value.toString().getBytes().length;} else if (value instanceof Character) {xssfCell.setCellValue((Character) value);valueLength = value.toString().getBytes().length;} else if (value instanceof Date){SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");String formatDate = simpleDateFormat.format(value);xssfCell.setCellValue(formatDate);valueLength = formatDate.getBytes().length;}return valueLength;}/*** 自动适应列宽* @param xssfSheet sheet对象* @param widthList 记录了第i列可能需要被设置的列宽* @param autoSizedList 记录了第i列是否自适应列宽的布尔值*/private static void setAutoSizeColumn(XSSFSheet xssfSheet,List<Integer> widthList,List<Boolean> autoSizedList){for (int i = 0; i < widthList.size() ; i++) {if (autoSizedList.get(i)){xssfSheet.setColumnWidth(i, (widthList.get(i)+1)*256);}}}
}

解释说明:
Excel相关基础操作用的是POI,本人是Maven项目,记得在Pom.xml加依赖。

 <dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.1.1</version></dependency>

其他的一些说明和注意点都在代码中注释了。

步骤四:编写实体类(或给已有实体类添加注解)

package com.poiexcel;import java.io.Serializable;
import java.util.Date;/*** @Description: 用于导出的实体类* @Author : wqin* @DateTime : yyyy/MM/dd HH:mm*/
@ExportExcelSheet("Book")
public class Book implements Serializable {@ExportExcelField(title = "书名", order = 1)private String title;@ExportExcelField(title = "书名(英文)", order = 2)private String titleEn;@ExportExcelField(title = "作者", order = 20)private String authors;@ExportExcelField(title = "出版社", order = 4, width = 20, autoSized = false)private String publishingHouse;@ExportExcelField(title = "出版日期")private Date publishingDate;@ExportExcelField(title = "ISBN", order = 6)private String isbn;@ExportExcelField(title = "单价/元", order = 7)private Double price;/*** 注意!!!* 此处为了看起来方便,我省略了Getter和Setter方法还有构造方法* 实际上功能实现逻辑中要求必须有以上方法,运行Demo的时候记得自行加上* 可以引入lombok依赖加上需要的注解*/
}

解释说明:
此处也可以选择你已经有的实体类在类名和需要导出的属性上分别加上@ExportExcelSheet和@ExportExcelField注解即可。

步骤五:编写测试类

package com.poiexcel;import org.apache.poi.xssf.usermodel.XSSFWorkbook;import java.io.FileOutputStream;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;/*** @Description: Excel导出功能测试类* @Author : wqin* @DateTime : yyyy/MM/dd HH:mm*/
public class ExportExcelUtilTest {public static void main(String[] args) throws ParseException {// 构建数据List,随便赛点数据进去即可List<Book> books = new ArrayList<>();SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");books.add(new Book("编码:隐匿在计算机软硬件背后的语言","Code: The Hidden Language of Computer Hardware and Software","Charles Petzold","电子工业出版社",sdf.parse("2012-10-01"),"7121181185",57.2));books.add(new Book("世界是数字的","D is for Digital","Brian Kernighan","人民邮电出版社",sdf.parse("2011-11-11"),"7115318751",49.0));books.add(new Book("深入理解计算机系统(第2版)","Computer Systems:A Programmer's Perspective","Randal E. Bryant & David R. O'Hallaron","机械工业出版社",sdf.parse("2011-01-01"),"7111321332",99.0));// 调用我们的工具类ExportExcelUtil的静态方法getSimpleXSSFWorkbook创建出一个XSSFWorkbook对象// 入参为数据list及数据实体类的.classXSSFWorkbook wb = ExportExcelUtil.getSimpleXSSFWorkbook(books, Book.class);// 将上面获取到的XSSFWorkbook对象导出为Excel文件FileOutputStream fileOutputStream = null;String filePath = "这里填写你的文件路径(绝对路径)";try {fileOutputStream=new FileOutputStream(filePath + "这里填写你的文件名" + ".xlsx");if (wb != null){wb.write(fileOutputStream);}} catch (IOException e) {e.printStackTrace();} finally {if (fileOutputStream != null){try {fileOutputStream.close();} catch (IOException e) {e.printStackTrace();}}}}
}

解释说明:
至此基本功能已经实现,当你需要:

  • 增删要导出的字段:在实体类中新增/删除字段和注解(当然也可以只删除注解,这样就不会导出了)
  • 改变导出表格的样式:修改或扩充工具类里对应内容

需要补充说明的是,目前的功能中,需要事先将数据处理好,工具类对数据本身是不做任何处理的。
最后附上导出的文件内容:

【开发随机】JAVA+POI+自定义注解+反射构建自定义工具类实现快捷简便的Excel模板化导出(附demo代码)相关推荐

  1. java自定义注解处理器_java自定义注解学习(注解处理器)

    如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了.使用注解的过程中,很重要的一部分就是创建于使用注解处理器.Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处 ...

  2. java 自定义json解析注解 复杂json解析 工具类

    java 自定义json解析注解 复杂json解析 工具类 目录 java 自定义json解析注解 复杂json解析 工具类 1.背景 2.需求-各式各样的json 一.一星难度json[json对象 ...

  3. Java - HuTool 使用 EscapeUtil、XmlUtil等工具类(四)

    Java - HuTool 使用 EscapeUtil.XmlUtil等工具类(四) 本篇主要介绍 HuTool工具, 其是 java工具类,对于一些静态方法进行封装,虽然很小,但很全,里面拥有平时我 ...

  4. POI读取Excel模板并导出大量数据

    POI读取Excel模板并导出大量数据 我在使用XSSFWorkbook读取Excel模板并导出大量数据(百万级)时,发现很长时间没有响应,debugger模式发现在读取第三四十万条数据时,程序直接停 ...

  5. JAVA之多sheet页表格生成工具类

    JAVA之多sheet页表格生成工具类 主要方法: import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.h ...

  6. java 手机号脱敏,身份证号脱敏 工具类

    java 手机号脱敏,身份证号脱敏 工具类 import org.apache.commons.lang3.StringUtils;/*** * @title: 脱敏工具类* @author: wll ...

  7. 封装自定义的redis切库工具类ByteArrayRedisTemplate,读取byte数组反序列化成List<Object>

    封装自定义的redis切库工具类ByteArrayRedisTemplate,读取byte数组反序列化成List<Object>(使用lettuce连接池) 代码环境 框架:springb ...

  8. java将链接生成二维码工具类

    一.添加依赖 <!-- 生成二维码--><dependency><groupId>com.google.zxing</groupId><artif ...

  9. Java生成和解析二维码工具类(简单经典)

    Java生成和解析二维码工具类 开箱即用,简单不废话. pom.xml引入依赖 <!-- https://mvnrepository.com/artifact/com.google.zxing/ ...

最新文章

  1. 实验二 建立基本的游戏场景
  2. 深入理解分布式技术 - 漫谈分布式事务及解决方案
  3. C++中的空类,默认产生哪些类成员函数?
  4. Java垃圾收集蒸馏
  5. 第七季4:网络telnet调试、海思proc文件系统调试接口
  6. Java技术知识点的一些总结
  7. 百度seo排名规则_SEO界的潜规则(百度死不承认的SEO秘密)
  8. python 读grid 数据_jqGrid获取到python返回的Json数据,未能显示到页面上,为毛???...
  9. CVonline: Image Databases
  10. linux使用磁盘配额,linux磁盘配额使用
  11. 《2020年全球程序员收入报告》,看完报告我酸了!
  12. SAP CO-PC物料标准价格更改方案
  13. 华为eNSP配置FR 帧中继协议
  14. left join和left outer join 有什么区别?这样解释通俗易懂
  15. 软件工程实训有必要吗_软件工程专业实训心得体会
  16. 计算机软考access 题的做法_计算机二级Access 操作题汇总
  17. 最全各种浏览器网页星号点号密码查看最简方法(技术小白也能看懂使用)
  18. 强联通分量与双连通分量
  19. 方舟生存进化助手隐私政策条款
  20. camera Intrinsic and Extrinsic

热门文章

  1. 工作11年后一切归零
  2. java重复代码重构_重构重复代码
  3. web开发前台,懒人建站资源库
  4. SpringCloud-gateway资料以及详解
  5. MySQL 千万级数据处理
  6. elementUI表格中气泡位置偏移
  7. 如何在Vue3中使用router
  8. 囚徒健身(中文完整版)(保罗·威德)
  9. Python批量处理表格有用吗_python批量读入图片、处理并批量输出(可用于深度学习训练集的制作)...
  10. 使用 Python 脚本执行国密 sm2 加解密