Excel导出与上传实现,包含单元格样式,合并单元格,对象属性的属性值获取等

年前上线的最后一个需求是对业务系统进行界面优化,其中我所负责的是按照优化界面进行定制化的Excel导出和上传。和以往导出上传不同点在于:
1、导出项会根据页面的不同选择导出相应的属性列;
2、导出DTO对象中不再是仅包含导出属性,还包含属性对象,属性对象中还有自定义属性,而导出属性除了DTO中的属性外,还得导出属性对象中的自定义属性值;
3、导出表头背景色不同,数据格式不同,且存在合并单元格等。

本文仅对所做的下载与上传部分作逻辑说明,对以后进行相关开发起到一个参考借鉴作用。注意两个方法:导出多元属性解析的getWeekDataValue和上传多元属性解析的getSetterMethod。

一、自定义下载注解:ColumnDesc

该下载注解,主要是为了在下载时,根据所写的下载规则,获取其注解内的title等信息,从而构建出所需要的Excel表头。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ColumnDesc {
    /**
     *  title,可以用来构建表头的文字内容
    */
    String title() default "";
    /**
    * index,可以用来指定表头的顺序
    */
    int index() default 0;
}

二、导出Dto对象,也可作上传Dto对象

@Getter
@Setter
@ToString
public class ExportDto {  // 导出对象
    @ColumnDesc(title = "商品编码", index = 0)
    private String id;
    
    @ColumnDesc(title = "商品名称", index = 0)
    private String name;
    
    @ColumnDesc
    private WeekData weekData;// 周数据
}

@Getter
@Setter
@ToString
public class WeekData { //周数据对象
    @ColumnDesc(title = "销售数量")
    private double sales;
    @ColumnDesc(title = "销售金额")
    private double saleroom;
}

如上所示,我们构建了一个导出Dto,其属性赋予了自定义注解,Dto内还设置了一个对象属性,该对象属性内的属性是我们所要构建表头时所需要的内容。

三、导出工具类ExcelWriter

1、ExcelWriter属性及一些导出设置

由于导出涉及到合并单元格等问题,因此设计了两个header。以下为导出类的一些属性。

public class ExcelWriter {
    /**
     *  表头1,含有合并项的
     */
    private List firstHeader;
    /**
     *  表头2,和数据项一一对应的表头
     */
    private List secondHeader;
    /**
     * 组装SecondHeader表头
     */
    private List titleAndPropertyList;
    /**
     * 传入的数据集合
     */
    private List contentList;
    /**
     * Excel版本
     */
    private Excel version;
    /**
     * 最大写入限制,默认-1 不限制
     */
    private int maxExportNum = -1;
    /**
     * 第一行是否换行,不换行则高度默认
     */
    private boolean isAutoNewLineOfFirstHeader =  false;
    /**
     *  表示数据的类型
     */
    private Class dataClazz;
}

其中,HeaderProperties是为了设置表头单元格的样式,是否合并单元格,如要合并,则合并长度是多少。

@Getter
@Setter
public class HeaderProperties {
    // 标题
    String title;
    // 是否合并单元格,默认不合并
    boolean isMerged = false;
    // 合并单元格的长度,为0则不合并
    int mergeLength;
    // 单元格样式问题
    CellStyleProperties cellStyleProperties;
    //  表头顺序
    int order;

    public HeaderProperties(String title, boolean isMerged, int mergeLength,
        CellStyleProperties cellStyleProperties, int order) {
        this.title = title;
        this.isMerged = isMerged;
        this.mergeLength = mergeLength;
        this.cellStyleProperties = cellStyleProperties;
        this.order = order;
    }

    public HeaderProperties(String title, CellStyleProperties cellStyleProperties, int order) {
        this.title = title;
        this.isMerged = false;
        this.mergeLength = 0;
        this.cellStyleProperties = cellStyleProperties;
        this.order = order;
    }

    public static List listHeaderProperties(List titles, CellStyleProperties cellStyleProperties, int order) {
        List headerProperties = Lists.newArrayList();
        titles.forEach(title -> {
            headerProperties.add(new HeaderProperties(title, cellStyleProperties, order));
        });

        return headerProperties;
    }
}

单元格的格式设置如下:

import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFFont;
import org.apache.poi.hssf.util.HSSFColor;

@Data
@NoArgsConstructor
public class CellStyleProperties {
    String fontName;//字体
    short fontBoldweight;//字体线条粗细
    short fontHeightInPoints;//字体大小
    short fontColor;//字体颜色
    short fillForegroundColor;//背景色
    // 填充方式
    short fillPattern;
    // 是否自动换行
    boolean autoWrapText;
    // 水平对齐格式
    short alignCenter;
    //设置上下对齐方式
    short crossCenter;
    // 左边框格式
    short borderLeft;
    //边框颜色
    short borderLeftColor;
    // 上边框格式
    short borderTop;
    //边框颜色
    short borderTopColor;
    // 右边框格式
    short borderRight;
    //边框颜色
    short borderRightColor;
    // 下边框格式
    short borderBottom;
    //边框颜色
    short borderBottomColor;


    public CellStyleProperties(String fontName, short fontBoldweight, short fontHeightInPoints,
        short fontColor, short fillForegroundColor, short fillPattern, boolean autoWrapText,
        short alignCenter, short crossCenter, short borderLeft, short borderLeftColor,
        short borderTop, short borderTopColor, short borderRight, short borderRightColor, short borderBottom,
        short borderBottomColor) {
        this.fontName = fontName;
        this.fontBoldweight = fontBoldweight;
        this.fontHeightInPoints = fontHeightInPoints;
        this.fontColor = fontColor;
        this.fillForegroundColor = fillForegroundColor;
        this.fillPattern = fillPattern;
        this.autoWrapText = autoWrapText;
        this.alignCenter = alignCenter;
        this.crossCenter = crossCenter;
        this.borderLeft = borderLeft;
        this.borderLeftColor = borderLeftColor;
        this.borderTop = borderTop;
        this.borderTopColor = borderTopColor;
        this.borderRight = borderRight;
        this.borderRightColor = borderRightColor;
        this.borderBottom = borderBottom;
        this.borderBottomColor = borderBottomColor;
    }

    /**
     *  默认边框格式
     * @param fontName
     * @param fontBoldweight
     * @param fontHeightInPoints
     * @param fontColor
     * @param fillForegroundColor
     * @param fillPattern
     * @param alignCenter
     */
    public CellStyleProperties(String fontName, short fontBoldweight, short fontHeightInPoints,
        short fontColor, short fillForegroundColor, short fillPattern, boolean autoWrapText, short alignCenter) {
        this(fontName, fontBoldweight, fontHeightInPoints, fontColor, fillForegroundColor,
            fillPattern, autoWrapText, alignCenter, HSSFCellStyle.VERTICAL_CENTER,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index);
    }

    /**
     *  默认边框格式为25%的灰色
     * @param fontName
     * @param fontBoldweight
     * @param fontHeightInPoints
     * @param fontColor
     * @param fillForegroundColor
     * @param fillPattern
     * @param alignCenter
     */
    public CellStyleProperties(String fontName, short fontBoldweight, short fontHeightInPoints,
        short fontColor, short fillForegroundColor, short fillPattern, boolean autoWrapText, short alignCenter, short crossCenter) {
        this(fontName, fontBoldweight, fontHeightInPoints, fontColor, fillForegroundColor,
            fillPattern, autoWrapText, alignCenter, crossCenter,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index);
    }

    /**
     *  默认边框格式为25%的灰色,默认对齐方式为左对齐
     * @param fontName
     * @param fontBoldweight
     * @param fontHeightInPoints
     * @param fontColor
     * @param fillForegroundColor
     * @param fillPattern
     */
    public CellStyleProperties(String fontName, short fontBoldweight, short fontHeightInPoints,
        short fontColor, short fillForegroundColor, short fillPattern) {
        this(fontName, fontBoldweight, fontHeightInPoints, fontColor, fillForegroundColor,
            fillPattern, false, HSSFCellStyle.ALIGN_LEFT, HSSFCellStyle.VERTICAL_CENTER,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index);
    }

    /**
     *  除了填充颜色外,其余均默认
     *  微软雅黑,加粗,字体大小9,字体颜色黑色,背景全填充,居中
     *  边框颜色为灰色
     * @param fillForegroundColor
     */
    public CellStyleProperties(short fillForegroundColor) {
        this("微软雅黑", HSSFFont.BOLDWEIGHT_BOLD, (short)9, HSSFColor.BLACK.index, fillForegroundColor,
            HSSFCellStyle.SOLID_FOREGROUND, false,
            HSSFCellStyle.ALIGN_CENTER, HSSFCellStyle.VERTICAL_CENTER,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index);
    }

    /**
     *  除了填充颜色和是否自动对齐外,其余均默认
     *  微软雅黑,加粗,字体大小9,字体颜色黑色,背景全填充,居中
     *  边框颜色为灰色
     * @param fillForegroundColor
     */
    public CellStyleProperties(short fillForegroundColor, boolean autoWrapText) {
        this("微软雅黑", HSSFFont.BOLDWEIGHT_BOLD, (short)9, HSSFColor.BLACK.index, fillForegroundColor,
            HSSFCellStyle.SOLID_FOREGROUND, autoWrapText,
            HSSFCellStyle.ALIGN_CENTER, HSSFCellStyle.VERTICAL_CENTER,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index,
            HSSFCellStyle.BORDER_THIN, HSSFColor.BLACK.index);
    }

}

2、ExcelWriter:导出方法

public class ExcelWriter {
	....//(属性等见本节1开头部分)
	public HorizontalExcelWriter setFirstHeader(List firstHeader) {
        firstHeader.sort(Comparator.comparing(HeaderProperties::getOrder));
        this.firstHeader = firstHeader;
        checkIfNewLineOfFirstHeader();
        return this;
    }
    
    /**
    *	检查第一行表头是否需要换行操作
    */
    private void checkIfNewLineOfFirstHeader() {
        for (HeaderProperties hp : firstHeader) {
            if (hp.getCellStyleProperties().autoWrapText) {
                isAutoNewLineOfFirstHeader = true;
                break;
            }
        }
    }
    
	public HorizontalExcelWriter setdataClass(Class clazz) {
	        this.dataClazz = clazz;
	        return this;
	 }
	  
	/**
     * @param contentList  实体对象集合
     */
    public  HorizontalExcelWriter(List contentList) {
        this.contentList = contentList;
    }

	public HorizontalExcelWriter build() throws POIException, IOException {
        version = Excel.EXCEL_2007;
        // 根据导出数据类获取其注解对应的表头信息,并进行第二行表头的创建
        titleAndPropertyList = this.readTitleAndPropertyList(dataClazz);
        // 根据titleAndPropertyList构建第二行的header
        getSecondHeader();
        return this;
    }	
    .....// 其他方法见下面
 }

3、ExcelWriter:获取导出表头信息(**)

