【ExcelUtil】二次封装,注解驱动,用起来不要太舒服!
需求分析
除了最基础的表头名转换、表头和内容列宽自适应居中外,还需增加对表头顺序位置的指定,指定导出的日期数据时间日期格式,马达马达,对于枚举内容希望能够通过指定的分隔符读取写入值,此外,对于无数据的单元格可以按照需求给默认值 ......
最后,给一个是否导出数据标识用来应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写。
我:
代码实现
自定义注解
首先,根据需求自定义一个注解,其中的每个属性对应一个功能:
/**
* @description: 自定义导出 Excel 数据注解
* @author: HUALEI
* @date: 2021-11-19
* @time: 15:37
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel {
/**
* 导出到 Excel 中的表头别名
*/
String headerAlias() default "";
/**
* 导出时在 Excel 中的排序
*/
int sort() default Integer.MAX_VALUE;
/**
* 日期格式,如: yyyy-MM-dd
*/
String dateFormat() default "";
/**
* 根据分隔符读取内容转表达式 (如: 0=男,1=女,2=未知)
*/
String readConverterExp() default "";
/**
* 分隔符(默认为 "," 逗号),读取字符串组内容(注意:有些特殊分割字符需要用 "\\sparator" 或 "[sparator]"进行转义,否则分割字符串失败)
*/
String separator() default ",";
/**
* 当值为空时,字段的默认值
*/
String defaultValue() default "";
/**
* 是否导出数据
*/
boolean isExport() default true;
enum Type {
/** 导出导入 */
ALL(0),
/** 仅导出 */
EXPORT(1),
/** 仅导入 */
IMPORT(2);
private final int value;
Type(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
/**
* 字段类型(0:导出导入;1:仅导出;2:仅导入)
*/
Type type() default Type.ALL;
}
复制代码
注解中有一个 Type
内部枚举类,用来区分被注解标识字段是导入还是导出,虽然这里的需求只要做导出,防范于未然,帮助你立身于需求高地。
工具类封装
通过 new ExcelUtil<>(xxx.class);
来创建二次封装对象,ExcelUtil
类中
包含文件名、工作表名等基本属性,还有注解字段列表用来存储通过反射获取被注解标识的 Field
字段对象和对应的注解属性,内部存储结构为:[[Field
, Excel
], ...] 。
/**
* @description: ExcelUtil 工具类二次封装
* @author: HUALEI
* @date: 2021-11-20
* @time: 17:56
*/
public class ExcelUtil<T> {
private static final Logger logger = LoggerFactory.getLogger(ExcelUtil.class);
/**
* Excel 文件名
*/
private String fileName;
/**
* 工作表名称
*/
private String sheetName;
/**
* 导出类型
*/
private Excel.Type type;
/**
* 文件名后缀
*/
private String fileNameSuffix;
/**
* 导入导出数据源列表
*/
private List sourceList;
/**
* 注解字段列表 [[Field, Excel], ...]
*/
private List
封装类中除了成员变量外,最重要的就是成员方法了,考虑到导出的文件可能有时会需要 .xls
格式,所以我重载了导出 Excel
方法,默认为 .xlsx
格式。
/**
* 对数据源列表写入到 Excel 文件中
*
* @param response HttpServletResponse 对象
* @param list 数据源列表
* @param fileName Excel 文件名
* @param sheetName Excel 中工作表名
*/
public void exportExcel(HttpServletResponse response,
List list,
String fileName,
String sheetName
) throws Exception {
this.excelWriter = cn.hutool.poi.excel.ExcelUtil.getBigWriter();
logger.info("=============== 初始化 Excel ===============");
init(list, fileName, sheetName, Excel.Type.EXPORT);
exportExcel(response, null);
logger.info("=============== 导出 Excel 成功 ===============");
}
/**
* 对数据源列表写入到 Excel 文件中
*
* @param response HttpServletResponse 对象
* @param list 数据源列表
* @param fileName Excel 文件名
* @param fileNameSuffix Excel 文件名后缀
* @param sheetName Excel 中工作表名
*/
public void exportExcel(HttpServletResponse response,
List list,
String fileName,
String fileNameSuffix,
String sheetName
) throws Exception {
this.excelWriter = cn.hutool.poi.excel.ExcelUtil.getBigWriter();
logger.info("=============== 初始化 Excel ===============");
init(list, fileName, sheetName, Excel.Type.EXPORT);
exportExcel(response, fileNameSuffix);
logger.info("=============== 导出 Excel 成功 ===============");
}
复制代码
导出方法中,首先就是要初始化写入器,然后初始化类属性值:
/**
* 初始化类属性
*
* @param list 数据源列表
* @param fileName 导出文件名
* @param sheetName 工作表名
* @param type 导出类型
*/
public void init(List list, String fileName, String sheetName, Excel.Type type) throws Exception {
this.sourceList = Optional.ofNullable(list).orElseGet(ArrayList::new);
this.fileName = fileName;
this.sheetName = sheetName;
// 设置 Sheet 工作表名称
this.excelWriter.renameSheet(sheetName);
this.type = type;
// 创建表头
createExcelField();
// 处理数据源
handleDataSource();
}
复制代码
初始化部分成员变量后,创建指定顺序表头,并设置表头别名:
/**
* 创建指定顺序表头,并设置表头别名
*/
private void createExcelField() {
this.fields = new ArrayList
先获取目标实体对象的父类和自身所有声明字段,存入临时字段列表,然后循环遍历过滤出被 @Excel
注解标识的字段,然后通过筛选目标导出类型构建一个大小为 2 的数组放入注解字段列表 this.fields
中。
其次,根据注解中 sort
属性值进行升序排序,如果全未设置顺序值,则默认根据字段定义的先后顺序进行排序。排序好之后按顺序设置表头别名,未设置的保持默认字段名。
创建完表头后,接下来就需要根据注解字段列表 fields
中每个字段上的注解属性对象对数据源列表进行处理:
/**
* 根据注解属性处理数据源列表
*
* @throws Exception 获取类属性值可能抛出的异常
*/
private void handleDataSource() throws Exception {
for (Object[] arr : this.fields) {
// 注解标识的字段
Field field = (Field) arr[0];
// 注解属性对象
Excel attr = (Excel) arr[1];
// 设置实体类私有属性可访问
field.setAccessible(true);
for (T object: this.sourceList) {
// 获取当前字段的属性值
Object value = field.get(object);
if (attr.isExport()) {
if (value != null) {
// 设置时间格式
if (StrUtil.isNotBlank(attr.dateFormat())) {
field.set(object, cn.hutool.core.convert.Convert.convert(field.getType(), DateUtil.format(new DateTime(value.toString()), attr.dateFormat())));
}
// 设置转换值
if (StrUtil.isNotBlank(attr.readConverterExp())) {
String convertResult = convertByExp(Convert.toStr(value), attr.readConverterExp(), attr.separator());
field.set(object, convertResult);
}
} else {
// 设置默认值
if (StrUtil.isNotBlank(attr.defaultValue())) {
field.set(object, attr.defaultValue());
}
}
} else {
field.set(object, null);
}
}
}
}
复制代码
上述代码主要通过 Java
反射原理拿到当前对象 object
下 field
字段的属性值,判断当前列数据是否需要导出,需要则进一步判断注解中的属性对应的是否有值,有值且字段属性值不为 null
,就去更改原有值;有值但字段属性值为 null
的,就可以设置为指定的默认值。反之,不需要导出,则将该列所有单元格置空。
单纯理解文字可能没有一个流程图来得直观、清楚,这就给你安排上:
对于解析导出值方法 convertByExp()
,通过分隔符分割翻译注解字符串,根据 "=" 等于号左边为键、右边为值原则进行解析,具体实现代码如下:
/**
* 解析导出值
*
* @param propertyValue 参数值
* @param converterExp 翻译注解
* @param separator 分隔符
* @return 解析后值
*/
public static String convertByExp(String propertyValue, String converterExp, String separator) {
StringBuilder propertyString = new StringBuilder();
String[] convertSource = converterExp.split(separator);
for (String item : convertSource) {
String[] itemArray = item.split("=");
if (StringUtils.containsAny(separator, propertyValue)) {
for (String value : propertyValue.split(separator)) {
if (itemArray[0].equals(value)) {
propertyString.append(itemArray[1]).append(separator);
break;
}
}
}
else {
if (itemArray[0].equals(propertyValue)) {
return itemArray[1];
}
}
}
return StringUtils.stripEnd(propertyString.toString(), separator);
}
复制代码
以上就完成所有的初始化的工作了,接下来就可以愉快地往 Excel
里写数据,最后写出文件到客户端进行下载。
/**
* 写出到客户端下载
*
* @param response HttpServletResponse 对象
* @param suffix 导出 Excel 文件名后缀
*/
public void exportExcel(HttpServletResponse response, String suffix) throws IOException {
// 输出流
ServletOutputStream out = response.getOutputStream();
this.excelWriter.write(this.sourceList, true);
cellWidthSelfAdaption();
initResponse(response, suffix);
this.excelWriter.flush(out, true);
// 关闭 writer,释放内存
this.excelWriter.close();
// 关闭输出 Servlet 流
IoUtil.close(out);
}
复制代码
cellWidthSelfAdaption()
方法是用来实现中文宽度自适应的,这里就不贴代码了,详细说明和代码获取请点这里 传送门 (づ ̄3 ̄)づ╭❤~initResponse()
根据导出的Excel
文件名后缀初始化HttpServletResponse
对象来响应体和响应类型。
/**
* 根据导出的 Excel 文件名后缀初始化 HttpServletResponse 对象
*
* @param response HttpServletResponse 对象
* @param suffix 文件名后缀
* @throws UnsupportedEncodingException 不支持的编码异常
*/
public void initResponse(HttpServletResponse response, String suffix) throws UnsupportedEncodingException {
// 默认导出文件名后缀
this.fileNameSuffix = ".xlsx";
if (suffix != null) {
switch (suffix.toLowerCase()) {
case "xls":
case ".xls":
this.fileNameSuffix = ".xls";
response.setContentType("application/vnd.ms-excel;charset=utf-8");
break;
case "xlsx":
case ".xlsx":
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
break;
default:
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
}
} else {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
}
// 文件名中文编码
String encodingFilename = encodingFilename(this.fileName);
response.setHeader("Content-Disposition","attachment;filename="+ encodingFilename);
}
复制代码
默认导出文件格式为 .xlsx
,不过也可指定为 .xls
,通过设置不同的内容类型实现。至于导出的文件名加个后缀编个码拼接到响应头上即可!
/**
* 编码文件名
*
* @param filename 文件名
*/
public String encodingFilename(String filename) throws UnsupportedEncodingException {
filename = filename + this.fileNameSuffix;
return URLEncoder.encode(filename, CharsetUtil.UTF_8);
}
复制代码
至此,整个注解 + ExcelUtil
二次封装的代码就写完了。
暴露接口
实体对象
老样子,实体对象给它套上 @Excel
注解,随便加点属性 " Buff ":
@Data
public class ProvinceCustomAnnotationExcelVO implements Serializable {
private static final long serialVersionUID = 877981781678377000L;
/**
* 省份
*/
@Excel(headerAlias = "省份")
private String province;
/**
* 省份的简称
*/
@Excel(headerAlias = "简称")
private String abbr;
/**
* 省份的面积(km²)
*/
@Excel(headerAlias = "面积(km²)")
private Integer area;
/**
* 省份的人口(万)
*/
@Excel(headerAlias = "人口(万)")
private BigDecimal population;
/**
* 省份的著名景点
*/
@Excel(headerAlias = "著名景点")
private String attraction;
/**
* 省会的邮政编码
*/
@Excel(headerAlias = "邮政编码", readConverterExp = "100=牛逼就完事|050000=哈哈哈", separator = "\\|")
private String postcode;
/**
* 省会名
*/
@Excel(headerAlias = "省会", defaultValue = "默认值")
private String city;
/**
* 省会的别名
*/
@Excel(headerAlias = "别名", isExport = false)
private String nickname;
/**
* 省会的气候类型
*/
@Excel(headerAlias = "气候类型")
private String climate;
/**
* 省会的车牌号
*/
@Excel(headerAlias = "车牌号", defaultValue = "数据暂无")
private String carcode;
/**
* 测试时间
*/
@Excel(headerAlias = "创建时间", dateFormat = "yyyy年MM月dd日 HH时mm分ss秒")
private String createTime;
}
复制代码
控制层
Service
层 getAllProvinceDetails()
方法具体代码实现请参考 【ExcelUtil】实现文件写出到客户端下载全过程 - 掘金 (juejin.cn) 。
@GetMapping("provinces/custom/excel/export/{fileNameSuffix}")
public void customAnnotationExcelExport(HttpServletResponse response, @PathVariable("fileNameSuffix") String fileNameSuffix) throws Exception {
// 获取省份详情信息
List provinceExcelList = this.provinceService.getAllProvinceDetails();
// Bean 对象转换拿到数据源列表
List provinceCustomAnnotationExcelList = BeanUtil.copyToList(provinceExcelList, ProvinceCustomAnnotationExcelVO.class);
// 为了测试导出时间格式化,添加点随机日期时间
provinceCustomAnnotationExcelList.forEach(p -> p.setCreateTime(RandomUtil.randomDate(new Date(), DateField.SECOND, 0, 24*60*60).toString()));
// 使用有参构造(必需)创建一个 ExcelUtil 对象
ExcelUtil excelUtil = new ExcelUtil<>(ProvinceCustomAnnotationExcelVO.class);
// 文件名(当天日期_各省份信息)
String fileName = StrUtil.format("{}{}各省份信息", DateUtil.today(), StrUtil.UNDERLINE);
// Sheet 工作表名
String sheetName = "省份详情表";
if (StrUtil.isBlank(fileNameSuffix)) {
// 测试导出默认格式
excelUtil.exportExcel(response, provinceCustomAnnotationExcelList, fileName, sheetName);
} else {
// 测试导出指定格式
excelUtil.exportExcel(response, provinceCustomAnnotationExcelList, fileName, fileNameSuffix, sheetName);
}
}
复制代码
导出的文件名后缀放在路径上主要是为了测试的方便,实际开发中 duck 不必这样!
接口测试
开始测试:
GET: http://localhost:8088/file/provinces/custom/excel/export/xls
GET: http://localhost:8088/file/provinces/custom/excel/export/.xlsx
GET: http://localhost:8088/file/provinces/custom/excel/export/""
GET: http://localhost:8088/file/provinces/custom/excel/export/HUALEI
测试全部通过,堪称完美,填坑成功!!撒花 ✿✿ヽ(°▽°)ノ✿
总结
总体实现下来并不算太难,使用注解驱动简直不要太香,用起来很方便,即便没学过编程的小白也会用,一两行代码就能完成一个数据源列表的导出。
唯一不足的就是数据导入没有集成进去,不过本文重点并不在于导入,哈哈哈,有兴趣的小伙伴可以尝试一下哦 ヾ(◍°∇°◍)ノ゙
作者:HUALEI
链接:https://juejin.cn/post/7033664913373560840
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。