基于注解的Excel导出工具

谭梓
2023-12-01

前言

大家工作中,往往会遇到Excel导出的问题,但是写导出数据的时候,需要构造很多不需要的参数,如Title,列名,还有数据列表。不知道小伙伴们有没有感觉很繁琐,小编自己觉得很繁琐,所以想有一个能够不用 构造这么多东西去写Excel导出,能不能直接给我传递一个集合,我就能直接给导出呢?
针对这个小编也是想了很久终于找到了一套方案,最近也是写了一个初级版本。这里分享给大家,希望能够对那些觉得Excel导出操作繁琐的小伙伴有所帮助。

Excel导出理解

POI简介

Jakarta POI 是一套用于访问微软格式文档的Java API。Jakarta POI有很多组件组成,其中有用于操作Excel格式文件的HSSF和用于操作Word的HWPF,在各种组件中目前只有用于操作Excel的HSSF相对成熟。

官方主页http://poi.apache.org/index.html,

API文档http://poi.apache.org/apidocs/index.html

我的功能是使用的POI来完成的Excel导出的,所以这个依赖包是必须有的。

Jakarta POI HSSF API组件

HSSF(用于操作Excel的组件)提供给用户使用的对象在rg.apache.poi.hssf.usermodel包中,主要部分包括Excel对象,样式和格式,有以下几种常用的对象:

  • HSSFWorkbook excel的文档对象 -相当于Excel文件
  • HSSFSheet excel的表单 -相当于Excel中的Sheet页
  • HSSFRow excel的行 -相当于Excel中的Sheet页中的行
  • HSSFCell excel的格子单元 -相当于Excel中的Sheet页中的列
  • HSSFFont excel字体 -这个是设置Excel中字体用的。
  • HSSFCellStyle cell样式 -这个是设置Excel中样式用的。

基本操作步骤

基本操作其实和我们写一个Excel一样的。

  1. 创建Excel文件:HSSFWorkbook打开或者创建“Excel文件对象”
  2. 创建Sheet页:用HSSFWorkbook对象返回或者创建Sheet对象
  3. 在Sheet页中每列的数据:用Sheet对象返回行对象,用行对象得到Cell对象,往对应的Cell对象中写数据。

这就是我们生成一个Excel的基本操作。

网上常出现的导出实例

工具类代码:

public class ExcelUtil {

    /**
     * 导出Excel
     * @param sheetName sheet名称
     * @param title 标题
     * @param values 内容
     * @param wb HSSFWorkbook对象
     * @return
     */
    public static HSSFWorkbook getHSSFWorkbook(String sheetName,String []title,String [][]values, HSSFWorkbook wb){

        // 第一步,创建一个HSSFWorkbook,对应一个Excel文件
        if(wb == null){
            wb = new HSSFWorkbook();
        }

        // 第二步,在workbook中添加一个sheet,对应Excel文件中的sheet
        HSSFSheet sheet = wb.createSheet(sheetName);

        // 第三步,在sheet中添加表头第0行,注意老版本poi对Excel的行数列数有限制
        HSSFRow row = sheet.createRow(0);

        // 第四步,创建单元格,并设置值表头 设置表头居中
        HSSFCellStyle style = wb.createCellStyle();
        style.setAlignment(HSSFCellStyle.ALIGN_CENTER); // 创建一个居中格式

        //声明列对象
        HSSFCell cell = null;

        //创建标题
        for(int i=0;i<title.length;i++){
            cell = row.createCell(i);
            cell.setCellValue(title[i]);
            cell.setCellStyle(style);
        }

        //创建内容
        for(int i=0;i<values.length;i++){
            row = sheet.createRow(i + 1);
            for(int j=0;j<values[i].length;j++){
                //将内容按顺序赋给对应的列对象
                row.createCell(j).setCellValue(values[i][j]);
            }
        }
        return wb;
    }
}

我们今天说的不是这个,而是在这个基本操作上再次进行封装。得到一个操作简便的ExcelUtil。

基于注解的Excel

使用案例

Controller:

@GetMapping("uploadSwopScoreStatement")
    public void uploadSwopScoreStatement(HttpServletResponse response, @NotNull Long userId,
                                         Date startTime, Date endTime) {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");
        String dateStr = formatter.format(new Date());
        //excel文件名
        String fileName = "积分流动报表_" + dateStr + ".xls";
        //调用Excel实现得到Excel
        HSSFWorkbook wb = statementService.swopScoreStatement(userId, startTime, endTime);

        //把文件输出流放到Response中
        try {
            this.setResponseHeader(response, fileName);
            OutputStream os = response.getOutputStream();
            wb.write(os);
            os.flush();
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Service实现:

public HSSFWorkbook swopScoreStatement(Long userId, Date startTime, Date endTime) {
        List<ObjId> objIds = sysUserMapper.getIdObj(userId);
        List<Long> userIds = powerControlService.getIdsByTree(objIds);
        userIds.add(userId);//添加自己的用户id
        //获取数据列表
        List<TradeFlowExcel> statement = orderMapper.selectAndTradeFlowDetail(userIds,startTime,endTime);
        //生成Excel对象(根据数据列表生成Excel)
        return ExcelGenerator.genExcel(statement);
    }

大家应该看到了,这些操作很简单但是怎么实现的呢 ?带大家看一下。

entity:

@Data
@Sheet(name = "积分变动概况统计") //给Sheet起名字
@Title(value = "积分变动概况统计") //给导出的列表起一个Title
public class TradeFlowExcel {

    private Long userId;

    @Cell("用户名") //列名
    private String nickName;

    @Cell("用户账号")
    private String account;

    @Cell("流水变动情况")
    private String tradeAmount;

    @Sheet(name = "用户【{}】明细流水",field = "account") //级联查询的话,可以使用这个一口气生成多个Sheet。
    List<TradeFlowDetailExcel> tradeFlowDetailExcels;

    private Date startTime;

    private Date endTime;
}

@Data
@Title(value = "积分流水明细")
public class TradeFlowDetailExcel {

    @Cell("用户名")
    private String nickName;

    @Cell("用户账号")
    private String account;

    @Cell("流水类型")
    private String busiType;

    @Cell("收支类型")
    private String tradeType;

    @Cell("交易金额")
    private String tradeAmount;

}

这样导出就 直接有列和Title还有Sheet啦。

下面我们看一下代码实现。

使用到的注解

此工具类 是基于反射和注解来实现的。
用到的注解分别有@Cell、@Title、@Sheet 后期还会加入@Font、@Style 控制字体和控制样式。

目前还没有Group分组,Group分组应该会在第二个版本出。

@Cell:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cell {

    /**
     * 列名(表头)
     * @return
     */
    String value() default "";
}

@Sheet:

@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Sheet {

    String name() default "sheet";

    String [] value() default {};

    String [] field() default {};

}

@Title:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Title {

    /**
     * Excel表明
     * @return
     */
    String value() default "数据列表";
}

使用的工具类

工具类:

public class ExcelGenerator<T> {

	/**
	 * 生成Excel文件对象: 1.创建一个HSSFWorkbook,对应一个Excel文件
	 * 2.第二步,在workbook中添加一个sheet,对应Excel文件中的sheet 3.在sheet中添加表头第0行,创建单元格,并设置值表头
	 * 设置表头居中,注意老版本poi对Excel的行数列数有限制 4.插入数据
	 * 
	 * @param data
	 *            需要生成Excel的数据列表
	 * @param <T>
	 * @return
	 */
	public static <T> HSSFWorkbook genExcel(List<T> data) {
		// 第一步,创建一个HSSFWorkbook,对应一个Excel文件
		HSSFWorkbook wb = new HSSFWorkbook();
		// 第二步,在workbook中添加一个sheet,对应Excel文件中的sheet
		Map<HSSFSheet, List> sheetMap = genSheet(wb, data);
		for (HSSFSheet sheet : sheetMap.keySet()) {
			// 第三步,在sheet中添加表头第0行,注意老版本poi对Excel的行数列数有限制
			genTitle(sheet, sheetMap.get(sheet), wb);
			// 第四步,插入数据
			insertDate(sheet, sheetMap.get(sheet));
		}
		return wb;
	}

	/**
	 * 生成Sheet
	 * 
	 * @param wb
	 * @param datas
	 * @param <T>
	 */
	private static <T> Map<HSSFSheet, List> genSheet(HSSFWorkbook wb, List<T> datas) {
		Map<HSSFSheet, List> res = new HashMap<>();// 结果集合
		// 遍历数据列表解析data中的Sheet注解,并获取名称和对应的data列表
		if (datas == null || datas.size() == 0) {
			throw new RuntimeException("没有数据,无法导出Excel");
		}
		Object data = datas.get(0);
		Class clazz = data.getClass();
		Sheet sheetAnnotation = (Sheet) clazz.getAnnotation(Sheet.class);
		String sheetName;
		SheetAnnotationResolver<Sheet> sheetAnnotationResolver = new SheetAnnotationResolver();
		if (sheetAnnotation == null) {
			sheetName = "";
		} else {
			// 获取SheetName数据
			sheetName = (String) sheetAnnotationResolver.resolver(sheetAnnotation);

		}
		HSSFSheet sheetParent = wb.createSheet(sheetName);
		res.put(sheetParent, datas);
		// 获取获取属性 @Sheet 注解,获取并解析
		// 获取所有的field
		Field[] fields = clazz.getDeclaredFields();
		int index = 0;
		for (Field field : fields) {// 遍历data里面的所有字段,判断 数据模型中是否有 其他列表
			Sheet annotation = field.getAnnotation(Sheet.class);
			if (annotation != null) {
				for (T itme : datas) {// datas中的有多少条数据就有多少个sheet
					// 解析sheetName
					String sheetItemName = (String) sheetAnnotationResolver.resolver(annotation, itme);
					HSSFSheet sheet = null;
					// 创建Sheet
					if (sheetItemName != null && !sheetItemName.equals("")) {
						sheetItemName = sheetItemName + "_" + index;
						sheet = wb.createSheet(sheetItemName);
					} else {
						sheet = wb.createSheet();
					}
					// 初始化返回数据
					Method method;
					List itmeData = null;
					try {
						if (field.getType().equals("boolean")) {// 基本变量
							method = clazz.getMethod(getBooleanPrefix(field.getName()));
						} else {
							method = clazz.getMethod("get" + getMethodName(field.getName()));
						}
						itmeData = (List) method.invoke(data);
					} catch (NoSuchMethodException e) {
						e.printStackTrace();
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					} catch (InvocationTargetException e) {
						e.printStackTrace();
					}
					res.put(sheet, itmeData);
				}
				index++;
			}
		}
		return res;
	}

	/**
	 * 根据sheet来创建表头
	 * 
	 * @param sheet
	 * @param data
	 * @param <T>
	 * @return
	 */
	private static <T> void genTitle(HSSFSheet sheet, List<?> data, HSSFWorkbook wb) {
		// 创建一行,放表头
		if (data == null || data.size() == 0) {
			return;
		}
		// 初始化表头
		Class clazz = data.get(0).getClass();
		// 在sheet中添加表头第0行,注意老版本poi对Excel的行数列数有限制
		HSSFRow row = sheet.createRow(1);
		// 创建单元格,并设置值表头 设置表头居中
		HSSFCellStyle style = wb.createCellStyle();
		Field[] fields = clazz.getDeclaredFields();
		style.setAlignment(HSSFCellStyle.ALIGN_CENTER); // 创建一个居中格式
		// 声明列对象
		HSSFCell cell = null;
		int index = 0;
		// 遍历属性创建表头
		for (Field field : fields) {
			Cell annotation = field.getAnnotation(Cell.class);
			if (annotation != null) {// 设置了表头
				cell = row.createCell(index);
				cell.setCellValue(annotation.value());
				cell.setCellStyle(style);
				index++;
			}
		}
		// 初始化title
		row = sheet.createRow(0);
		for (int i = 0; i < index; i++) {
			row.createCell(i);
		}
		sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, index - 1));
		Title titleAnnotation = (Title) clazz.getAnnotation(Title.class);
		String title = titleAnnotation.value();
		HSSFCell cell1 = row.getCell(0);
		cell1.setCellValue(title);
		cell1.setCellStyle(style);
	}

	/**
	 * 插入表格数据
	 * 
	 * @param sheet
	 * @param data
	 */
	private static void insertDate(HSSFSheet sheet, List data) {
		if (data == null || data.size() == 0) {
			return;
		}
		int index = 1;
		for (Object obj : data) {
			HSSFRow row = sheet.createRow(index);
			Class clazz = data.get(0).getClass();
			Field[] fields = clazz.getDeclaredFields();
			int fieldIndex = 0;
			for (Field field : fields) {
				Cell cell = field.getAnnotation(Cell.class);
				Object value = null;
				if (cell != null) {// 需要填充数据
					Method method;
					try {
						if (field.getType().equals("boolean")) {// 基本变量
							method = clazz.getMethod(getBooleanPrefix(field.getName()));
						} else {
							method = clazz.getMethod("get" + getMethodName(field.getName()));
						}
						value = method.invoke(obj);
					} catch (NoSuchMethodException e) {
						e.printStackTrace();
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					} catch (InvocationTargetException e) {
						e.printStackTrace();
					}
					HSSFCell tabCell = row.createCell(fieldIndex);

					if (value != null) {
						if (!(value instanceof Boolean) && !(value instanceof Date)) {
							String val = value.toString();
							tabCell.setCellValue(val);
						} else if (value instanceof Boolean) {
							tabCell.setCellValue((Boolean) value);
						} else if (value instanceof Date) {
							tabCell.setCellValue((Date) value);
						}
					}
					fieldIndex++;
				}
			}
			index++;
		}
	}

	public static String getBooleanPrefix(String fieldName) {
		String prefix = fieldName.substring(0, 2);
		System.out.println(prefix);
		if (prefix.equals("is")) {
			return fieldName;
		} else {
			return "is" + getMethodName(fieldName);
		}
	}

	/**
	 * 获取属性的首字母大写
	 * 
	 * @param fildeName
	 * @return
	 */
	public static final String getMethodName(String fildeName) {
		byte[] items = fildeName.getBytes();
		items[0] = (byte) ((char) items[0] - 'a' + 'A');
		return new String(items);
	}

注解解析器

接口:

public interface AnnotationResolver <T extends Annotation> {


    /**
     * 注解解析
     * @param annotation
     * @return
     */
    Object resolver(T annotation);

    Object resolver(T annotation,Object taget);

}

解析@Sheet 注解的注解解析器

public class SheetAnnotationResolver <T extends Annotation> implements AnnotationResolver <T> {
    @Override
    public Object resolver(T annotation) {
        Sheet sheetAnnotation = (Sheet)annotation;
        //获取 Sheet字符串
        StringBuffer stringBuffer = new StringBuffer(sheetAnnotation.name());
        //字符串占位符替换
        if (sheetAnnotation.value() != null) {
            String[] values = sheetAnnotation.value();
            for (int i = 0; i < values.length; i++) {
                if (stringBuffer.indexOf("{", 0) != -1) {
                    stringBuffer.replace(stringBuffer.indexOf("{", 0), stringBuffer.indexOf("}", 0)+1, values[i]);
                }
            }

        }
        return stringBuffer.toString();
    }

    @Override
    public Object resolver(T annotation, Object taget) {
        Sheet sheetAnnotation = (Sheet)annotation;
        //获取 Sheet字符串
        StringBuffer stringBuffer = new StringBuffer(sheetAnnotation.name());
        //字符串占位符替换
        if (sheetAnnotation.value() != null) {
            Class clazz = taget.getClass();
            Field[] fields = clazz.getDeclaredFields();
            for (Field field :
                    fields) {
                String [] fieldStrs = sheetAnnotation.field();
                for (int i = 0; i < fieldStrs.length; i++) {
                    if (field.getName().equals(fieldStrs[i])) {//是拼接该字段
                        Method method = null;
                        Object val =null;
                        try {
                            if (field.getType().equals("boolean")) {// 基本变量
                                method = clazz.getMethod(ExcelGenerator.getBooleanPrefix(field.getName()));
                            } else {
                                method = clazz.getMethod("get" + ExcelGenerator.getMethodName(field.getName()));
                            }
                            val = method.invoke(taget);
                        } catch (NoSuchMethodException e) {
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }
                        if (val != null ) {//将注解配置中的字段和
                            if (stringBuffer.indexOf("{", 0) != -1) {
                                stringBuffer.replace(stringBuffer.indexOf("{", 0), stringBuffer.indexOf("}", 0)+1, val.toString());
                            }
                        }

                    }
                }

            }
        }
        return stringBuffer.toString();
    }


    public static void main(String[] args) {
        String[] values = new String[]{"user","name","age"};
        StringBuffer stringBuffer = new StringBuffer("{}qwe;");
        for (int i = 0; i < values.length; i++) {
            System.out.println(stringBuffer.indexOf("{", 0));
            if (stringBuffer.indexOf("{", 0) != -1) {
                stringBuffer.replace(stringBuffer.indexOf("{", 0), stringBuffer.indexOf("}", 1)+1, values[i]);
            }

        }
        System.out.println(stringBuffer.toString());
    }


}

简单的注解解析,就放在工具类里面直接使用了。

这个代码就算是全部都交代清楚了。
原理就是使用反射 获取传入数据的类型的注解,通过不同的注解,执行不同的时间,最后数据封装在工具类中,省去了每次生成Excel都要构造参数等繁琐的操作,通过注解配置便实现了这一步,实体类中,只有使用@Cell注解的列才能导入到Excel中。

这是一个初级版本,后面可能会加一些功能,比如列类型分组,通过分组可以实现一个实体类可以 实现多种不同的Excel导出列的表格。

样式的控制,通过注解Style 里面通过设置枚举类型来设置样式

字体设置,通过枚举设置字体。
以上说的这些分组、字体、样式这些功能是会在以后的版本中更新。如果小伙伴有什么更好的建议或意见,欢迎来评论。

这里希望能给小伙伴们带来帮助,关注我!不迷路哦!

 类似资料: