在开发中我们经常遇到导出csv文件的需求,而数据是一个List,list里面是JavaBean对象。
下面是一个简单的示例
StringBuilder header = new StringBuilder();
header.append(" 媒体ID,广告消耗(元),备注\n");
for (SettledMonthReportRsp data : datas) {
StringBuilder row = new StringBuilder();
row.append(START_PREFIX);
//普通的数据
row.append(data.getAppId());
row.append(WORD_SEPARATOR);
//为保存金额的准确,数据库用整数,前台转成小数
row.append(DataTool.calculateFenToYuan(data.getAdvertConsume()));
row.append(WORD_SEPARATOR);
//有些需求我们在没有值时不显示null,需要特殊处理
row.append(ObjectUtils.defaultIfNull(data.getNote(),"-"));
row.append(END_POSTFIX);
header.append(row);
}
- 标题和内容没有对应起来,一不留神就会对应错字段
- 代码不直观,有很多模板代码
然后希望使用csv的库来简化代码,然后就选择了uniVocity-parsers 这个库,戳这里访问github地址。
1. 项目中引入uniVocity-parsers依赖
2. 使用
CsvWriterSettings settings = new CsvWriterSettings();
//这里需要设置处理的类型,就是JavaBean的类型
BeanWriterProcessor beanWriterProcessor = new BeanWriterProcessor<>(classType);
settings.setRowWriterProcessor(beanWriterProcessor);
//设置编码及输出
CsvWriter writer = new CsvWriter(out, Charset.forName("GBK"), settings);
//写标题
writer.writeHeaders();
//写内容
targetList.forEach(writer::processRecord);
writer.close();
- 上面的代码只需要换掉classType即可,你可能会好奇业务逻辑在哪里处理,实际上业务逻辑基本上都在JavaBean中(下面的代码和一开始的例子没有任何关系)。
public class SlotDataRsp {
@Parsed(field = "媒体ID")
private Long appId;
@Parsed(field = "广告位曝光", defaultNullWrite = "-")
private Long actExposeCount;
@Convert(conversionClass = PercentConversion.class)
@Parsed(field = "广告位点击率", defaultNullWrite = "-")
private Float actClickRate;
@Convert(conversionClass = FenToYuanConversion.class)
@Parsed(field = "广告消耗(元)", defaultNullWrite = "-")
private Long adConsume;
//省略setter和getter方法
}
下面解释一下上面的JavaBean:
@Parsed(field = "广告消耗(元)", defaultNullWrite = "-")
中field代表此字段对应的标题名称,defaultNullWrite代表此字段为null时该如何显示,这里需要注意的是,这里是使用get方法返回的值为null。
public String getActClickRate() {
return actClickRate+"%";
}
比如上面的get方法当launchSuccessRate为null是并不会为-,因为框架会判断返回为-,并不为null,当然我们也可以用这个特性做一些定制。
@Parsed还可以使用一个参数index,可以定义标题的顺序,从1开始。如果不定义,如上面的JavaBean,他会按照字段声明的先后顺序,一遍还是建议定义index参数。我之所以不定义index的原因在于,我有另一JavaBean继承了这个类,我需要在这个类的首位再添加一个当前日期的标题,如果定义了index,子类的字段只能在父类的后面了。
上面的actClickRate+"%"
中我需要将一个浮点型转成一个百分比,只要在后面加一个百分号即可,一开始我是使用上面的那种方法。上面那种方法维护性并不好,于是又找了一下,我发现我们可以实现Conversion类来自定义转换。
public class PercentConversion implements Conversion<String,String> {
//csv转JavaBean使用
@Override public String execute(String input) {
return null;
}
//导出使用
@Override public String revert(String input) {
if(NumberUtils.isNumeric(input)) {
return input + "%";
}else {
return input;
}
}
}
然后我就可以使用 @Convert(conversionClass = PercentConversion.class)
注解,这样逻辑就非常明了而且非常利于复用。上述代码当然也并不完美,我知道input肯定是一个数字,但是我将Conversion<String,String>
修改为Conversion<String,Long>
导出时会出错。
for (int i = conversions.size() - 1; i >= 0; i--) {
conversion = conversions.get(i);
value = conversion.revert(value);
}
我们看到conversions.size
,它先会用默认的转换成String,然后在调用你的自定义转换。所以我只能实现Conversion<String,String>
,目前也没有找到其他办法。
如果你仔细想了,为什么不直接返回return input + "%"
,我们需要关心其是不是数字,我们只要在其后拼上百分号即可。问题在于他会和@Parsed(field = "广告位点击率", defaultNullWrite = "-")
中的defaultNullWrite有冲突,如果input为null,则会返回null%
,这样不会显示-。
对于上面的代码,我们还可以进一步封装:
public static <T, U> void export(OutputStream out, List<T> sourceList, Class<U> classType) {
List<U> targetList = new ArrayList<>();
CsvWriterSettings settings = new CsvWriterSettings();
BeanWriterProcessor beanWriterProcessor = new BeanWriterProcessor<>(classType);
settings.setRowWriterProcessor(beanWriterProcessor);
CsvWriter writer = new CsvWriter(out, Charset.forName("GBK"), settings);
writer.writeHeaders();
if(sourceList==null){
writer.close();
return;
}
try {
for (T t : sourceList) {
U u = classType.newInstance();
BeanUtils.copyProperties(t, u);
targetList.add(u);
}
} catch (Exception e) {
throw new ExportException(ErrorCode.E9999999);
}
targetList.forEach(writer::processRecord);
writer.close();
}
我们使用的时候就可以像这样
if(req.getExportType()==0){
ExportTool.export(out, slotStatisticsDataList, SlotDataRsp.class);
}else {
ExportTool.export(out, slotStatisticsDataList, SlotDailyDataRsp.class);
}