java數據導出Excel的CSV格式、自定義字段導出字段

一、流程圖

二.導出流程

2.1導出csv byte數據流工具類

package com.geo.source.csv;

import com.geo.source.csv.annotation.CsvCell;
import com.geo.source.csv.annotation.CsvRow;
import com.geo.source.csv.dto.CsvFileInfo;
import com.geo.source.csv.service.FieldAdaptor;
import com.geo.source.csv.service.impl.DefaultFieldAdaptorImpl;
import org.apache.commons.collections.CollectionUtils;
import org.apache.http.util.Asserts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;

/**
 * 生成csv文件
 *
 * @author YanZhen
 * @date 2020/03/16 11:04
 **/
public class CsvClient {
    private static Logger log = LoggerFactory.getLogger(CsvClient.class);

    /**
     * csv導出最大行數
     */
    public static final int MAX_ROW_NUMBER = 100_000;

    /**
     * 數據庫一次輸出最大條數
     */
    public static final int MAX_SQL_NUMBER = 1000;

    /**
     * Excel正確顯示數字的最大位數
     */
    public static final int MAX_EXCEL_SHOW_BITS = 11;

    /**
     * 純數字正則表達式
     */
    public static final String NUMBER_EXPRESSION = "^[0-9]*$";

    /**
     * 獲取csv文件流
     *
     * @param ts  數據源
     * @param <T> 數據源類型
     * @return csv文件流
     */
    public static <T> CsvFileInfo getCsvByte(List<T> ts) {
        return getCsvByte(ts, null);
    }

    /**
     * 獲取csv文件信息
     *
     * @param <T>      ts 數據源
     * @param fileName 文件名
     * @param titleMap 標題名
     * @return 編碼後字符串
     */
    public static <T> CsvFileInfo getCsvByteByMap(List<Map<String, T>> ts, String fileName, Map<String, String> titleMap) {
        return doExportByMap(ts, fileName, titleMap);
    }

    /**
     * 獲取csv文件流
     *
     * @param ts       數據源
     * @param fieldNos 數據源中需要篩選的字段編號
     * @param <T>      數據源類型
     * @return csv文件流
     */
    public static <T> CsvFileInfo getCsvByte(List<T> ts, List<Short> fieldNos) {
        if (ts == null || ts.isEmpty()) {
            log.warn("csv導出失敗,無數據導出!");
            return null;
        } else if (ts.size() > MAX_ROW_NUMBER) {
            log.warn("csv導出失敗,數據量太大(最大十萬條,實際{}條)!", ts.size());
            return null;
        }

        // 對象
        final Class<?> tClazz = ts.get(0).getClass();

        // 獲取文件名
        String fileName;
        // 擴展名
        String ext = ".csv";
        // 文件全稱
        String fileExtName;
        if (tClazz.isAnnotationPresent(CsvRow.class)) {
            final CsvRow csvRow = tClazz.getDeclaredAnnotation(CsvRow.class);
            fileName = csvRow.value() + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
            fileExtName = fileName + ext;
        } else {
            throw new IllegalArgumentException("類上缺少@CsvRow註釋");
        }

        // 獲取字段值
        final List<Field> fields = getFields(fieldNos, tClazz);
        if (fields == null || fields.isEmpty()) {
            throw new IllegalArgumentException("字段上缺少@CsvCell註釋");
        }

        final StringJoiner csvFile = new StringJoiner("\r\n");
        // csv數據標題
        final StringJoiner titleRow = new StringJoiner(",");
        for (Field field : fields) {
            titleRow.add(field.getDeclaredAnnotation(CsvCell.class).title());
        }
        csvFile.merge(titleRow);

        // 生成csv格式的數據
        for (T t : ts) {
            final StringJoiner row = new StringJoiner(",");
            for (Field field : fields) {
                field.setAccessible(true);
                row.add(getFieldValue(t, field));
            }
            csvFile.merge(row);
        }

        final byte[] b = csvFile.toString().getBytes(StandardCharsets.UTF_8);

        // 賦值文件信息
        return new CsvFileInfo(b, b.length, fileExtName);
    }

