一、内容概述
在实际开发中,难以避免遇到对Excel文件导入、导出的需求,由于我是第一次做便在此做学习记录,目的在于加深印象,便于回顾。本文将使用到POI技术处理包、java反射机制实现导入前端上传导入Excel文件,后端读取信息并将信息封装入实体对象;反过来,实现把实体对象的属性信息以Excel文件导出。最后将功能封装成工具类,便于复用。
二、主要内容
1.创建一个简单的Spring boot 项目
1.1写一个简单Controller返回测试页面
@Controller
public class TestController {
@RequestMapping(value = "/upload",method = RequestMethod.GET)
public String upload(){
return "upload";
}
}
1.2前端页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data" onsubmit="return void(0)">
<input type="file" name="file" id="file">
<button type="submit">上传</button>
</form>
<a href="/download">导出</a>
</body>
</html>
一个简单的报表和一个导出按钮,报表用于上传Excel文件。
2.导包
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
PS:如果在运行时出现java.lang.ClassNotFoundException: org.apache.poi.xssf.usermodel.XSSFWorkbook异常,多半为poi包与poi-ooxml包的版本不匹配造成,若检查版本没有问题,可以选择注释掉poi让maven自动导入poi依赖。
此外还需导入工具包:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
3.创建自定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcelColumn {
String value() default "";
int col() default 0;
}
4.创建实体类,并加自定义注解
import com.guohui.demo_poi.annotation.ExcelColumn;
public class Student {
@ExcelColumn(value = "学号",col = 1)
private Long studentId;
@ExcelColumn(value = "姓名",col = 2)
private String name;
@ExcelColumn(value = "性别",col = 3)
private String sex;
@ExcelColumn(value = "班级",col = 4)
private String grade;
@Override
public String toString() {
return "Student{" +
"studentId=" + studentId +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
", grade='" + grade + '\'' +
'}';
}
public Student(){
}
public Long getStudentId() {
return studentId;
}
public void setStudentId(Long studentId) {
this.studentId = studentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getGrade() {
return grade;
}
public void setGrade(String grade) {
this.grade = grade;
}
}
- value值需与Excel表头列名对应,导入Excel进行数据封装时以此对应关系进行赋值。多个注解value可以相同,赋值时会一并赋值。
- col值为列索引,导出Excel文件时以col值从小到大向右排列。
5.创建ExcelUtils工具类
import com.guohui.demo_poi.annotation.ExcelColumn;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ExcelUtils {
private final static Logger log = LoggerFactory.getLogger(ExcelUtils.class);
private final static String EXCEL2003 = "xls";
private final static String EXCEL2007 = "xlsx";
/**
* 读取EXCEL文件
* @param cls 封装对象类类型
* @param file 前端上传的MultipartFile对象
* @param <T> 封装对象类型
* @return T对象集合
*/
public static <T> List<T> readExcel(Class<T> cls,MultipartFile file){
String fileName = file.getOriginalFilename(); //获取上传文件名
if (!fileName.matches("^.+\\.(?i)(xls)$") && !fileName.matches("^.+\\.(?i)(xlsx)$")) {
log.error("上传文件格式不正确");
}
List<T> dataList = new ArrayList<>();
Workbook workbook = null;
try {
InputStream is = file.getInputStream();
if (fileName.endsWith(EXCEL2007)) {
// FileInputStream is = new FileInputStream(new File(path));
workbook = new XSSFWorkbook(is);
}
if (fileName.endsWith(EXCEL2003)) {
// FileInputStream is = new FileInputStream(new File(path));
workbook = new HSSFWorkbook(is);
}
if (workbook != null) {
//类映射 注解 value-->bean columns(注解value可能相同,并注释多个字段)
Map<String, List<Field>> classMap = new HashMap<>();
//获取字段集合
List<Field> fields = Stream.of(cls.getDeclaredFields()).collect(Collectors.toList());
//遍历字段集合
fields.forEach(
field -> {
//过滤未加ExcelColumn的字段
ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
if (annotation != null) {
//获取注解值
String annotationValue = annotation.value();
if (StringUtils.isBlank(annotationValue)) {
//过滤注解value为空的字段
return;//return起到的作用和continue是相同的语法
}
//若注解value未存在则添加映射
if (!classMap.containsKey(annotationValue)) {
classMap.put(annotationValue, new ArrayList<>());
}
//开启字段访问权限
field.setAccessible(true);
classMap.get(annotationValue).add(field);
}
}
);
//索引-->columns
Map<Integer, List<Field>> reflectionMap = new HashMap<>(16);
//默认读取第一个sheet
Sheet sheet = workbook.getSheetAt(0);
boolean firstRow = true;
//遍历行
for (int i = sheet.getFirstRowNum(); i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
//首行 提取注解(列名)
if (firstRow) {
//遍历列
for (int j = row.getFirstCellNum(); j <= row.getLastCellNum(); j++) {
Cell cell = row.getCell(j);
String columnName = getCellValue(cell);
if (classMap.containsKey(columnName)) {
//以列名为key获取对应的封装字段集合
reflectionMap.put(j, classMap.get(columnName));
}
}
firstRow = false;
} else {
//忽略空白行
if (row == null) {
continue;
}
try {
//反射创建封装对象
T t = cls.newInstance();
//是否为空白单元格
boolean cellBlank = true;
//遍历列
for (int j = row.getFirstCellNum(); j <= row.getLastCellNum(); j++) {
if (reflectionMap.containsKey(j)) {
Cell cell = row.getCell(j);
String cellValue = getCellValue(cell);
if (StringUtils.isNotBlank(cellValue)) {
cellBlank = false;
}
//通过列索引获取字段集合
List<Field> fieldList = reflectionMap.get(j);
//遍历字段集合,利用反射机制循环封装数据
fieldList.forEach(
x -> {
try {
handleField(t, cellValue, x);
} catch (Exception e) {
log.error(String.format("反射写入异常!字段名:%s 值:%s ", x.getName(), cellValue), e);
}
}
);
}
}
//若单元不全为空,则保存封装对象
if (!cellBlank) {
dataList.add(t);
} else {
log.warn(String.format("第%s行为空,已忽略!", i));
}
} catch (Exception e) {
log.error(String.format("解析第%s行异常!", i), e);
}
}
}
}
} catch (Exception e) {
log.error(String.format("解析excel文件异常!"), e);
} finally {
if (workbook != null) {
try {
workbook.close();
} catch (Exception e) {
log.error(String.format("解析excel文件异常!"), e);
}
}
}
return dataList;
}
/**
* 反射写入属性
* @param t
* @param value
* @param field
* @param <T>
* @throws Exception
*/
private static <T> void handleField(T t, String value, Field field) throws Exception {
//获取字段类型
Class<?> type = field.getType();
if (type == null || type == void.class || StringUtils.isBlank(value)) {
return;
}
if (type == Object.class) {
field.set(t, value);
//数字类型
} else if (type.getSuperclass() == null || type.getSuperclass() == Number.class) {
if (type == int.class || type == Integer.class) {
field.set(t, NumberUtils.toInt(value));
} else if (type == long.class || type == Long.class) {
field.set(t, NumberUtils.toLong(value));
} else if (type == byte.class || type == Byte.class) {
field.set(t, NumberUtils.toByte(value));
} else if (type == short.class || type == Short.class) {
field.set(t, NumberUtils.toShort(value));
} else if (type == double.class || type == Double.class) {
field.set(t, NumberUtils.toDouble(value));
} else if (type == float.class || type == Float.class) {
field.set(t, NumberUtils.toFloat(value));
} else if (type == char.class || type == Character.class) {
field.set(t, CharUtils.toChar(value));
} else if (type == boolean.class) {
field.set(t, BooleanUtils.toBoolean(value));
} else if (type == BigDecimal.class) {
field.set(t, new BigDecimal(value));
}
} else if (type == Boolean.class) {
field.set(t, BooleanUtils.toBoolean(value));
} else if (type == Date.class) {
//
field.set(t, value);
} else if (type == String.class) {
field.set(t, value);
} else {
Constructor<?> constructor = type.getConstructor(String.class);
field.set(t, constructor.newInstance(value));
}
}
/**
* 获取单元格内容
* @param cell
* @return
*/
private static String getCellValue(Cell cell) {
if (cell == null) {
return "";
}
if (cell.getCellTypeEnum() == CellType.NUMERIC) {
if (DateUtil.isCellDateFormatted(cell)) {
return HSSFDateUtil.getJavaDate(cell.getNumericCellValue()).toString();
} else {
return new BigDecimal(cell.getNumericCellValue()).toString();
}
} else if (cell.getCellTypeEnum() == CellType.STRING) {
return StringUtils.trimToEmpty(cell.getStringCellValue());
} else if (cell.getCellTypeEnum() == CellType.FORMULA) {
return StringUtils.trimToEmpty(cell.getCellFormula());
} else if (cell.getCellTypeEnum() == CellType.BLANK) {
return "";
} else if (cell.getCellTypeEnum() == CellType.BOOLEAN) {
return String.valueOf(cell.getBooleanCellValue());
} else if (cell.getCellTypeEnum() == CellType.ERROR) {
return "ERROR";
} else {
return cell.toString().trim();
}
}
/**
* 生成excel对应的Workbook对象
* @param dataList 对象集合
* @param cls 对象类类型
* @param <T> 对象类型
* @return Workbook对象
*/
public static <T> Workbook writeExcel(List<T> dataList, Class<T> cls){
//获取字段集合
Field[] fields = cls.getDeclaredFields();
//过滤没有ExcelColumn注解的字段,并排序
List<Field> fieldList = Arrays.stream(fields)
.filter(field -> {
ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
if (annotation != null && annotation.col() > 0) {
field.setAccessible(true);
return true;
}
return false;
}).sorted(Comparator.comparing(field -> {
int col = 0;
ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
if (annotation != null) {
col = annotation.col();
}
return col;
})).collect(Collectors.toList());
//创建工作页
Workbook wb = new XSSFWorkbook();
//新建工作页
Sheet sheet = wb.createSheet("Sheet1");
//创建线程安全的AtomicInteger,默认为0
AtomicInteger ai = new AtomicInteger();
{
Row headRow = sheet.createRow(ai.getAndIncrement());
AtomicInteger aj = new AtomicInteger();
//写入头部
fieldList.forEach(field -> {
ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
String columnName = "";
if (annotation != null) {
columnName = annotation.value();
}
//创建标题行
Cell cell = headRow.createCell(aj.getAndIncrement());
//设置格式
CellStyle cellStyle = wb.createCellStyle();
cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
cellStyle.setAlignment(HorizontalAlignment.CENTER);
//设置字体
Font font = wb.createFont();
font.setBold(true);
//应用格式与字体
cellStyle.setFont(font);
cell.setCellStyle(cellStyle);
cell.setCellValue(columnName);
});
}
//写入内容
if (CollectionUtils.isNotEmpty(dataList)) {
dataList.forEach(object -> {
Row contentRow = sheet.createRow(ai.getAndIncrement());
AtomicInteger aj = new AtomicInteger();
fieldList.forEach(field -> {
Class<?> type = field.getType();
Object value = "";
try {
value = field.get(object);
} catch (Exception e) {
e.printStackTrace();
}
Cell cell = contentRow.createCell(aj.getAndIncrement());
if (value != null) {
if (type == Date.class) {
cell.setCellValue(value.toString());
} else {
cell.setCellValue(value.toString());
}
cell.setCellValue(value.toString());
}
});
});
}
//冻结窗格
wb.getSheet("Sheet1").createFreezePane(0, 1, 0, 1);
return wb;
}
/**
* 浏览器下载excel
* @param fileName
* @param wb
* @param response
*/
public static void buildExcelDocument(String fileName, Workbook wb,HttpServletResponse response){
try {
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader("Content-Disposition", "attachment;filename="+URLEncoder.encode(fileName, "utf-8"));
response.flushBuffer();
wb.write(response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 生成excel文件
* @param path 生成excel路径
* @param wb
*/
private static void buildExcelFile(String path, Workbook wb){
File file = new File(path);
if (file.exists()) {
file.delete();
}
try {
wb.write(new FileOutputStream(file));
} catch (Exception e) {
e.printStackTrace();
}
}
本想自己写一个封装工具类,但发现一位大佬博主已经写好了(原网站在之后给出),我在此基础上进行了一些简单的修改(poi版本不一样),并根据自己的理解做了一些注释,改善代码的可读性。
5.1值得注意问题的问题
-
数据格式问题
取值前应先判断单元格的数据格式,再按对应数据格式进行取值,否则报错。
poi支持6种类型,并且每种类型都定义了类型常量:
图中为poi 3.15版本之前的调用方式,3.15之后的调用方法可见工具类,但支持的类型还是一样的。
6.增加Controller方法
@ResponseBody
@RequestMapping(value = "/upload",method = RequestMethod.POST)
public String doUpload(MultipartFile file) throws Exception {
if (file != null){
System.out.println(file.getOriginalFilename());
}else {
System.out.println("空");
}
List<Student> students = ExcelUtils.readExcel(Student.class, file);
students.forEach(student -> {
System.out.println(student.toString());
});
return "OK";
}
@RequestMapping(value = "/download")
public void download(HttpServletResponse response) throws IOException {
//创建测试数据
List<Student> studentList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Student student = new Student();
student.setStudentId(Long.valueOf(i+1));
student.setName("测试"+i);
student.setSex("男"+i);
student.setGrade("大班"+i);
studentList.add(student);
}
Workbook workbook = ExcelUtils.writeExcel(studentList, Student.class);
ExcelUtils.buildExcelDocument("test.xlsx",workbook,response);
}
- doUpload方法用于接收前端表单上传的Excel文件,调用ExcelUtils类readExcel方法进行数据读取并封装。
-
download方法为前端导出Excel文件的处理方法。
测试Excel如下:
7.运行测试
1.上传Excel文件输出
2.导出文件输出
三、参考文档
1.https://blog.csdn.net/qq_33223299/article/details/78916457
2.https://www.jianshu.com/p/1184aa8f1124
3.https://blog.csdn.net/qq_28379809/article/details/82181849
4.https://www.jianshu.com/p/3a89e19a1bc3