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:

说明:实体类格式全字段输出

说明:实体类格式自定义输出字段

资源

资源已经上传,结构如下图:

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