    /**
     * 獲取對象中的字段值,可篩選對象中的字段(需要使用@CsvCell註解,指定字段編號)
     *
     * @param fieldNos 字段編號
     * @param tClazz   對象類
     * @return 篩選後字段
     */
    private static List<Field> getFields(List<Short> fieldNos, Class<?> tClazz) {
        final Field[] declaredFields = tClazz.getDeclaredFields();
        final List<Field> fields;
        // 字段全部顯示
        if (fieldNos == null || fieldNos.isEmpty()) {
            fields = Arrays.stream(declaredFields)
                    .filter(field -> field.isAnnotationPresent(CsvCell.class))
                    .sorted(Comparator.comparingInt(o -> o.getDeclaredAnnotation(CsvCell.class).index()))
                    .collect(Collectors.toList());

            // 自定義顯示的字段
        } else {
            fields = Arrays.stream(declaredFields)
                    .filter(field -> field.isAnnotationPresent(CsvCell.class) && fieldNos.contains(field.getDeclaredAnnotation(CsvCell.class).fieldNo()))
                    .sorted(Comparator.comparingInt(o -> o.getDeclaredAnnotation(CsvCell.class).index()))
                    .collect(Collectors.toList());
        }
        return fields;
    }

    /**
     * 獲取對象的字段值
     *
     * @param <T>   t 數據源對象
     * @param field 對象的字段信息
     * @return 字段對應的值
     */
    private static <T> String getFieldValue(T t, Field field) {
        try {
            final CsvCell csvCell = field.getDeclaredAnnotation(CsvCell.class);
            final FieldAdaptor fieldAdaptor = csvCell.valueAdaptor().newInstance();
            String value = fieldAdaptor.process(field.get(t));

            // csv都轉成字符串格式
            return symbolManipulation(value);
        } catch (IllegalAccessException | InstantiationException e) {
            log.error("字段獲取錯誤!", e);
            return "";
        }
    }

    /**
     * 字段值中的格式處理
     *
     * @param value 字段值
     * @return 處理後的格式
     */
    private static String symbolManipulation(String value) {
        final String s = "\"";
        final String d = ",";
        final boolean b = value.contains(s);
        // 有雙引號時要替換成兩個
        if (b) {
            final String s1 = "\"\"";
            value = value.replace(s, s1);
        }

        // 有英文逗號、雙引號時要兩邊加雙引號
        if (b || value.contains(d)) {
            value = s + value + s;
        }
        return value;
    }

    /**
     * 針對Map對象的導出
     *
     * @param ts       數據源
     * @param fileName 文件名
     * @param titleMap 標題信息
     * @param <T>      只支持進本類型,(Number、String、Date、..)
     * @return CSV的文件信息
     */
    private static <T> CsvFileInfo doExportByMap(List<Map<String, T>> ts, String fileName, Map<String, String> titleMap) {
        Asserts.notNull(titleMap, "Title");
        fileName = Objects.toString(fileName, "");
        if (CollectionUtils.isEmpty(ts)) {
            log.warn("csv導出失敗,無數據導出!");
            return null;
        } else if (ts.size() > MAX_ROW_NUMBER) {
            log.warn("csv導出失敗,數據量太大(最大十萬條,實際{}條)!", ts.size());
            return null;
        }

        // 擴展名
        String ext = ".csv";
        // 文件名
        fileName += new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        // 文件全稱
        String fileExtName = fileName + ext;

        // csv文件內容
        final StringJoiner csvFile = new StringJoiner("\r\n");

        // csv數據標題
        // key值
        final Set<String> keys = ts.get(0).keySet();
        final StringJoiner titleRow = new StringJoiner(",");
        for (String field : keys) {
            titleRow.add(titleMap.get(field));
        }
        csvFile.merge(titleRow);

        // 生成csv格式的數據
        FieldAdaptor adaptor = new DefaultFieldAdaptorImpl();
        for (Map<String, T> t : ts) {
            final StringJoiner row = new StringJoiner(",");
            t.forEach((k, v) -> row.add(symbolManipulation(adaptor.process(v))));
            csvFile.merge(row);
        }

        // 文件的byte信息
        final byte[] b = csvFile.toString().getBytes(StandardCharsets.UTF_8);

        // 設置文件格式
        return new CsvFileInfo(b, b.length, fileExtName);
    }
}

2.2.註解

2.2.1csv文件名和行數據的代表註釋

package com.geo.source.csv.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 導出csv文件的註釋
 * @author YanZhen
 * @date 2020/01/20 15:19
 **/
@Target(TYPE)
@Retention(RUNTIME)
public @interface CsvRow {

    /**
     * 給出csv的文件名
     * @return 文件名
     */
    String value() default "";
}

2.2.2 各個行中每個格的值和標題註釋

package com.geo.source.csv.annotation;

import com.geo.source.csv.service.FieldAdaptor;
import com.geo.source.csv.service.impl.DefaultFieldAdaptorImpl;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 字段註釋,代表標題及註釋
 * @author YanZhen
 * @date 2020/01/20 15:32
 **/
@Target(FIELD)
@Retention(RUNTIME)
public @interface CsvCell {
    /**
     * 列名
     * @return 名稱
     */
    String title() default "";

    /**
     * 所在位置順序,用於排序
     * @return 索引
     */
    short index() default 0;

    /**
     * 該字段的數字編號,用於字段的自定義導出
     * @return 字段編號
     */
    short fieldNo() default 0;

    /**
     * 字段值自定義處理器
     */
    Class<? extends FieldAdaptor> valueAdaptor() default DefaultFieldAdaptorImpl.class;
}

2.3.文件內容dto

package com.geo.source.csv.dto;

import lombok.Data;

import java.util.Base64;

/**
 * 上傳fastDfs的文件信息
 * @author yan zhen
 * @date 2020/02/10 19:08
 */
@Data
public class CsvFileInfo {

    /**
     * 文件流
     */
    private byte[] fileContent;

    /**
     * base64文件流
     */
    private String fileContentEncode;

    /**
     * 文件大小
     */
    private long fileSize;

    /**
     * 文件名
     */
    private String fileName;

    public CsvFileInfo(byte[] fileContent, long fileSize, String fileName) {
        this.fileContent = fileContent;
        this.fileSize = fileSize;
        this.fileName = fileName;
        this.fileContentEncode = Base64.getEncoder().encodeToString(fileContent);
    }
}

2.4.行中各個格的值的格式控制

對每個字段進行格式化,展示出用戶想要的格式,也可以自定義實現,只需在對象字段上註明實現類即可!

2.4.1接口

package com.geo.source.csv.service;

/**
 * @author YanZhen
 * @date 2020/03/16 11:11
 **/
@FunctionalInterface
public interface FieldAdaptor {
    /**
     * 處理字段
     * @param <T> 字段值類型
     * @param t 字段值
     * @return 返回字段值的字符串類型
     */
    <T> String process(T t);
}

2.4.2實現

package com.geo.source.csv.service.impl;

import com.geo.source.csv.CsvClient;
import com.geo.source.csv.service.FieldAdaptor;
import org.apache.commons.lang3.StringUtils;

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Objects;

/**
 * @author YanZhen
 * @date 2020/03/16 11:20
 **/
public class DefaultFieldAdaptorImpl implements FieldAdaptor {
    @Override
    public <T> String process(T t) {
        String s;
        if (t instanceof String) {
            s = (String) t;
        } else {
            // 時間類型默認轉格式
            final String pattern = "yyyy-MM-dd HH:mm:ss";
            if (t instanceof Date) {
                s = new SimpleDateFormat(pattern).format(t);
            } else if (t instanceof LocalDateTime) {
                s = ((LocalDateTime) t).format(DateTimeFormatter.ofPattern(pattern));
            } else {
                s = Objects.toString(t, "");
            }
        }
        s = Objects.toString(s, "");

        // 校驗是否全爲數字,默認前面添加單引號以在Excel中正確顯示
        if (StringUtils.isNotBlank(s) && s.length() > CsvClient.MAX_EXCEL_SHOW_BITS) {
            final String regex = CsvClient.NUMBER_EXPRESSION;
            if (s.matches(regex)) {
                s = "'" + s;
            }
        }
        return s;
    }
}

三.測試

package com.geo.source.csv;

import com.geo.source.csv.annotation.CsvCell;
import com.geo.source.csv.annotation.CsvRow;
import com.geo.source.csv.dto.CsvFileInfo;
import lombok.Data;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author YanZhen
 * @date 2020/03/16 11:45
 **/
public class CsvTest {
    /**
     * 模擬數據庫
      */
    static Map<String, Integer> DATA_MAP = new HashMap<String, Integer>() {{
        put("k1", 1);
        put("k2", 2);
        put("k3", 3);
        put("k4", 4);
        put("k5", 5);
    }};

    public static void main(String[] args) {
//        method1();
        method2();
    }

    /**
     * 針對數據源是map的接口
     */
    private static void method2() {
        // 獲取數據庫數據
        final List<Map<String, Object>> csvObjs = new ArrayList<>();
        DATA_MAP.forEach((s, o) -> {
            final Map<String, Object> obj = new LinkedHashMap<>(2);
            obj.put("k", s);
            obj.put("v", o);
            csvObjs.add(obj);
        });

        Map<String, String> titleMap = new HashMap<>(2);
        titleMap.put("k", "鍵");
        titleMap.put("v", "值");

        // 導出
        // byte數據
        final CsvFileInfo csvInfo = CsvClient.getCsvByteByMap(csvObjs, "今天", titleMap);
        // 寫入文件
        putFile(csvInfo);
    }

    /**
     * 字段測試
     */
    private static void method1() {
        // 獲取數據庫數據
        final List<CsvObj> csvObjs = new ArrayList<>();
        DATA_MAP.forEach((s, o) -> {
            final CsvObj obj = new CsvObj();
            obj.setK(s);
            obj.setV(o);
            csvObjs.add(obj);
        });

        // 全字段輸出文件
        csvExport1(csvObjs);
        try {
            TimeUnit.MILLISECONDS.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // k字段輸出文件
        csvExport2(csvObjs);
    }

    /**
     * 自定義字段導出
     * @param csvObjs 數據源
     */
    private static void csvExport2(List<CsvObj> csvObjs) {
        // 獲取csv文件流
        final CsvFileInfo csvInfo = CsvClient.getCsvByte(csvObjs, Collections.singletonList((short) 1));
        if (csvInfo == null) {
            System.out.println("No data");
            return;
        }

        // 寫入文件
        putFile(csvInfo);
    }

    /**
     * 全字段導出
     * @param csvObjs 數據源
     */
    private static void csvExport1(List<CsvObj> csvObjs) {
        // 獲取csv文件流
        final CsvFileInfo csvInfo = CsvClient.getCsvByte(csvObjs);
        if (csvInfo == null) {
            System.out.println("No data");
            return;
        }

        // 寫入文件
        putFile(csvInfo);
    }

    /**
     * 寫入文件
     * @param csvInfo 文件內容
     */
    private static void putFile(CsvFileInfo csvInfo) {
        try (final FileChannel channel = new FileOutputStream("D:/test/" + csvInfo.getFileName(), true).getChannel()) {
            final ByteBuffer byteBuffer = ByteBuffer.wrap(csvInfo.getFileContent());
            channel.write(byteBuffer);
        } catch (FileNotFoundException e) {
            System.err.println("寫入異常:");
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Data
    @CsvRow("測試數據")
    static
    class CsvObj {
        @CsvCell(title = "鍵", index = 1, fieldNo = 1)
        private String k;
        @CsvCell(title = "值", index = 2, fieldNo = 2)
        private Integer v;
    }
}

結果:

方法method1:

說明:Map格式數據導出

 方法method2:

說明:實體類格式全字段輸出

說明:實體類格式自定義輸出字段

資源

資源已經上傳,結構如下圖:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章