[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;
    }
    
}

 

posted @ 2020-08-27 16:25  我爱我家喵喵  阅读(6739)  评论(1编辑  收藏  举报