一、流程圖
二.導出流程
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:
說明:實體類格式全字段輸出
說明:實體類格式自定義輸出字段
資源
資源已經上傳,結構如下圖: