Java使用Bean上加註解完成csv文件導出

自己寫了一個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>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章