Administrator
Published on 2026-01-06 / 1 Visits
0
0

Excel导出接口的异常处理

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;

Comment