/获取注解信息,创建第二行表头。**尤其注意后半部分,如何去寻找到对象属性中的属性信息。**
    private List readTitleAndPropertyList(Class clazz) {

        List titleAndPropertyList = new ArrayList<>();

        List fieldList = getFields(clazz);
        for (Field field : fieldList) {
            ColumnDesc columnDesc = field.getAnnotation(ColumnDesc.class);
            //没有注解的,不写入Excel
            if (columnDesc == null) {
                continue;
            }
            // 有注解有title
            if(StringUtils.isNotBlank(columnDesc.title()) ) {
                titleAndPropertyList.add(new 	 	  
                        TitleAndProperty(columnDesc.title(),field.getName()));
                continue;
            }
            //有注解但无title,即为默认的"",则为周数据,需要向下解析
            List weekFileds = getFields(field.getType());
             if (CollectionUtils.isEmpty(weekFileds)) {
                 continue;
             }
             for (Field weekFiled : weekFileds) {
                    ColumnDesc weekColumnDesc = 
                                weekFiled.getAnnotation(ColumnDesc.class);
                    //没有注解的,不写入Excel
                    if (weekColumnDesc == null) {
                        continue;
                    }
                   titleAndPropertyList.add(new TitleAndProperty(weekColumnDesc.title(),
                                "get" + field.getName().substring(0,1).toUpperCase()
                                 +  field.getName().substring(1) + "().get"  
                                 + weekFiled.getName().substring(0,1).toUpperCase() 
                                 + weekFiled.getName().substring(1)));
        	}
        }
       return titleAndPropertyList;
}

注意:在后半部分再进行了一次获取属性的反射操作,并为了获取到信息,将其组装成了“dto.getWeekData().getSales”这样的形式。
其中getFiled方法如下:

private List getFields(Class clazz) {
        Field[] fields = clazz.getDeclaredFields();

        //按照index排序
        return Arrays.stream(fields).sorted((field1,field2) -> {
            int order1 = 0;
            int order2 = 0;
            ColumnDesc columnDesc1 = field1.getAnnotation(ColumnDesc.class);
            if (columnDesc1 != null) {
                order1 = columnDesc1.index();
            }
            ColumnDesc columnDesc2 = field1.getAnnotation(ColumnDesc.class);
            if (columnDesc2 != null) {
                order2 = columnDesc2.index();
            }
            return order1 - order2;
        }).collect(Collectors.toList());
    }

所以getSecondHeader方法构建第二行header的方法实现如下:

private void getSecondHeader() {
        secondHeader = Lists.newArrayList();
        int index = 0;
        for(TitleAndProperty tp : titleAndPropertyList) {
            secondHeader.get(index).setTitle(tp.getTitle());
            index ++;
        }
    }

4、ExcelWriter:将导出数据对象写入excel

该方法就是主要的将导出对象中的值写入到excel对应的表头项下面。

/**
     * 集合数据写入Excel
     */
    public Workbook writeExcel() throws Exception {

        Workbook workbook = this.getWorkbook();
        Sheet sheet = workbook.createSheet();
        
        // 第一行表头
        Row header0 = sheet.createRow(0);
        // 第一行表头,若存在换行操作,则需将其高度变高,可以自行调整,改变1.6这个值就行
        if (isAutoNewLineOfFirstHeader) {
            header0.setHeight((short)(header0.getHeight() * 1.6));
        }
        if (this.firstHeader != null) {
            int columnIndex = 0;
            for(HeaderProperties headerPro : firstHeader) {
            	// 解析表头行并创建表头单元格,详细实现方法见后面
                columnIndex = 
                		solveHeader(workbook, sheet, header0, columnIndex, headerPro);
            }
        }
        // 第二行表头
        Row header1 = sheet.createRow(1);
        if (this.secondHeader != null) {
            int columnIndex = 0;
            for(HeaderProperties headerPro : secondHeader) {
            	// 解析表头行并创建表头单元格,详细实现方法见后面
                columnIndex = 
                	solveHeader(workbook, sheet, header1, columnIndex, headerPro);
            }
        }
        // 数据内容解析
        if (this.contentList != null) {
            for (int rowIndex = 0; rowIndex < this.contentList.size(); rowIndex++) {
                //数据样式:一行一样,轮换着来,ExcelUtils中定义了一些自己常用的数据样式等
                CellStyle bodyStyle = ExcelUtils.getStyle(workbook, 
                				ExcelUtils.dataStyle3);
                if (maxExportNum != -1 && rowIndex >= maxExportNum) {// 超出限制
                    break;
                }
                // 因为表头有两行,所以是+2,若为1行则是+1
                Row content = sheet.createRow(rowIndex + 2);
                int col = 0;
                T t = this.contentList.get(rowIndex);
                for (TitleAndProperty titleAndProperty : titleAndPropertyList) {
                    Object val = new Object();
                    // 这就是前面为啥要做.get的操作,是为了获取到对象所对应的那个值,方法在后面会贴着
                    if (titleAndProperty.property.contains("().get")) {
                        val = getWeekDataValue(titleAndProperty.property, t);
                    } else {
                        val = ObjectUtils.getProperties(t,titleAndProperty.property);
                    }
                    Cell cell = content.createCell(col);
                    setCellValue(cell,val);
                    cell.setCellStyle(bodyStyle);
                    col++;
                }
            }
        }
        return workbook;
    }

以下方法是如何解析表头的方法,并说明了如何进行换行操作,用到了cell.setCellValue(new XSSFRichTextString(text))方法,还有就是合并单元格的方法sheet.addMergedRegion

 private int solveHeader(Workbook workbook, Sheet sheet, Row header, int columnIndex,
        HeaderProperties headerPro) {
        // 先判断是否需要合并
        if (headerPro.isMerged()) {
            int index = columnIndex + headerPro.getMergeLength();
            int firstColumn = columnIndex;
            for(;columnIndex < index; columnIndex++) {
                Cell cell = header.createCell(columnIndex);
                if (columnIndex == firstColumn) {
                    String text = headerPro.getTitle();
                    // 该处是为了判断哪种情况下可以对表头进行换行,该处可以代替为自己的业务
                    if (isAutoNewLineOfFirstHeader && text.contains("(")) {
                        text = headerPro.getTitle().substring(0,headerPro.getTitle().indexOf("("))
                            + "\r\n" + headerPro.getTitle().substring(headerPro.getTitle().indexOf("("));
                    }
                    cell.setCellValue(new XSSFRichTextString(text));
                } else {
                    cell.setCellValue("");
                }
                cell.setCellStyle(headerPro.getCellStyleProperties() != null ?
                    ExcelUtils.getStyle(workbook, headerPro.getCellStyleProperties()):
                        ExcelUtils.getStyle(workbook, ExcelUtils.titleStyle1));
                // 列长度
                sheet.setColumnWidth(columnIndex,13 * 256);
            }
            // 合并单元格
            sheet.addMergedRegion(new CellRangeAddress(0, 0,
                columnIndex - headerPro.getMergeLength(), columnIndex - 1));
        } else {
            Cell cell = header.createCell(columnIndex);
            cell.setCellValue(headerPro.getTitle());
            cell.setCellStyle(headerPro.getCellStyleProperties() != null ?
                ExcelUtils.getStyle(workbook, headerPro.getCellStyleProperties()):
                ExcelUtils.getStyle(workbook, ExcelUtils.titleStyle1));
            // 列长度
            sheet.setColumnWidth(columnIndex++,13 * 256);
        }
        return columnIndex;
    }

以下是如何获取到对象属性的属性值的方法,用到了反射。
引用了工具类import org.springframework.util.ReflectionUtils

private Object getWeekDataValue(String fieldName, T obj)
        throws InvocationTargetException, IllegalAccessException {
        String fieldName1 = fieldName.split("\\(\\).")[0];
        String fieldName2 = fieldName.split("\\(\\).")[1];
        Method method = ReflectionUtils.findMethod(obj.getClass(), fieldName1);
        Object object = method.invoke(obj, new Object[0]);
        Method method1 = ReflectionUtils.findMethod(object.getClass(), fieldName2);
        Object val = method1.invoke(object, new Object[0]);
        return val;
    }

以下是ObjectUtils通过反射获取Javabean属性的值
ObjectUtils.getProperties

/用反射获得指定JAVABean属性的值
  public static Object getProperties(Object obj, String fieldName) throws Exception {
      PropertyDescriptor pd = new PropertyDescriptor(fieldName, obj.getClass());
      Method method = pd.getReadMethod();
      return method.invoke(obj);
  }

以下是excel根据不同数据类型进行转换

 /**
     * 不同的数据类型做转换,可以保证数值类型下载excel后能直接计算或求和等
     */
    private void setCellValue(Cell cell,Object val) {
        if (val instanceof Date) {
            cell.setCellValue(DateFormatUtils.format((Date) val,"yyyy-MM-dd HH:mm:ss"));
        } else if (val instanceof BigDecimal) {
            cell.setCellValue(val == null ? 0 : ((BigDecimal) val).doubleValue());
        } else if (val instanceof Double) {
            cell.setCellValue(val == null ? 0 : (Double) val);
        } else if (val instanceof Integer) {
            cell.setCellValue(val == null ? 0 : (Integer) val);
        } else if (val instanceof Long) {
            cell.setCellValue(val == null ? 0 : (Long) val);
        } else {
            cell.setCellValue(val == null ? "" : val.toString());
        }
    }

以下是ExcelUtils工具类,可以根据CellStyleProperties来构建样式,不管是背景、对齐、边框等都可以根据需要而设置。

@Log4j
@Data
public class ExcelUtils {

    /**
     * 方法描述 - 根据CellStyleProperties,构建样式
     */
    public static CellStyle getStyle(Workbook workbook, CellStyleProperties cellStyleProperties) {
        CellStyle style = workbook.createCellStyle();
        Font font = workbook.createFont();
        font.setBoldweight(cellStyleProperties.fontBoldweight);
        font.setFontName(cellStyleProperties.fontName);
        font.setColor(cellStyleProperties.fontColor);
        font.setFontHeightInPoints(cellStyleProperties.fontHeightInPoints);
        style.setFont(font);
        style.setWrapText(cellStyleProperties.autoWrapText);
        style.setFillForegroundColor(cellStyleProperties.fillForegroundColor);
        style.setFillPattern(cellStyleProperties.fillPattern);
        style.setAlignment(cellStyleProperties.alignCenter);// 水平对齐方式
        style.setVerticalAlignment(cellStyleProperties.crossCenter);//设置上下对齐方式
        style.setBorderBottom(cellStyleProperties.borderBottom);//下边框
        style.setBorderLeft(cellStyleProperties.borderLeft);//左边框
        style.setBorderRight(cellStyleProperties.borderRight);//右边框
        style.setBorderTop(cellStyleProperties.borderTop);//上边框
        style.setTopBorderColor(cellStyleProperties.borderTopColor);//上边框颜色
        style.setRightBorderColor(cellStyleProperties.borderRightColor);//右边框颜色
        style.setBottomBorderColor(cellStyleProperties.borderBottomColor);//下边框颜色
        style.setLeftBorderColor(cellStyleProperties.borderLeftColor);//左边框颜色
        return style;
    }

