Excel导出接口的异常处理
前言:
在 Spring Boot 项目中,若希望通过自定义注解 + AOP的方式统一处理 Excel 导出接口的异常与正常下载逻辑,可以按照以下思路实现:
✅ 目标
导出业务逻辑抛异常时:返回 JSON 格式的错误信息(供前端 Vue 弹框提示);
无异常时:直接触发浏览器下载 Excel 文件;
通过一个自定义注解(如 @ExcelExport)标注在导出接口上,自动应用上述逻辑;
适用于大量导出接口,避免重复代码
1.添加注解
ExcelExport.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelExport {
String value() default "";
}
2.添切面拦截注解
ExcelExportAspect.java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
@Aspect
@Component
@Order(1) // 确保优先级高,早于其他拦截器
public class ExcelExportAspect {
@Around("@annotation(excelExport)")
public Object handleExcelExport(ProceedingJoinPoint joinPoint, ExcelExport excelExport) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return joinPoint.proceed(); // 非 Web 环境,直接执行
}
HttpServletResponse response = attributes.getResponse();
if (response == null) {
return joinPoint.proceed();
}
try {
// 执行目标方法
Object result = joinPoint.proceed();
// 假设导出方法返回 byte[] 或 void(通过 response.getOutputStream() 写入)
// 这里我们约定:导出方法必须自己写入 response.getOutputStream()
// 所以切面不处理 result,只负责异常捕获和 headers 设置
return result;
} catch (Exception e) {
// 捕获业务异常,返回 JSON 错误
handleException(response, e);
return null; // 不再继续处理
}
}
private void handleException(HttpServletResponse response, Exception e) {
response.reset();
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Cache-Control", "no-cache");
try (OutputStream os = response.getOutputStream()) {
String errorMsg = "{\"code\":500,\"message\":\"" + escapeJson(e.getMessage()) + "\"}";
os.write(errorMsg.getBytes("UTF-8"));
os.flush();
} catch (IOException ioEx) {
throw new RuntimeException("Failed to write error response", ioEx);
}
}
// 简单转义 JSON 字符串(实际建议用 Jackson)
private String escapeJson(String str) {
if (str == null) return "";
return str.replace("\"", "\\\"").replace("\n", "\\n");
}
}
3.Controller 导出接口(示例)
ExportController.java
@RestController
@RequestMapping("/export")
public class ExportController {
@ExcelExport
@GetMapping("/user")
public void exportUserExcel(HttpServletResponse response) {
// 1. 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=user.xlsx");
// 2. 业务校验(可能抛异常)
if (someCondition()) {
throw new IllegalArgumentException("用户数据为空,无法导出!");
}
// 3. 生成 Excel 并写入输出流
try (OutputStream os = response.getOutputStream()) {
// 使用 EasyExcel / Apache POI 生成 Excel
// 示例:EasyExcel.write(os, User.class).sheet("用户").doWrite(userDataList);
os.flush();
} catch (IOException e) {
throw new RuntimeException("导出失败", e);
}
}
}
4.前端vue处理(示例)
// 使用 axios 下载文件(需设置 responseType: 'blob')
axios({
method: 'get',
url: '/export/user',
responseType: 'blob'
})
.then(response => {
// 正常下载
const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'user.xlsx';
link.click();
})
.catch(error => {
// 判断是否是业务错误(JSON)
if (error.response && error.response.status === 500) {
// 尝试解析错误信息
const reader = new FileReader();
reader.onload = () => {
try {
const err = JSON.parse(reader.result);
alert(err.message || '导出失败');
} catch (e) {
alert('导出发生未知错误');
}
};
reader.readAsText(error.response.data);
} else {
alert('网络错误');
}
});
5.前端vue使用Axios方式
在 response interceptor 中判断响应类型
第一步:确保 Axios 请求配置正确
// api/export.js
export function downloadUserExcel() {
return request({
url: '/export/user',
method: 'get',
responseType: 'blob', // ⚠️ 关键!告诉 Axios 期待二进制数据
});
}
第二步:在 response interceptor 中智能处理
// utils/request.js 或 main.js 中的 axios 配置
import axios from 'axios';
import { ElMessage } from 'element-plus'; // 或你用的 UI 库(如 antd message)
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 60000,
});
// 请求拦截器(可选)
service.interceptors.request.use(config => {
// ...
return config;
});
// 响应拦截器 👇
service.interceptors.response.use(
(response) => {
const { data, config } = response;
// 只处理 responseType === 'blob' 的请求(即文件下载类)
if (config.responseType === 'blob') {
// 检查是否真的是文件(而非错误 JSON)
if (data.type === 'application/json') {
// 情况:后端返回了 JSON 错误,但 responseType 是 blob
// 需要读取 blob 内容并解析
const reader = new FileReader();
reader.readAsText(data, 'utf-8');
return new Promise((resolve, reject) => {
reader.onload = () => {
try {
const errJson = JSON.parse(reader.result);
ElMessage.error(errJson.message || '导出失败');
reject(new Error(errJson.message || 'Export failed'));
} catch (e) {
ElMessage.error('导出发生未知错误');
reject(new Error('Unknown export error'));
}
};
reader.onerror = () => {
ElMessage.error('读取错误信息失败');
reject(new Error('Read error failed'));
};
});
} else {
// 正常文件流,交还给调用方处理下载
return response; // 调用方自己触发 download
}
}
// 非 blob 请求,正常返回
return response;
},
(error) => {
// 网络错误、超时、4xx/5xx(非 blob 场景)
ElMessage.error(error.message || '请求失败');
return Promise.reject(error);
}
);
export default service;