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

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();
        //创建一个Excel
        XSSFWorkbook 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及数据实体类的.class
        XSSFWorkbook 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();
                }
            }
        }
    }
}

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

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

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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值