日常开发中常会遇到Excel导入导出的需求,得空写个前后端通用版的Excel导入导出demo,供之后开发效率事半功倍
功能实现
数据表
初始数据
CREATE TABLE `student` ( |
注:字段全为字符类型,避免解析数据保存到数据库表时需过多的数据类型转换逻辑
依赖文件
|
配置文件
server: |
引导类
package cn.goitman; |
四个自定义注解
EnableExport(允许导出类注解)
实体类上必须定义此注解,因工具类会判断当前类是否为允许导出
package cn.goitman.annotation; |
EnableExportField(允许导出字段注解)
在实体类上,需导出数据的字段上定义此注解,可选
package cn.goitman.annotation; |
ImportIndex(导入字段注解)
必须在实体类上需导入数据的字段上定义此注解;定义字段索引属性时,必须与数据库相应字段的索引一致;实体类字段名必须与数据库字段名称一致,在本实例中未做转换逻辑
package cn.goitman.annotation; |
EnableSelectList(下拉列表注解)
在有下拉列表数据的字段定义此注解,可选
package cn.goitman.annotation; |
枚举
package cn.goitman.enums; |
数据层
package cn.goitman.mapper; |
文件工具类
package cn.goitman.utils; |
实例测试
导入数据
常规导入
前端请求
实体类
|
接口调用:
入参名称必须与前端传参名称一致
/**
* @method uploadFile
* @description 常规导入
* @param [file]
* @return boolean
*/
public boolean uploadFile( { MultipartFile file)
return excelService.uploadFile(file);
}逻辑层:parseExcelToList方法,没有校验@EnableSelectList注解和ImportIndex注解中的useSetMethodName属性
public boolean uploadFile(MultipartFile file) { |
ExcelUtils工具类方法
此时实体类没有定义ImportIndex注解的useSetMethodName属性/**
* @param [excel 文件, clazz pojo类型]
* @return java.util.List<?>
* @method parseExcelToList
* @description 导入,将Excel数据转换为集合对象
*/
public static List<?> parseExcelToList(File excel, Class clazz) {
List<Object> res = new ArrayList<>();
// 创建输入流
InputStream is = null;
// 创建工作表
Sheet sheet = null;
try {
is = new FileInputStream(excel.getAbsolutePath());
if (is != null) {
// 创建工作簿
Workbook workbook = WorkbookFactory.create(is);
// 获取第一个工作表
sheet = workbook.getSheetAt(0);
if (sheet != null) {
int i = 1;
String values[];
// 获取第二行数据,第一行为标题
Row row = sheet.getRow(i);
while (row != null) {
// 获取总列数
int cellNum = row.getPhysicalNumberOfCells();
values = new String[cellNum];
for (int j = 0; j <= cellNum; j++) {
// 获取单元格数据
Cell cell = row.getCell(j);
String value = null;
if (cell != null) {
// 将单元格数据类型设置为字符串
cell.setCellType(Cell.CELL_TYPE_STRING);
value = cell.getStringCellValue() == null ? null : cell.getStringCellValue();
values[j] = value;
}
}
// 反射获取类中所有声明字段
Field[] fields = clazz.getDeclaredFields();
Object obj = clazz.newInstance();
for (Field f : fields) {
// 判断ImportIndex注解是否在此字段上,true为存在
if (f.isAnnotationPresent(ImportIndex.class)) {
ImportIndex annotation = f.getAnnotation(ImportIndex.class);
// 获取索引值
int index = annotation.index();
// 获取方法名
String useSetMethodName = annotation.useSetMethodName();
if (!"".equals(useSetMethodName)) {
// fastjson TypeUtils工具类,实现常用数据类型和对象间的相互转换
Object val = TypeUtils.cast(values[index], f.getType(), null);
// 取消Java语言访问检查
f.setAccessible(true);
// 参数一:方法名,参数二:方法参数数组
Method method = clazz.getMethod(useSetMethodName, new Class[]{f.getType()});
// 暴力访问
method.setAccessible(true);
// 返回值是Object接收,参数一:对象是谁,参数二:调用该方法的实际参数
method.invoke(obj, new Object[]{val});
} else {
f.setAccessible(true);
Object val = TypeUtils.cast(values[index], f.getType(), null);
// 将指定对象上此 Field字段设置为新值。参数一:指定对象,参数二:新值
f.set(obj, val);
}
}
}
// 将对象数据保存至集合
res.add(obj);
i++;
// 遍历下一行数据
row = sheet.getRow(i);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return res;
}执行结果
控制台信息
数据库数据
下拉列表注解的数据转化
前端请求
以实体类中性别字段为例
接口调用
/**
* @method uploadFileSelectList
* @description 演示EnableSelectList注解、ImportIndex注解useSetMethodName属性的使用
* @param [file]
* @return boolean
*/
public boolean uploadFileSelectList( { MultipartFile file)
try {
return excelService.uploadFileSelectList(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return false;
}逻辑层:此处的ALL_SELECT_LIST_MAP预存数据硬编码不灵活,改善的方式很多,在此不衍生讨论啦
import static cn.goitman.utils.ExcelUtils.ALL_SELECT_LIST_MAP;
public boolean uploadFileSelectList(MultipartFile file) throws FileNotFoundException {
// 预存数据,配合EnableSelectList注解使用
Map<String, String> selsctList = new HashMap<>();
selsctList.put("1","男");
selsctList.put("2","女");
ALL_SELECT_LIST_MAP.put(3,selsctList);
String path = FileUtil.saveFileToLocal(file);
FileInputStream inputStream = new FileInputStream(path);
List<Student> students = (List<Student>) ExcelUtils.parseExcelToList(inputStream, Student.class);
return excelMapper.insertList(students) > 0;
}ExcelUtils工具类方法
/**
* @param [excel 文件输入流, clazz pojo类型]
* @return java.util.List<?>
* @method parseExcelToList
* @description 导入,将Excel数据转换为集合对象,另校验EnableSelectList注解或ImportIndex注解useSetMethodName属性
*/
public static List<?> parseExcelToList(InputStream excel, Class clazz) {
List<Object> res = new ArrayList<>();
InputStream is = null;
Sheet sheet = null;
try {
is = excel;
if (is != null) {
Workbook workbook = WorkbookFactory.create(is);
sheet = workbook.getSheetAt(0);
if (sheet != null) {
int i = 1;
String values[];
Row row = sheet.getRow(i);
while (row != null) {
int cellNum = row.getPhysicalNumberOfCells();
values = new String[cellNum];
for (int j = 0; j < cellNum; j++) {
Cell cell = row.getCell(j);
if (cell != null) {
cell.setCellType(Cell.CELL_TYPE_STRING);
String value = cell.getStringCellValue() == null ? null : cell.getStringCellValue();
values[j] = value;
}
}
Field[] fields = clazz.getDeclaredFields();
Object obj = clazz.newInstance();
for (Field f : fields) {
if (f.isAnnotationPresent((ImportIndex.class))) {
ImportIndex annotation = f.getAnnotation(ImportIndex.class);
int index = annotation.index();
Object value = values[index];
// 判断EnableSelectList注解是否在此字段上,true为存在
if (f.isAnnotationPresent(EnableSelectList.class)) {
// 根据索引获取下拉列表值
value = getKeyByValue(ALL_SELECT_LIST_MAP.get(index), String.valueOf(value));
}
String useSetMethodName = annotation.useSetMethodName();
if (!"".equals(useSetMethodName)) {
Object val = TypeUtils.cast(value, f.getType(), null);
f.setAccessible(true);
Method method = clazz.getMethod(useSetMethodName, new Class[]{f.getType()});
method.setAccessible(true);
method.invoke(obj, new Object[]{val});
} else {
f.setAccessible(true);
Object val = TypeUtils.cast(value, f.getType(), null);
f.set(obj, val);
}
}
}
res.add(obj);
i++;
row = sheet.getRow(i);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return res;
}执行结果
数据表还原回初始数据,此代码段将性别字段数据做了转化处理
控制台信息
数据库数据
useSetMethodName属性的数据转化
前端请求、接口调用和ExcelUtils工具类方法与上述下拉列表注解的数据转化相同
还是以实体类中性别字段为例,注销EnableSelectList注解,setSex方法将性别数据做了转化
逻辑层:注销预存数据ALL_SELECT_LIST_MAP代码块
执行结果
还原回初始数据,此反射代码解析useSetMethodName属性
控制台信息
数据库数据
常规导出
前端请求:此链接为本地测试链接
实体类:以导出姓名、年龄、性别三个字段数据为例
接口调用
/**
* @method downloadFile
* @description 常规导出
* @param [response]
* @return void
*/
public void downloadFile(HttpServletResponse response) {
excelService.downloadFile(response);
}逻辑层
public void downloadFile(HttpServletResponse response) { |
ExcelUtils工具类方法
/**
* @param [outputStream 输出流, dataList 导出的数据, clazz 导出数据的pojo类型, selectMap 下拉列表的列, exportTitle 标题]
* @return void
* @method exportExcel
* @description 导出Excel
*/
public static void exportExcel(HttpServletResponse response, List dataList, Class clazz, Map<Integer, Map<String, String>> selectMap, String exportTitle) {
// 创建工作簿
HSSFWorkbook workbook = new HSSFWorkbook();
// 创建工作表
HSSFSheet sheet = workbook.createSheet();
// 设置工作表行的默认高度
sheet.setDefaultRowHeight((short) (20 * 20));
// 判断当前类是否允许导出
if (clazz.isAnnotationPresent(EnableExport.class)) {
EnableExport export = (EnableExport) clazz.getAnnotation(EnableExport.class);
// 所有列标题名称
List<String> colNames = new ArrayList<>();
// 所有列标题背景颜色
List<ColorEnum> colors = new ArrayList<>();
// 允许导出的字段
List<Field> fieldList = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(EnableExportField.class)) {
EnableExportField enableExportField = field.getAnnotation(EnableExportField.class);
colNames.add(enableExportField.colName());
colors.add(enableExportField.cellColor());
fieldList.add(field);
}
}
// 设置每列的宽度
for (int i = 0; i < fieldList.size(); i++) {
Field field = fieldList.get(i);
sheet.setColumnWidth(i, field.getAnnotation(EnableExportField.class).colWidth() * 20);
}
HSSFRow hssfRow = null; // 表行
HSSFCell hssfCell = null; // 单元格
// 设置列标题
String fileName = export.fileName();
if (exportTitle != null) {
fileName = exportTitle;
}
// 绘制标题,可选
createTitle(workbook, hssfRow, hssfCell, sheet, colNames.size() - 1, fileName, export.cellColor());
// 创建表头列名
createHeadRow(workbook, hssfRow, hssfCell, sheet, colNames, colors);
try {
// 绘制单元格样式
HSSFCellStyle cellStyle = getBasicCellStyle(workbook);
// 插入数据
int i = 0;
for (Object obj : dataList) {
// 表头标题和列名已创建,所以从第三行开始
hssfRow = sheet.createRow(i + 2);
for (int j = 0; j < fieldList.size(); j++) {
Field field = fieldList.get(j);
field.setAccessible(true);
Object value = field.get(obj);
EnableExportField enableExportField = field.getAnnotation(EnableExportField.class);
String getMethodName = enableExportField.useGetMethod();
if (!"".equals(getMethodName)) {
Method method = clazz.getMethod(getMethodName, new Class[]{field.getType()});
method.setAccessible(true);
method.invoke(obj, new Object[]{value});
}
if (field.isAnnotationPresent(EnableSelectList.class)) {
if (selectMap != null && selectMap.get(j) != null) {
value = selectMap.get(j).get(value);
}
}
setCellValue(value, hssfCell, hssfRow, cellStyle, j);
}
i++;
}
// 提供下载框,并设置文件名
response.setContentType("octets/stream");
// 防止中文文件名称乱码,需encode,并设置字符集
response.setHeader("Content-Disposition", "attachment;filename=" + java.net.URLEncoder.encode(fileName, "UTF-8") + ".xls");
OutputStream outputStream = null;
// 获取响应流
outputStream = response.getOutputStream();
workbook.write(outputStream);
outputStream.flush();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}执行结果
控制台信息
导出的excel数据
如果不需要标题,即可注释createTitle方法
,同时需在插入数据时修改行数值为i+1
总结
下拉列表注解和useSetMethodName属性的玩法,当然不止数据转化这一种,在此不延伸啦,在评论区写下你的构思一起学习吧!
源码地址:https://github.com/wangdaicong/spring-boot-project/tree/master/poi-importAndExport-demo