基于 SSM + Vue 的 POI Excel 导入导出全流程实现
本文详细讲解基于 SSM(Spring + SpringMVC + MyBatis)后端与 Vue + Element UI 前端的 Excel 导入导出功能实现。通过自定义注解的方式,实现通用的 POI 操作流程。
1. 环境准备与依赖引入
首先需要在项目的 pom.xml 中引入 Apache POI 及相关工具类的依赖。
xml
<!-- POI 核心依赖(支持 .xls 格式 - Office 2003) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<!-- POI OOXML(支持 .xlsx 格式 - Office 2007+,目前主流) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<!-- Commons BeanUtils:用于简化实体类属性的反射操作 -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
<!-- Commons FileUpload:SpringMVC 处理文件上传的核心依赖 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
2. 后端核心架构设计
2.1 自定义注解
通过注解标记实体类与 Excel 表格的映射关系,实现通用解析。
-
@ExcelField:- 作用 :标注在实体类的字段上。
- 功能:定义字段在 Excel 表头中对应的中文名称、排序号、是否参与导入/导出等。
- 元注解 :
@Target(ElementType.FIELD)。 


-
@ExcelEntity:- 作用 :标注在实体类上。
- 功能:定义实体类对应的 Excel 表名(Sheet 名)。
- 元注解 :
@Target(ElementType.TYPE)。 
2.2 工具类设计
ExcelAnnotationUtil:负责解析实体类上的注解,生成"表头-字段"的映射关系。ExcelUtil:核心工具类,负责创建 Workbook、填充数据(导出)以及读取 Workbook、封装实体(导入)。- 注:完整工具类代码见文末附录。
3. 业务逻辑层 (Service) 实现
3.1 导入逻辑
思路 :Controller 接收文件 -> 调用工具类解析 Excel 为 List<User> -> 调用 Mapper 批量插入数据库。

3.2 导出逻辑
思路 :Mapper 查询所有数据 -> 调用工具类解析注解(获取表头和Sheet名) -> 生成 Excel 二进制流 -> 通过 HttpServletResponse 输出。

4. SpringMVC 配置与 Controller
4.1 配置文件上传解析器
在 SpringMVC 配置文件中添加 MultipartResolver,否则后端无法接收 MultipartFile。
xml
<!-- 配置文件上传解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 最大上传文件大小:10MB (10 * 1024 * 1024) -->
<property name="maxUploadSize" value="10485760"/>
<!-- 默认编码格式,防止文件名乱码 -->
<property name="defaultEncoding" value="UTF-8"/>
</bean>
4.2 Controller 编写
定义导入和导出两个接口。

5. 前端实现 (Vue + Element UI)
5.1 界面组件
使用 Element UI 的按钮组件触发操作。

5.2 导出功能实现 (Blob 流处理)
核心逻辑:
- 调用后端接口,设置响应类型为
blob。 - 接收二进制流,创建
Blob对象。 - 创建临时的
<a>标签触发下载。 - 释放 URL 对象。
Axios 封装 :

业务代码:
javascript
handleExport() {
this.$message.info('正在导出数据,请稍等...');
exportUserExcel()
.then(response => {
// 1. 校验响应是否为有效 blob
if (!response.data || response.data.size === 0) {
this.$message.error('导出失败:无数据可导出');
return;
}
// 2. 创建 Blob 对象,指定 MIME 类型为 Excel
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
// 3. 创建下载链接
const link = document.createElement('a');
// 使用 encodeURIComponent 解决中文文件名乱码
const fileName = encodeURIComponent('用户列表.xlsx');
link.href = window.URL.createObjectURL(blob);
link.download = decodeURIComponent(fileName); // 下载属性设为解码后的文件名
// 4. 触发点击并清理
link.click();
window.URL.revokeObjectURL(link.href); // 释放内存
this.$message.success('导出成功');
})
.catch(err => {
// 处理错误信息(注意:Blob 报错时可能需要将 Blob 转回 JSON 才能读取 msg)
this.$message.error('导出失败:' + (err.response?.data?.msg || err.message));
console.error(err);
});
},
5.3 导入功能实现 (FormData 上传)
核心逻辑:
- 点击按钮触发隐藏的
<input type="file">。 handleFileChange捕获文件。- 前端校验文件格式(.xls/.xlsx)和大小(<10MB)。
- 使用
FormData封装文件并发送请求。

业务代码:
javascript
handleImport() {
// 触发隐藏的文件输入框
this.$refs.fileInput.click();
},
handleFileChange(event) {
const file = event.target.files[0];
if (!file) return;
// 1. 前端校验文件类型
const isExcel = file.type === 'application/vnd.ms-excel' ||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
// 2. 前端校验文件大小 (10MB)
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isExcel) {
this.$message.error('只能上传xls/xlsx格式的Excel文件!');
event.target.value = ''; // 清空选择,允许重复选择同名文件
return;
}
if (!isLt10M) {
this.$message.error('文件大小不能超过10MB!');
event.target.value = '';
return;
}
// 3. 调用导入接口
importUserExcel(file)
.then(() => {
this.$message.success('导入成功');
this.fetchUsers(); // 刷新列表
event.target.value = '';
})
.catch(err => {
this.$message.error('导入失败:' + (err.response?.data?.msg || err.message));
console.error(err);
event.target.value = '';
});
},
API 调用 :

