一、流程图
二.导出流程
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信息,*******20200708更正*******
final byte[] b = addStringEncode(csvFile.toString().getBytes(StandardCharsets.UTF_8));
// 设置文件格式
return new CsvFileInfo(b, b.length, fileExtName);
}
/**
* 添加字符串编码,防止中文乱码
* 20200708更正
*
* @param b 字符串流
* @return 编码后
*/
private static byte[] addStringEncode(byte[] b) {
byte[] bytes = new byte[b.length + 3];
// bytes[0] = (byte) 0xEF;
// bytes[1] = (byte) 0xBB;
// bytes[2] = (byte) 0xBF;
System.arraycopy(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}, 0, bytes, 0, 3);
System.arraycopy(b, 0, bytes, 3, b.length);
return bytes;
}
}
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:
说明:实体类格式全字段输出
说明:实体类格式自定义输出字段
资源
资源已经上传,结构如下图: