自己写了一个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>