6. 效果展示
-
导出成功 :


-
导入成功 :


7. 常见问题与解决方案
7.1 导出的 Excel 无法打开 (文件损坏)

| 可能原因 | 解决方案 |
|---|---|
| 前端未设置 Blob | 前端 Axios 请求必须添加 responseType: 'blob',否则二进制流会被当做字符串/JSON 解析导致乱码。 |
| 后端流未关闭 | 后端 ExcelUtil 中必须执行 os.flush() 和 os.close(),确保缓冲区数据完全写入。 |
| 拦截器干扰 | 项目中的全局响应拦截器(Response Interceptor)可能强制解析 JSON。需添加判断:如果是 Blob 类型则直接返回,不进行 JSON 解析。 |
7.2 导入 Excel 报 500 错误
| 可能原因 | 解决方案 |
|---|---|
| 未配置解析器 | 检查 SpringMVC 配置文件是否包含 CommonsMultipartResolver Bean。 |
| 请求头错误 | 前端必须使用 FormData 对象封装文件,浏览器会自动设置 Content-Type: multipart/form-data。 |
| 参数名不一致 | 后端 @RequestParam("file") 中的名称必须与前端 formData.append('file', file) 中的 key 保持一致。 |
8. 附录:核心工具类源码
8.1 ExcelAnnotationUtil.java
用于解析注解,建立映射关系。
java
/**
* @className: ExcelAnnotationUtil
* @description: Excel注解解析工具类:解析实体类注解,生成表头/字段映射
*/
public class ExcelAnnotationUtil {
/**
* 解析实体类注解,生成导出用的表头和字段数组
* @param clazz 实体类Class
* @return 返回数组:[0]headers(表头名称数组),[1]fields(实体类字段名数组)
*/
public static <T> String[][] parseExportAnnotation(Class<T> clazz) {
return parseAnnotation(clazz, true);
}
/**
* 解析实体类注解,生成导入用的字段数组
* @param clazz 实体类Class
* @return 返回数组:[0]headers(表头用于校验),[1]fields(实体类字段名用于赋值)
*/
public static <T> String[][] parseImportAnnotation(Class<T> clazz) {
return parseAnnotation(clazz, false);
}
/**
* 通用注解解析逻辑
* @param isExport true为导出模式,false为导入模式(根据注解中的 isExport/isImport 属性过滤)
*/
private static <T> String[][] parseAnnotation(Class<T> clazz, boolean isExport){
// 获取所有声明的字段
Field[] declaredFields = clazz.getDeclaredFields();
// 使用 TreeMap 存储字段信息,key 为 sort 值,实现自动按 sort 升序排列
Map<Integer, Map<String, String>> fieldMap = new TreeMap<>();
for (Field field : declaredFields){
ExcelField excelField = field.getAnnotation(ExcelField.class);
if (excelField == null){
continue; // 跳过无注解的字段
}
// 根据模式过滤字段
if (isExport && !excelField.isExport()) {
continue;
}
if (!isExport && !excelField.isImport()) {
continue;
}
// 封装字段名、表头名
Map<String, String> fieldInfo = new TreeMap<>();
fieldInfo.put("fieldName", field.getName());
fieldInfo.put("headerName", excelField.name());
// 存入 Map 进行排序
fieldMap.put(excelField.sort(), fieldInfo);
}
// 将 Map 转换为数组返回
List<String> headers = new ArrayList<>();
List<String> fields = new ArrayList<>();
for (Map<String, String> info : fieldMap.values()){
headers.add(info.get("headerName"));
fields.add(info.get("fieldName"));
}
return new String[][]{
headers.toArray(new String[0]),
fields.toArray(new String[0])
};
}
/**
* 获取字段的注解类型(用于导入时的类型转换)
* 例如:Excel 中是文本,但实体类是 Integer,需要获取该类型进行转换
*/
public static <T> Class<?> getFieldType(Class<T> clazz, String fieldName){
try {
Field field = clazz.getDeclaredField(fieldName);
ExcelField excelField = field.getAnnotation(ExcelField.class);
// 默认为 String 类型
return excelField == null ? String.class : excelField.type();
} catch (NoSuchFieldException e) {
e.printStackTrace();
return String.class;
}
}
}
8.2 ExcelUtil.java
POI 操作核心类,包含样式设置、数据读写、类型转换。
java
/**
* 适配注解的 POI 工具类(支持 .xlsx 和类型转换)
*/
public class ExcelUtil {
// ====================== 导出逻辑 ======================
public static <T> void exportExcel(HttpServletResponse response, List<T> dataList,
String[] headers, String[] fields, String fileName) {
// 1. 创建工作簿 (XSSFWorkbook 支持 .xlsx)
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("数据列表");
// 2. 创建表头行并设置样式
Row headerRow = sheet.createRow(0);
CellStyle headerStyle = getHeaderStyle(workbook);
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
sheet.autoSizeColumn(i); // 简单的自适应列宽
}
// 3. 写入数据行
if (dataList != null && !dataList.isEmpty()) {
CellStyle contentStyle = getContentStyle(workbook);
for (int rowNum = 0; rowNum < dataList.size(); rowNum++) {
Row dataRow = sheet.createRow(rowNum + 1);
T data = dataList.get(rowNum);
// 遍历字段数组,通过反射获取值
for (int colNum = 0; colNum < fields.length; colNum++) {
Cell cell = dataRow.createCell(colNum);
cell.setCellStyle(contentStyle);
try {
// 使用 BeanUtils 获取属性值
String value = BeanUtils.getProperty(data, fields[colNum]);
cell.setCellValue(value == null ? "" : value);
} catch (Exception e) {
cell.setCellValue("");
e.printStackTrace();
}
}
}
}
// 4. 写入响应流
try {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// 防止中文文件名乱码
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
OutputStream os = response.getOutputStream();
workbook.write(os);
os.flush(); // 关键:刷新缓冲区
os.close();
workbook.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// ====================== 导入逻辑 ======================
public static <T> List<T> importExcel(MultipartFile file, Class<T> clazz) {
List<T> dataList = new ArrayList<>();
if (file.isEmpty()) {
return dataList;
}
// 1. 解析导入注解,获取表头和字段映射
String[][] annotationData = ExcelAnnotationUtil.parseImportAnnotation(clazz);
String[] headers = annotationData[0]; // Excel表头(用于校验)
String[] fields = annotationData[1]; // 实体类字段名
try {
// WorkbookFactory 自动识别 xls 或 xlsx
Workbook workbook = WorkbookFactory.create(file.getInputStream());
Sheet sheet = workbook.getSheetAt(0);
if (sheet == null) {
return dataList;
}
// 2. 校验 Excel 表头(可选,确保导入文件格式正确)
Row headerRow = sheet.getRow(0);
if (headerRow == null) {
throw new RuntimeException("Excel无表头行");
}
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.getCell(i);
String cellValue = getCellValue(cell);
if (!headers[i].equals(cellValue)) {
throw new RuntimeException("Excel表头错误:第" + (i+1) + "列应为【" + headers[i] + "】,实际为【" + cellValue + "】");
}
}
// 3. 逐行解析数据 + 类型转换
int lastRowNum = sheet.getLastRowNum();
for (int rowNum = 1; rowNum <= lastRowNum; rowNum++) {
Row row = sheet.getRow(rowNum);
if (row == null) continue;
T data = clazz.newInstance(); // 实例化对象
for (int colNum = 0; colNum < fields.length; colNum++) {
Cell cell = row.getCell(colNum);
String cellValue = getCellValue(cell);
if (cellValue.isEmpty()) continue; // 空值跳过
// 核心:根据注解指定的类型转换值
String fieldName = fields[colNum];
Class<?> fieldType = ExcelAnnotationUtil.getFieldType(clazz, fieldName);
// 使用 ConvertUtils 进行类型转换 (String -> Integer/Date等)
Object convertValue = ConvertUtils.convert(cellValue, fieldType);
// 赋值给实体类
BeanUtils.setProperty(data, fieldName, convertValue);
}
dataList.add(data);
}
workbook.close();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("导入失败:" + e.getMessage());
}
return dataList;
}
// ====================== 私有工具方法 (样式与单元格读取) ======================
/** 获取表头样式(加粗、居中、浅灰背景、边框) */
private static CellStyle getHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
Font font = workbook.createFont();
font.setBold(true);
font.setFontName("微软雅黑");
font.setFontHeightInPoints((short) 12);
style.setFont(font);
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
return style;
}
/** 获取内容样式(居中、边框) */
private static CellStyle getContentStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
Font font = workbook.createFont();
font.setFontName("微软雅黑");
font.setFontHeightInPoints((short) 11);
style.setFont(font);
return style;
}
/** 统一获取单元格值(适配 String, Numeric, Boolean, Formula) */
private static String getCellValue(Cell cell) {
if (cell == null) return "";
CellType cellType = cell.getCellType();
switch (cellType) {
case STRING:
return cell.getStringCellValue().trim();
case NUMERIC:
// 处理日期格式
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
// 防止数字变成科学计数法,这里简单转 String
return String.valueOf(cell.getNumericCellValue()).trim();
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
// 递归获取公式计算后的值
return getCellValue(cell.getCachedFormulaResultType(), cell);
default:
return "";
}
}
/** 处理公式单元格的结果 */
private static String getCellValue(CellType cellType, Cell cell) {
if (cellType == CellType.NUMERIC) {
return String.valueOf(cell.getNumericCellValue()).trim();
} else if (cellType == CellType.STRING) {
return cell.getStringCellValue().trim();
} else {
return "";
}
}
}