自己寫了一個Pojo->Csv導出的工具類,讓這種操作更加簡單;
使用到的技術
- SpringEL+自定義註解+反射+緩存解決的問題
解決的問題:
- 以往拼寫csv數據格式,邏輯重複,代碼量大,寫起來很麻煩,改起來也很麻煩,核心邏輯不突出.
- csv字段顯示值:前端可能需要1,0這種狀態碼,而csv文件需要詳細的顯示值(比如男女).
這個時候可能就需要寫兩個構造方法或加參數來區分. - 同一個Bean或者VO在不同的場景需要的字段可能不是完全相同的,比如對於工人這個Bean,前臺展示關於身份證這種敏感的字段可能不展示,後臺就需要展示,如何滿足這種需求.
- csv輸出的標題和字段是分開的,標題和字段初始化順序稍有不慎就會出現張冠李戴…
package com.csvhelper.demo;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CsvColumn {
/**
* 該列數據的標題名
*
* @return String
*/
String title();
/**
* 排序規則,按照asc排序。如果不初始化該字段,將按照field定義的先後順序,
* 對field定義的先後順序強依賴是不健壯的,如果對順序苛求的場景,應初始化該字段。
*
* @return int
*/
int weight() default 0;
/**
* 通過的SpringEL表達,處理簡單自定義輸出的需求,
*
* @return String
*/
String springEL() default "";
/**
* 分組:同一個VO不同需求場景,在CSV文件中需要展示的字段可能存在不同,通過此字段區分
* 定義該字段後,想要對應方法生成的CSV中包含被註解的字段,必須在調用CSVUtil方法時加入該參數。
* 只有顯式聲明相應組的方法才【會】加入被註解字段
* @return String
*/
String doGroup() default "";
/**
* 分組:同一個VO不同需求場景,在CSV文件中需要展示的字段可能存在不同,通過此字段區分
* 定義該字段後,想要對應方法生成的CSV中剔除被註解的字段
* 只有顯式聲明相應組的方法才【不會】加入被註解字段
* @return String
*/
String unDoGroup() default "";
}
註解基本解決了以上說的問題.每個註解都有特定的意義,註釋很詳細.....下面看下具體實現
Model
package com.csvhelper.demo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* User: suyouliang
* Date: 3/31/19
* Time: 8:02 PM
* Description:工人
*/
@Data
public class Worker {
@CsvColumn(title = "姓名")
@ApiModelProperty("姓名")
private String name;
@CsvColumn(title = "年齡", weight = 2)
@ApiModelProperty("年齡")
private Integer age;
@CsvColumn(title = "性別", weight = 4, springEL = "sex==0?'女':'男'")
@ApiModelProperty("0:女,1:男")
private Integer sex;
@ApiModelProperty("生日")
@CsvColumn(title = "生日", weight = 3, springEL = "T(com.csvhelper.demo.DateUtil).getYMDMms(birthDay)")
private Date birthDay;
@CsvColumn(title = "身份證號", weight = 3, unDoGroup = "myGroup")
@ApiModelProperty("身份證號")
private String IdCard;
}
Controller
package com.csvhelper.demo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Created by suyouliang .
*/
@Controller
@RequestMapping("/api/test/v1/")
public class DemoController {
@GetMapping("/download-csv-normal")
@ResponseBody
public void downloadCsvNormal(HttpServletResponse response) {
List<Worker> workers = initData();
CsvUtil.sendDataStream(response, workers, "have_id_card", null);
}
@GetMapping("/download-csv-group")
@ResponseBody
public void downLoadCsvGroup(HttpServletResponse response) {
List<Worker> workers = initData();
CsvUtil.sendDataStream(response, workers, "no_id_card", "myGroup");
}
private List<Worker> initData() {
List<Worker> dataList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Worker worker = new Worker();
worker.setName("張" + i);
worker.setAge(10 + i);
worker.setSex(i % 2 == 0 ? 0 : 1);
worker.setBirthDay(new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 10 * i));
worker.setIdCard("345454198" + i + "xxxxxxx");
dataList.add(worker);
}
return dataList;
}
}
將需要輸出的List傳入即可.兩個方法,一個不指定分組,一個指定分組.用來模擬同一個VO不同場景可能需要的字段不一致的問題.
結果:
具體實現:
package com.csvhelper.demo;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
/**
* User: suyouliang
* Date: 2019/1/4
* Time: 4:39 PM
* Description: 使用SpringEL+自定義註解CsvColumn實現的Csv內容初始化通用工具
* 使用:
* 在需要導出的DTO或PO的field加上@CsvColumn,該field就會寫入csv
*/
@Slf4j
public class CsvUtil<T> {
/**
* CSV文件列分隔符
*/
private static final String CSV_COLUMN_SEPARATOR = ",";
/**
* CSV文件數據出現和分隔符相同時的替換字符(也可以轉譯)
*/
private static final String CSV_COLUMN_SEPARATOR_REPLACE = ".";
/**
* CSV文件換行符
*/
private static final String CSV_RN = "\r\n";
/**
* CSV文件名前綴
*/
private static final String FILE_PREFIX = "prefix_";
/**
* 被@CsvColumn註解的字段緩存
* key:Class+group
* value:Map<String, CsvColumn>
* key:filedName value:CsvColumn
*/
private static final Cache<String, Map<String, CsvColumn>> annotationMapCache = CacheBuilder.newBuilder().build();
/**
* csv文件標題行數據緩存
* key:Map<String, CsvColumn>對象的hashCode Hex
* value:Csv行數據
*/
private static final Cache<String, String> csvHeadLineCache = CacheBuilder.newBuilder().build();
static <T> void sendDataStream(HttpServletResponse response, List<T> dataList, String fileName, String group) {
Class dataClass = dataList.get(0).getClass();
final Map<String, CsvColumn> filedAnnotationMap = getFiledAnnotationMap(dataClass, group);
//3.拼接頭信息
StringBuilder builder = new StringBuilder();
builder.append(getCsvHeaderLine(filedAnnotationMap));
builder.append(CSV_RN);
//4.拼接數據列
dataList.forEach(obj -> builder.append(getCsvOneLine(filedAnnotationMap, obj, dataClass)));
//5.將數據寫入response流中
writeData(response, builder.toString(), fileName);
}
private static Map<String, CsvColumn> getFiledAnnotationMap(Class dataClass, String group) {
return Optional.ofNullable(annotationMapCache.getIfPresent(getAnnotationMapKey(dataClass, group)))
.orElseGet(() -> initFiledAnnotationMap(dataClass, group));
}
private static Map<String, CsvColumn> initFiledAnnotationMap(Class dataClass, String group) {
Map<String, CsvColumn> columnMap = new LinkedHashMap<>();
//1.查找帶有@CsvColumn註解的field,並裝入CsvColumnMap
Arrays.asList(dataClass.getDeclaredFields()).forEach(field -> {
CsvColumn annotation = field.getDeclaredAnnotation(CsvColumn.class);
if (annotation != null) {
field.setAccessible(true);
//剔除分組過濾的字段
if ((StringUtils.isNotEmpty(group) && StringUtils.isNotEmpty(annotation.doGroup()) && !annotation.doGroup().equals(group)) ||
(StringUtils.isNotEmpty(annotation.unDoGroup()) && annotation.unDoGroup().equals(group))) {
return;
}
columnMap.put(field.getName(), annotation);
}
});
//2.根據FileCsvColumn的weight屬性對CsvColumnMap進行排序.
final Map<String, CsvColumn> filedAnnotationMap = sortByValue(columnMap);
//3.加入緩存
annotationMapCache.put(getAnnotationMapKey(dataClass, group), filedAnnotationMap);
return filedAnnotationMap;
}
/**
* 拼接CVS表格一行數據
*
* @param filedAnnotationMap
* @return
*/
private static <T> String getCsvOneLine(Map<String, CsvColumn> filedAnnotationMap, T lineDate, Class dataClass) {
StringBuilder lineStrBuilder = new StringBuilder();
//1循環data,一個obj代表一行
filedAnnotationMap.forEach((key, value) -> {
//2循環filedAnnotationMap,一個Entity代表一列的數據
try {
Field field = dataClass.getDeclaredField(key);
field.setAccessible(true);
String dataColumn = Optional.ofNullable(field.get(lineDate)).orElse("").toString();
//3內容中存在","將和默認的csv間隔符衝突,這裏處理
dataColumn = dataColumn.replaceAll(CSV_COLUMN_SEPARATOR, CSV_COLUMN_SEPARATOR_REPLACE);
//4解析SpringEL表達式,處理自定義的輸出格式需求
if (StringUtils.isNotEmpty(value.springEL())) {
dataColumn = getSpringELValue(value.springEL(), lineDate);
}
lineStrBuilder.append(dataColumn);
lineStrBuilder.append(CSV_COLUMN_SEPARATOR);
} catch (NoSuchFieldException | IllegalAccessException e) {
log.error("CsvUtil根據反射操作屬性異常,異常信息{}", e);
}
});
return lineStrBuilder.append(CSV_RN).toString();
}
/**
* 獲取
*
* @param filedAnnotationMap
* @return String
*/
private static String getCsvHeaderLine(Map<String, CsvColumn> filedAnnotationMap) {
return StringUtils.join(Optional.ofNullable(csvHeadLineCache.getIfPresent(Integer.toHexString(filedAnnotationMap.hashCode())))
.orElseGet(() -> initCsvHeaderLine(filedAnnotationMap)), CSV_COLUMN_SEPARATOR);
}
/**
* 獲取annotationMapCache的key
*
* @param dataClass
* @param group
* @return
*/
private static String getAnnotationMapKey(Class dataClass, String group) {
return dataClass.getName().concat(Optional.ofNullable(group).orElse(""));
}
private static String initCsvHeaderLine(Map<String, CsvColumn> filedAnnotationMap) {
final String csvHeaderLineStr = StringUtils.join(filedAnnotationMap.values().stream()
.map(CsvColumn::title).collect(Collectors.toList()), CSV_COLUMN_SEPARATOR);
csvHeadLineCache.put(Integer.toHexString(filedAnnotationMap.hashCode()), csvHeaderLineStr);
return csvHeaderLineStr;
}
/**
* 根據csv的內容 使用HttpServletResponse 發送
*
* @param data csv內容
* @return
*/
private static void writeData(HttpServletResponse response, String data, String fileName) {
response.setContentType("application/csv");
response.setHeader("Content-Disposition", "attachment;filename=" + getRealCsvFileName(fileName) + ".csv");
data = "\ufeff".concat(data);
try {
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(data.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
private static String getSpringELValue(String springEL, Object sourceObj) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(springEL);
return exp.getValue(sourceObj) + "";
}
/**
* 根據FileColumn中的weight屬性爲Map<String, CsvColumn> map排序
*
* @param map 需要排序的map
* @return
*/
private static Map<String, CsvColumn> sortByValue(Map<String, CsvColumn> map) {
Map<String, CsvColumn> result = new LinkedHashMap<>();
map.entrySet().stream()
.sorted(Comparator.comparing(entry -> entry.getValue().weight()))
.forEach(e -> result.put(e.getKey(), e.getValue()));
return result;
}
private static String getRealCsvFileName(String fileName) {
fileName = StringUtils.isEmpty(fileName) ? "default" : fileName.trim();
return CsvUtil.FILE_PREFIX
.concat(fileName.replaceAll(" ", "-").concat("_"))
.concat(System.currentTimeMillis() + "");
}
}
其他相關類型:
package com.csvhelper.demo;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* User: suyouliang
* Date: 4/2/19
* Time: 8:43 PM
* Description:
*/
public class DateUtil {
/**
* 時間轉換格式yyyy年MM月dd日 HH:mm:ss
* @param date
* @return
*/
public static String getYMDMms(Date date) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
return formatter.format(date);
}
}
代碼環境使用springboot-starter 加個commons-lang3依賴就可以測試
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>