    /**
     * 判断是否是空行,empty rows,仅删除了内容,行依然存在
     *
     * @param row
     * @return
     */
    public static boolean isEmptyRow(Row row) {

        if (row == null) {
            return true;
        }
        if (row.getLastCellNum() <= 0) {
            return true;
        }
        for (int cellNum = row.getFirstCellNum(); cellNum < row.getLastCellNum(); cellNum++) {
            Cell cell = row.getCell(cellNum);
            if (cell != null && cell.getCellType() != Cell.CELL_TYPE_BLANK && StringUtils.isNotEmpty(cell.toString().trim())) {
                return false;
            }
        }
        return true;
    }

    //默认表头样式
    public static CellStyleProperties titleStyle = new CellStyleProperties("微软雅黑", (short) 10, (short) 14, HSSFColor.WHITE.index, HSSFColor.DARK_BLUE.index, HSSFCellStyle.SOLID_FOREGROUND);
    //默认数据样式
    public static CellStyleProperties dataStyle = new CellStyleProperties("微软雅黑", (short) 10, (short) 12, HSSFColor.BLACK.index, HSSFColor.WHITE.index, HSSFCellStyle.SOLID_FOREGROUND);

    //内置数据样式-1 2
    public static CellStyleProperties dataStyle1 = new CellStyleProperties("微软雅黑", (short) 10, (short) 9, HSSFColor.BLACK.index, HSSFColor.WHITE.index, HSSFCellStyle.SOLID_FOREGROUND);
    public static CellStyleProperties dataStyle2 = new CellStyleProperties("微软雅黑", (short) 10, (short) 9, HSSFColor.BLACK.index, HSSFColor.GREY_25_PERCENT.index, HSSFCellStyle.SOLID_FOREGROUND);
    public static CellStyleProperties dataStyle3 = new CellStyleProperties("微软雅黑", (short) 10, (short) 9, HSSFColor.BLACK.index, HSSFColor.WHITE.index, HSSFCellStyle.SOLID_FOREGROUND, false, HSSFCellStyle.ALIGN_LEFT);
    public static CellStyleProperties dataStyle4 = new CellStyleProperties("微软雅黑", (short) 10, (short) 9, HSSFColor.BLACK.index, HSSFColor.YELLOW.index, HSSFCellStyle.SOLID_FOREGROUND, false, HSSFCellStyle.ALIGN_LEFT);
    //内置表头样式
    public static CellStyleProperties titleStyle1 = new CellStyleProperties("微软雅黑", HSSFFont.BOLDWEIGHT_BOLD, (short) 9, HSSFColor.BLACK.index, HSSFColor.WHITE.index, HSSFCellStyle.SOLID_FOREGROUND, false, HSSFCellStyle.ALIGN_LEFT);
    public static CellStyleProperties titleStyle2 = new CellStyleProperties("微软雅黑", HSSFFont.BOLDWEIGHT_BOLD, (short) 9, HSSFColor.BLACK.index, HSSFColor.YELLOW.index, HSSFCellStyle.SOLID_FOREGROUND, false, HSSFCellStyle.ALIGN_LEFT);
    public static CellStyleProperties titleStyle3 = new CellStyleProperties("微软雅黑", HSSFFont.BOLDWEIGHT_BOLD, (short) 9, HSSFColor.BLACK.index, HSSFColor.LIGHT_ORANGE.index, HSSFCellStyle.SOLID_FOREGROUND, false, HSSFCellStyle.ALIGN_LEFT);
    public static CellStyleProperties titleStyle4 = new CellStyleProperties("微软雅黑", HSSFFont.BOLDWEIGHT_BOLD, (short) 9, HSSFColor.BLACK.index, HSSFColor.LIGHT_GREEN.index, HSSFCellStyle.SOLID_FOREGROUND, false, HSSFCellStyle.ALIGN_LEFT);
    public static CellStyleProperties titleStyle5 = new CellStyleProperties("微软雅黑", HSSFFont.BOLDWEIGHT_BOLD, (short) 9, HSSFColor.BLACK.index, HSSFColor.WHITE.index, HSSFCellStyle.SOLID_FOREGROUND, true, HSSFCellStyle.ALIGN_LEFT);
    public static CellStyleProperties titleStyle6 = new CellStyleProperties("微软雅黑", HSSFFont.BOLDWEIGHT_BOLD, (short) 9, HSSFColor.BLACK.index, HSSFColor.YELLOW.index, HSSFCellStyle.SOLID_FOREGROUND, true, HSSFCellStyle.ALIGN_LEFT);
    public static CellStyleProperties titleStyle7 = new CellStyleProperties("微软雅黑", HSSFFont.BOLDWEIGHT_BOLD, (short) 9, HSSFColor.BLACK.index, HSSFColor.LIGHT_ORANGE.index, HSSFCellStyle.SOLID_FOREGROUND, true, HSSFCellStyle.ALIGN_LEFT);
    public static CellStyleProperties titleStyle8 = new CellStyleProperties("微软雅黑", HSSFFont.BOLDWEIGHT_BOLD, (short) 9, HSSFColor.BLACK.index, HSSFColor.LIGHT_GREEN.index, HSSFCellStyle.SOLID_FOREGROUND, true, HSSFCellStyle.ALIGN_LEFT);

}

顺便再贴一张之前看到的一个简单的颜色图谱:
Excel导出与上传实现,包含单元格样式,合并单元格,对象属性的属性值获取等_第1张图片

5、ExcelWriter导出举例

该部分展示如何使用该下载工具类来实现。

List list = Lists.newArrayList(data);
ExcelWriter work = new ExcelWriter<>(list).setdataClass(HorizontalData.class)             .setFirstHeader(converter.getFirstHeader()).build();
Workbook workbook = work.writeExcel();
ExportUtils.writeOutPutStream(workbook, request, response);

以上就是整个ExcelWriter导出工具类的所有内容。最核心的一点就是根据对象属性获取到对象属性内的值吧,看似简单,但一开始做的时候,脑筋转不过弯,还走了很多弯路呢。
给看一个效果图吧:
Excel导出与上传实现,包含单元格样式,合并单元格,对象属性的属性值获取等_第2张图片

四、上传工具类ExcelReader

上传工具类的主要难点在于,如何找到对象属性列,并进行属性赋值。
话不多说,先看工具类的基本属性组成。

@Getter
public class ExcelReader {
    /**
     * workbook
     */
    private Workbook wookbook;
    /**
     * Excel版本
     */
    private Excel version;
    /**
     * 对象class
     */
    private Class clazz;
    /**
     *表头
     */
    private String[] headers;
    /**
     * 第几个sheet
     */
    private int sheet = 0;
    /**
     * 最大上传限制,默认1000
     */
    private int maxUploadSum = 1000;
    /**
     * 根据上传文件和相应对象转换而来的属性列
     * 用来获取到对应的数据值
     */
    private String[] properties;
	/**
	* 上传路径名,是为了得到返回文件的路径,可以返回上传的错误信息
	*/
    private String uploadFileFullName;
    
	public void setProperties(String[] properties) {
	        this.properties = properties;
	    }
	
	/**
     * 修改sheet号,从0开始
     */
    public ExcelReader sheet(int sheetNo) throws POIException {
        if (sheetNo < 0) {
            throw new POIException("sheetNo必须不小0");
        }
        if (sheetNo > this.wookbook.getNumberOfSheets() - 1) {
            log.warn("Out of the number of sheets! check the sheet parameter" 
            	+ sheet);
            throw new POIException("Excel错误,
            	文件中第[%s]个sheet文件不存在。",sheetNo);
        }
        this.sheet = sheetNo;
        return this;
    }

    /**
     * 最大上传数量,默认1000
     */
    public ExcelReader maxUploadSum(int num) throws POIException {
        if (num <= 0) {
            throw new POIException("最大上传数量必须是个大于0的整数。");
        }
        this.maxUploadSum = num;
        return this;
    }
}

1、上传文件初始化ExcelReader

public  ExcelReader(File file, Class clazz) throws IOException, POIException {
        if (!FileUtils.isExists(file)) {
            log.warn("File not exists");
            throw new POIException("对不起,上传的文件不存在");
        }
        uploadFileFullName = file.getAbsolutePath();
        FileInputStream in = null;
        try {
            in = new FileInputStream(file);
            if (file.getName().endsWith(".xls")) {
                version = Excel.EXCEL_2003;
                this.wookbook = new HSSFWorkbook(in);
            } else if (file.getName().endsWith(".xlsx")) {
                version = Excel.EXCEL_2007;
                this.wookbook = new XSSFWorkbook(in);
            } else {
                log.warn("Unsupported file type, only 'xls' or '.xlsx' allowed ");
                throw new POIException("不支持的文件后缀,
                		仅支持上传.xls或.xlsx结尾的Excel文件。");
            }
        } finally {
            if (in != null) {
                in.close();
            }
        }
        this.clazz = clazz;
        // 该方法是为了解析header行
        this.headers = this.getColumnNameList().toArray(new String[0]);
    }

解析header行方法如下getColumnNameList
由于下载的表格中是有两行header,且第二行才是有效的header行,因此在上传时也要根据该特点来解析。如果只有一行header,就只需要根据只有一行的状态来进行解析。
第二行表头的格式是:第一行对应表头_第二行表头

 /**
     * 得到sheet中的header
     */
    private List getColumnNameList() {
        List result = Lists.newArrayList();
        
        List ls1 = Lists.newArrayList();
        List ls2 = Lists.newArrayList();
        if (wookbook.getSheetAt(this.sheet) == null) {
            return result;
        }
        if (wookbook.getSheetAt(this.sheet).getRow(0) == null) {
            return result;
        }
        if (wookbook.getSheetAt(this.sheet).getRow(1) == null) {
            return result;
        }
        Sheet sheet = wookbook.getSheetAt(this.sheet);
        Row row = sheet.getRow(0);// 第一行
        for (int j = 0; row.getCell(j) != null ; j++) { 
        // 第一行每列不可以为null,但可以为""
            Cell cell = row.getCell(j);
            if (isMergedRegion(sheet, 0, j)) {
                ls1.add(getMergedRegionValue(sheet,0, j));
            } else {
                ls1.add(cell.toString().trim());
            }
        }
        //  对于横版上传而言,有效行为第二行
        Row row1 = sheet.getRow(1);// 第二行
        for (int j = 0; row1.getCell(j) != null && isNotBlank(row1.getCell(j).toString()); j++) {
            Cell cell = row1.getCell(j);
            if (isMergedRegion(sheet, 1, j)) {
                ls2.add(getMergedRegionValue(sheet,1, j));
            } else {
                ls2.add(cell.toString().trim());
            }
        }
        // 第一行应与第二行数量保持一致
        if (ls1.size() != ls2.size()) {
            return  result;
        }
        for (int i = 0; i < ls1.size(); i++) {
            // 周数据,叠加在第二行上,以下划线分开
            if (ls1.get(i).contains("(")) {
                result.add(ls1.get(i) + "_" + ls2.get(i));
            } else {
                result.add(ls2.get(i));
            }
        }
        return result;
    }

2、ExcelReader上传文件解析

该方法比较重要的点在于,如何去获取对象属性的属性信息且赋值,用到了反射的setter方法

 /**
     * 获取Excel文件数据
     */
    public List readExcel() throws Exception {
        List ls = new ArrayList();

        Sheet sheet = wookbook.getSheetAt(this.sheet);
        if (sheet == null) {
            return ls;
        } else {
            if (properties.length <= 0) {
                return ls;
            }
            for (int i = 2; sheet.getRow(i) != null && i <= maxUploadSum; i++) {
                Row row = sheet.getRow(i);
                // if the row is empty, skip this row
                if (this.isEmptyRow(row)) {
                    continue;
                }
                T t = (T)Class.forName(clazz.getCanonicalName()).newInstance();
                int col = 0;
                Object methodClass = new Object();
                Set methodNameSet = Sets.newHashSet();
                for (String property : properties) {
                    if (property.contains("_")) {
                        String methodName1 = property.split("_")[0];
                        String methodName2 = property.split("_")[1];
                        Method method1 = getSetterMethod(t.getClass(), methodName1);
                        if (!methodNameSet.contains(methodName1)) {
                            methodClass = Class.forName(method1.getParameterTypes()[0].getName()).newInstance();
                            methodNameSet.add(methodName1);
                        }
                        Method method2 = getSetterMethod(methodClass.getClass(), methodName2);
                        Class< ? >[] typeClasses = method2.getParameterTypes();
                        if (typeClasses.length == 1) {
                            Cell cell = row.getCell(col);
                            // setter 参数
                            Object[] params = this.getParams(typeClasses[0].getName(), cell);
                            if (params != null && params.length > 0) {
                                method2.invoke(methodClass, params);
                                method1.invoke(t, methodClass);
                            }
                        }
                    } else {
                        Method method = getSetterMethod(t.getClass(), property);
                        Class< ? >[] typeClasses = method.getParameterTypes();
                        if (typeClasses.length == 1) {
                            Cell cell = row.getCell(col);
                            // setter 参数
                            Object[] params = this.getParams(typeClasses[0].getName(), cell);
                            if (params != null && params.length > 0) {
                                method.invoke(t, params);
                            }
                        }
                    }
                    col++;
                }
                ls.add(t);
            }
            return ls;
        }
    }

以下为判断读取到的行是否为空的方法:

/**
     * 判断是否是空行,empty rows,仅删除了内容,行依然存在
     */
    private boolean isEmptyRow(Row row) {
        if (row == null) {
            return true;
        }
        if (row.getLastCellNum() <= 0) {
            return true;
        }
        for (int cellNum = row.getFirstCellNum(); cellNum < row.getLastCellNum(); cellNum++) {
            Cell cell = row.getCell(cellNum);
            if (cell != null && cell.getCellType() != Cell.CELL_TYPE_BLANK && isNotBlank(cell.toString())) {
                return false;
            }
        }
        return true;
    }

非常重要!!!得到setter方法,可以进行set值,不能用导出的那种findMethod方式去进行。

private Method getSetterMethod(Class t, String property) throws SecurityException, NoSuchFieldException,NoSuchMethodException {
        Type type = t.getDeclaredField(property).getGenericType();
        return t.getMethod(this.getPropertyMethodName(type, property, true),
            t.getDeclaredField(property).getType());
    }

    /**
     * 得到属性GET,SET方法
     */
    private String getPropertyMethodName(Type type, String propertyName, boolean isSet) {
        StringBuilder sb = new StringBuilder();
        if (!isSet) {
            // 判断是否是布尔类型
            if ("boolean".equals(type.toString())) {
                if (propertyName.indexOf("is") > 0) {
                    sb.append("is");
                }
                sb.append(propertyName);
                return sb.toString();
            }
            sb.append("get");
        } else {
            if (propertyName.indexOf("is") == 0 && "boolean".equals(type.toString())) {
                propertyName = propertyName.substring(2);
            }
            sb.append("set");
        }
        sb.append(propertyName.replaceFirst(propertyName.substring(0, 1),
            propertyName.substring(0, 1).toUpperCase()));
        return sb.toString();
    }

以下方法是获取参数类型并赋值:

 /**
     * 根据参数类型,获得参数对象数组
     */
    private Object[] getParams(String className, Cell cell) throws ParseException {

        String cellValue = this.getRightCellValue(cell);
        if (isBlank(cellValue)) {
            return new Object[] {};
        }
        if (className.equals("java.lang.String")) {
            return new Object[] { cellValue };
        } else if (className.equals("java.lang.Number")) {
            return new Object[] { NumberFormat.getInstance().parse(cellValue) };
        } else if (className.equals("char") || className.equals("java.lang.Character")) {
            return new Object[] { cellValue.toCharArray()[0] };
        } else if (className.equals("int") || className.equals("java.lang.Integer")) {
            return new Object[] { new Double(cellValue).intValue() };
        } else if (className.equals("byte") || className.equals("java.lang.Byte")) {
            return new Object[] { new Double(cellValue).byteValue() };
        } else if (className.equals("short") || className.equals("java.lang.Short")) {
            return new Object[] { new Double(cellValue).shortValue() };
        } else if (className.equals("float") || className.equals("java.lang.Float")) {
            return new Object[] { new Double(cellValue).shortValue() };
        } else if (className.equals("double") || className.equals("java.lang.Double")) {
            return new Object[] { new Double(cellValue) };
        } else if (className.equals("long") || className.equals("java.lang.Long")) {
            return new Object[] { new Double(cellValue).longValue() };
        } else if (className.equals("boolean") || className.equals("java.lang.Boolean")) {
            return new Object[] { Boolean.valueOf(cellValue) };
        } else if (className.equals("java.util.Date")) {
            return new Object[] { new Date(cell.getDateCellValue().getTime()) };
        } else {
            return new Object[] {};
        }
    }
/**
     * 处理特殊格式的数据,公式则保存公式统计的结果
     */
    public String getRightCellValue(Cell cell) {
        if (cell == null) {
            return "";
        }
        String cellValue = cell.toString();
        if (Cell.CELL_TYPE_FORMULA == cell.getCellType()) {
            cellValue = String.valueOf(getFormulaResult(cell));
        }
        return cellValue.trim();
    }
    
        /**
     * 返回公式计算的结果
     */
    private Object getFormulaResult(Cell cell) {
        FormulaEvaluator evaluator = wookbook.getCreationHelper().createFormulaEvaluator();
        CellValue cellValue = evaluator.evaluate(cell);
        switch (cellValue.getCellType()) {
            case Cell.CELL_TYPE_BOOLEAN:
                return String.valueOf(cellValue.getBooleanValue());
            case Cell.CELL_TYPE_NUMERIC:
                return String.valueOf(cellValue.getNumberValue());
            case Cell.CELL_TYPE_STRING:
                return cellValue.getStringValue();
            default:
        }
        return "";
    }

3、ExcelReader上传返回错误信息文件实现

对于上传来说,一般如果出现了错误信息,是需要进行文件返回的。
该返回文件是新建了sheet2进行写入的,也可以在当前sheet中的最后列中进行写入信息。

、/**
     *
     * 读取Excel操作后,在sheet2内注明信息
     * @param columnHeader
     *            列头名称,自定义列头名称
     * @param columnContents
     *            数据内容,K-V形式,K是列号,从1开始,V是列值。
     * @param excelPrefix
     *            用于定义返回的文件名称前缀,例如:上传更新错误信息_xxxx.xls
     * @return
     * @throws IOException
     */
    public File appendExcelNewSheetColumn(String columnHeader, Map columnContents,
        String excelPrefix) throws IOException {
        // 直接返回原文件
        if (null == columnContents || columnContents.isEmpty()) {
            return new File(uploadFileFullName);
        }
        Workbook wb = null;
        String suffix = ".xlsx";
        FileInputStream in = null;
        try {
            in = new FileInputStream(uploadFileFullName);
            switch (version) {
                case EXCEL_2003:
                    suffix = ".xls";
                    wb = new HSSFWorkbook(in);
                    break;
                case EXCEL_2007:
                    suffix = ".xlsx";
                    wb = new XSSFWorkbook(in);
                    break;
                default: // never happen
                    break;
            }
        } finally {
            if (in != null) {
                in.close();
            }
        }
        if (wb == null) {
            return null;
        }
        Sheet sheet = wb.createSheet();
        wb.setSheetName(wb.getSheetIndex(sheet),"上传错误信息");
        Row headerRow = sheet.getRow(0);
        headerRow = null != headerRow ? headerRow : sheet.createRow(0);
        String[] columns = columnHeader.split(",");
        // 表头
        for (int i = 0; i< columns.length; i++) {
            Cell headerCell = headerRow.createCell(i);
            headerCell.setCellStyle(ExcelUtils.getStyle(wb, ExcelUtils.titleStyle3));
            headerCell.setCellValue(columns[i]);
            // 最后一列为结果信息,因此要宽一些
            if (i < columns.length -1) {
                sheet.setColumnWidth(i, 9 * 256);
            } else {
                sheet.setColumnWidth(i, 80 * 256);
            }
        }
        // 体
        for (int i = 1 ; i <= columnContents.size(); i++) {
            String val = columnContents.get(i);
            if (isNotBlank(val)) {
                String[] infos = val.split("_");
                Row row = sheet.createRow(i);
                for (int j = 0; j < infos.length; j++) {
                    Cell cell = row.createCell(j);
                    if (j 

4、ExcelReader上传举例

如下所示,为该方法的上传示例。
其中destColumn是根据上传文件解析出来的表头,要和实际的导出类相关联,获取到导出对象的数据集。因为reader.getHeaders()是得到导出类的所有表头信息,而上传表头可能存在一些被删除的无效项。
msgMap为返回的错误信息集合。如果destColumn实际表头获取没问题,就可以进行取值等后续处理了。

ExcelReader reader = new ExcelReader(file, HorizontalData.class);
String[]   destColumn = reBuildHorizontalProperties(reader.getHeaders(), msgMap);
 if (msgMap.isEmpty()) {
      reader.setProperties(destColumn);
      List cel = reader.readExcel();
      ...
	//若存在错误信息返回,则多一行,其中第一个字段是在excel中类似表头的信息
	 returnFile =reader.appendExcelNewSheetColumn("上传结果", 
	 	msgMap, "上传返回结果");
 }

以上就是ExcelReader的全部内容。

五 总结

上传下载一般都有现成的工具类,但有时候这些工具类并不能很好地去贴合我们的业务场景,因此需要对其进行适当的改造。本文就是基于该目的进行改造的上传下载工具类。

你可能感兴趣的:(Java开发经验总结,JAVA个人学习经验)