[Java] 基于SpringBoot的后端服务实现导出CSV数据流给前端下载
一、增加注解 @CsvField
将此注解加到 Bean 的字段上,控制导出过程中的序列化。
import java.lang.annotation.*; /** * Bean导出CSV选项注解 */ @Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CsvField { /** * 字段的标题 * @return */ String value() default ""; /** * 是否忽略此字段 * @return */ boolean ignore() default false; /** * 转换器,按需生成结果 * @return */ Class<? extends CsvConvertHandler> using() default CsvConvertHandler.None.class; }
添加 CsvConvertVisitable 接口
public interface CsvConvertVisitable { String convert(Object value); }
CsvConvertHandler 虚类
/** * @author yangyxd * @date 2020.08.27 14:39 */ public abstract class CsvConvertHandler<T extends Object> implements CsvConvertVisitable { @Override public String convert(Object value) { return this.get((T) value); } protected abstract String get(T value); public abstract static class None extends CsvConvertHandler { public None() { } } }
二、 实现 CsvHelper 工具类
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.util.ClassUtil; import xxx.support.annotation.CsvField; import xxx.support.annotation.converter.CsvConvertHandler; import xxx.support.annotation.converter.CsvConvertVisitable; import org.jetbrains.annotations.NotNull; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; /** * Csv 工具 * * @author yangyxd * @date 2020.08.27 09:37 */ public class CsvHelper { private final static String charset = "GBK"; private static class FieldData { Field field; CsvConvertVisitable converter; public FieldData(Field field, CsvConvertVisitable converter) { this.field = field; this.converter = converter; } } /** * 导出列表 CSV * @param items 要导出的数据列表 * @param os 输出到的流 * @param res HttpServletResponse(可选),如果指定了就会添加文件下载的头部 * @param fileName 可选,文件名,用户下载的文件名,传入 res 有效 * @param <T> * @throws IOException */ public static <T extends Object> void writeCsv(List<T> items, OutputStream os, HttpServletResponse res, String fileName) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, JsonProcessingException { if (res != null) { setHttpHeader(res, fileName); if (os == null) os = res.getOutputStream(); } if (os == null) return; if (items == null || items.size() < 1) { os.flush(); return; } // 筛选出拥有注解的字段 List<String> titles = new ArrayList<String>(); List<FieldData> csvFields = initCsvFields(items, titles); if (csvFields.size() < 1) { os.flush(); return; } // 写入数据 writeData(items, titles, csvFields, os); } // 设置下载用的 Http 响应头部 private static void setHttpHeader(HttpServletResponse res, String fileName) { fileName = StringUtils.isEmpty(fileName) ? (generateRandomFileName() + ".csv") : fileName; // res.setHeader("content-type", "application/octet-stream"); res.setHeader("content-type", "application/octet-stream; charset=" + charset); res.setContentType("application/octet-stream"); res.setHeader("Content-Disposition", "attachment; filename=" + fileName); } private static String generateRandomFileName() { return UUID.randomUUID().toString().replaceAll("-", ""); } // 初始化要输出的CSV字段 private static <T extends Object> List<FieldData> initCsvFields(List<T> items, List<String> titles) { Class<? extends Object> cls = items.get(0).getClass(); Field[] fields = cls.getDeclaredFields(); // 筛选出拥有注解的字段 List<FieldData> csvFields = new ArrayList<>(); for(int i=0;i< fields.length;i++){ CsvField item = fields[i].getAnnotation(CsvField.class); if (item == null || !item.ignore()) { CsvConvertVisitable converter = null; if (item.using() != null && item.using() != CsvConvertHandler.None.class) { converter = ClassUtil.createInstance(item.using(), true); } csvFields.add(new FieldData(fields[i], converter)); if (item == null) titles.add("\"" + fields[i].getName() + "\""); else titles.add("\"" + item.value() + "\""); } } return csvFields; } // 写入数据 private static <T extends Object> void writeData(List<T> items, List<String> titles, List<FieldData> csvFields, OutputStream os) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, JsonProcessingException { // 写入标题 String text = stringArrayToCsvLine(titles.toArray(new String[titles.size()])) + "\n"; byte[] buffer = text.getBytes(charset); long bufSize = buffer.length; os.write(buffer); // 写入内容 List<Method> methods = fieldToMethods(csvFields, items.get(0)); for (T item : items) { text = itemToString(item, methods, csvFields); if (text == null || text.isEmpty()) continue; buffer = text.getBytes(charset); bufSize = bufSize + buffer.length; os.write(buffer); if (bufSize > 4096) { os.flush(); bufSize = 0; } } if (bufSize > 0) os.flush(); } private static List<Method> fieldToMethods(List<FieldData> csvFields, Object item) throws NoSuchMethodException { List<Method> result = new ArrayList<Method>(); for (int i=0; i< csvFields.size(); i++) { String fieldName = csvFields.get(i).field.getName(); String methodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); Method method = item.getClass().getMethod(methodName, null); result.add(method); } return result; } private static String itemToString(Object item, @NotNull List<Method> methods, List<FieldData> csvFields) throws InvocationTargetException, IllegalAccessException, JsonProcessingException { String[] values = new String[methods.size()]; ObjectMapper objectMapper = new ObjectMapper(); for (int i=0; i< values.length; i++) { Method method = methods.get(i); FieldData field = csvFields.get(i); Object val = method.invoke(item, null); if (field.converter != null) { values[i] = objectMapper.writeValueAsString(field.converter.convert(val)); } else if (val == null) { values[i] = ""; continue; } else values[i] = objectMapper.writeValueAsString(val); if (values[i] == null || values[i].isEmpty()) continue; if (!values[i].isEmpty() && (values[i].startsWith("{") || values[i].startsWith("["))) values[i] = "\"" + values[i].replace("\"", "\"\"") + "\""; else values[i] = values[i].replace("\\\"", "\"\""); } return stringArrayToCsvLine(values) + "\n"; } public static String stringArrayToCsvLine(String[] text) { if (text == null) return ""; int iMax = text.length - 1; if (iMax == -1) return ""; StringBuilder b = new StringBuilder(); for (int i = 0; ; i++) { b.append(text[i]); if (i == iMax) return b.toString(); b.append(","); } } }
三、使用示例
@Controller @RequestMapping("/manage/merchant") @Validated public class MerchantController { /** 导出excel */ @GetMapping("/excel") @ResponseBody Object getExcelFile(HttpServletResponse res) throws Exception { try { List<MyBean> items = XXXService.getItems(); CsvHelper.writeCsv(items, res.getOutputStream(), res, null); } catch (IOException e) { return ResponseDTO.error(e.getMessage()); } return null; } }