利用swagger注解解析對(duì)象數(shù)據(jù)生成csv文件

import io.swagger.annotations.ApiModelProperty;


import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * CSV文件,全稱(chēng)Comma-separated values珍特,就是逗號(hào)分隔的數(shù)據(jù)文件备闲。
 * CSV格式規(guī)范:
 * MS-DOS-style lines that end with (CR/LF) characters (optional for the last line)
 * Jiger: {使用回車(chē)換行(兩個(gè)字符)作為行分隔符檐束,最后一行數(shù)據(jù)可以沒(méi)有這兩個(gè)字符。}
 * An optional header record (there is no sure way to detect whether it is present, so care is required when importing).
 * Jiger:{標(biāo)題行是否需要饺著,要雙方顯示約定}.
 * Each record "should" contain the same number of comma-separated fields.
 * Jiger:{每行記錄的字段數(shù)要相同,使用逗號(hào)分隔。} 逗號(hào)是默認(rèn)使用的值饵较,雙方可以約定別的。
 * Any field may be quoted (with double quotes).
 * Jiger:{任何字段的值都可以使用雙引號(hào)括起來(lái)}. 為簡(jiǎn)單期間遭赂,可以要求都使用雙引號(hào)循诉。
 * Fields containing a line-break, double-quote, and/or commas should be quoted. (If they are not, the file will likely be impossible to process correctly).
 * Jiger:{字段值中如果有換行符,雙引號(hào)撇他,逗號(hào)的茄猫,必須要使用雙引號(hào)括起來(lái)狈蚤。這是必須的。}
 * A (double) quote character in a field must be represented by two (double) quote characters.
 * Jiger:{如果值中有雙引號(hào)划纽,使用一對(duì)雙引號(hào)來(lái)表示原來(lái)的一個(gè)雙引號(hào)}
 *
 * 根據(jù)swagger注解生成csv文件
 * @Author: xuebin.liu
 * @Date: 2021/5/30 14:17
 *
 */
public class CsvUtil {

    /**
     * 使用Map作為緩存(提高對(duì)象解析速度)脆侮,ThreadLocal 處理緩存的線程安全問(wèn)題
     */

    private static  ThreadLocal<ConcurrentHashMap<Class ,Field[]>> cacheHelper  = new ThreadLocal();

    /**
     * 根據(jù)csv格式規(guī)范處理csv單元格中特殊字符 " and ,
     * @param singleVal
     * @return
     */
    public static Object dealSpecialObject(Object singleVal) {
        if (singleVal == null) {
            return null;
        }
        if (singleVal instanceof String) {
            boolean hasSpecialChar = false;
            String val = (String) singleVal;
            if (val.contains("\"")) {
                val = val.replaceAll("\"", "\"\"");
                hasSpecialChar = true;
            } else if (val.contains(",")) {
                hasSpecialChar = true;
            } else if (val.contains("\r") || val.contains("\n")) {
                hasSpecialChar = true;
            }
            return hasSpecialChar ? "\"".concat(val).concat("\"") : val;
        } else {
            return singleVal;
        }
    }

    /**
     * 自動(dòng)追加換行符
     * @param sb
     */

    public static void line (StringBuilder sb ,String lineContent){
        sb.append(lineContent).append("\r\n");
    }

    /**
     * 新增空白行
     * @param sb
     */
    public static void line (StringBuilder sb ){
        sb.append(",,").append("\r\n");
    }

    /**
     * 新增單元格
     * @param sb
     * @param data
     */
    public static void cell (StringBuilder sb ,Object data){
        sb.append(dealSpecialObject(data)).append(",");
    }

    /**
     * 列表數(shù)據(jù)為空,展示空白行
     * @param sb
     */
    private static void lineAsEmptyList(StringBuilder sb){
        sb.append(",列表無(wú)數(shù)據(jù),").append("\r\n");
    }

    /**
     * 創(chuàng)建表頭
     * @param clazz
     * @param sb
     */
    private static void buildHeader(Class<?> clazz ,StringBuilder sb){
        Field[] fields = clazz.getDeclaredFields();
        CsvUtil.buildHeader(Arrays.asList(fields) , sb);
    }

    /**
     * 創(chuàng)建表頭
     * @param fields
     * @param sb
     */
    private static void buildHeader(List<Field> fields ,StringBuilder sb){
        StringBuilder headerLine = new StringBuilder();
        for (Field objField : fields) {
            ApiModelProperty properties = objField.getAnnotation(ApiModelProperty.class);
            String propertyName = properties.value();
            CsvUtil.cell(headerLine, propertyName);
        }
        CsvUtil.line(sb, headerLine.toString());
    }

    /**
     * 獲取對(duì)象的字段勇劣,并進(jìn)行列表字段優(yōu)先排序
     * @param value
     * @return
     */
    private static Field[] getObjFields (Object value){
        Class clazz = value.getClass();
       if(cacheHelper.get().containsKey(clazz)){
            return cacheHelper.get().get(clazz);
        }else{
           Field[] fields = value.getClass().getDeclaredFields();
           Arrays.sort(fields,((o1, o2) -> {
               if(o1.getType().getClass().equals(List.class)){
                   return 1 ;
               }
               return 0;
           }));
           cacheHelper.get().putIfAbsent(clazz,fields);
           return fields;
        }
    }

    /**
     * 處理普通對(duì)象,普通對(duì)象的字段中包含List也可處理
     * @param value
     * @param sb
     * @param isNeedHeader
     */
    private static void processObj(Object value,StringBuilder sb,boolean isNeedHeader){
        Field[] fields = getObjFields(value);
        StringBuilder bodyLine = new StringBuilder();
        List<Field> noListField = new ArrayList<>();
        for(Field objField :fields){
            String fieldName = objField.getName();
            Object fieldVal  = ReflectUtil.getFieldValueByName(value,fieldName);

            if(fieldVal instanceof List){
                processList( fieldVal, sb);
                continue;
            }
            noListField.add(objField);
            CsvUtil.cell(bodyLine,fieldVal);

        }

        if(isNeedHeader){
            buildHeader(noListField , sb);
        }
        CsvUtil.line(sb,bodyLine.toString());
    }

    /**
     * 處理列表對(duì)象
     * @param value
     * @param sb
     */

    private static void processList(Object value,StringBuilder sb){
        int count = 0;
        if(value==null ||((List)value).size()<=0){
            CsvUtil.lineAsEmptyList(sb);
        }
        for( Object instance  :(List)value){
            if(count == 0){
                buildHeader(instance.getClass() , sb);
            }
            processObj( instance, sb,false);
            count ++;
        }
    }


    /**
     * 處理Map里面的數(shù)據(jù)
     * @param value
     * @param sb
     */
    private  static void processVal(Object value,StringBuilder sb){
        if(value instanceof  List){
            processList( value, sb);
            // todo
        }else{
            processObj( value, sb,true);
        }

    }

    /**
     * 根據(jù)數(shù)據(jù)集配合swagger注解獲取CSV文件內(nèi)容
     * @param data
     * @return
     */
    public static String  getCsvContent(LinkedHashMap<String,Object>  data) {
        StringBuilder content = new StringBuilder();
        try {
            CsvUtil.initCache();
            data.entrySet().forEach((entry) -> {
                CsvUtil.line(content, (String) dealSpecialObject(entry.getKey()));
                processVal(entry.getValue(), content);
                CsvUtil.line(content);
            });
        } finally {
            CsvUtil.releaseCache();
        }
        return content.toString();
    }


    /**
     * 下載csv文件靖避,文件名不需要.csv后綴,字符編碼處理成GBK,防止在MS-EXCEL中字符亂碼
     * @param fileName
     * @param content
     * @param response
     */
    public static void downLoadCsv(String fileName,String content, HttpServletResponse response) {
        try {
            response.setCharacterEncoding("UTF-8");
            response.setHeader("content-Type","application/vnd.ms-excel;charset=utf-8");
            response.setHeader("Content-Disposition",
                    "attachment;filename=" + URLEncoder.encode( fileName + ".csv", "UTF-8"));
            response.getOutputStream().write(content.getBytes("GBK"));
            response.getOutputStream().flush();
            response.getOutputStream().close();
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 初始化當(dāng)前線程的ThreadLocal變量
     */
    private static void initCache(){
        cacheHelper.set(new ConcurrentHashMap<Class ,Field[]>());
    }

    /**
     * 釋放內(nèi)存數(shù)據(jù),防止OOM
     */
    private static void releaseCache(){
        cacheHelper.get().clear();
        cacheHelper.remove();
    }

使用示例

 List<TimeNodeVo> getGmvTrend(DashboardQueryDto dto);
@Data
@ApiModel(value="時(shí)間節(jié)點(diǎn)通用")
@Builder
public class TimeNodeVo {

    @ApiModelProperty(value = "時(shí)間")
    private String time ;

    @ApiModelProperty(value = "數(shù)值")
    private BigDecimal num ;

}
LinkedHashMap<String ,Object> kvs = new LinkedHashMap<>(16);
kvs.put("4.流水趨勢(shì)",learnService.getGmvTrend(dto));
String content =  CsvUtil.getCsvContent(kvs);
CsvUtil.downLoadCsv(fileName,content,response);

結(jié)果


屏幕截圖 2021-06-02 145156.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市比默,隨后出現(xiàn)的幾起案子幻捏,更是在濱河造成了極大的恐慌,老刑警劉巖命咐,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篡九,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡侈百,警方通過(guò)查閱死者的電腦和手機(jī)瓮下,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)钝域,“玉大人讽坏,你說(shuō)我怎么就攤上這事±ぃ” “怎么了路呜?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)织咧。 經(jīng)常有香客問(wèn)我胀葱,道長(zhǎng),這世上最難降的妖魔是什么笙蒙? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任抵屿,我火速辦了婚禮,結(jié)果婚禮上捅位,老公的妹妹穿的比我還像新娘轧葛。我一直安慰自己,他們只是感情好艇搀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布尿扯。 她就那樣靜靜地躺著,像睡著了一般焰雕。 火紅的嫁衣襯著肌膚如雪衷笋。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天矩屁,我揣著相機(jī)與錄音辟宗,去河邊找鬼爵赵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛慢蜓,可吹牛的內(nèi)容都是我干的亚再。 我是一名探鬼主播郭膛,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼晨抡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了则剃?” 一聲冷哼從身側(cè)響起耘柱,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棍现,沒(méi)想到半個(gè)月后调煎,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡己肮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年士袄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谎僻。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡娄柳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出艘绍,到底是詐尸還是另有隱情赤拒,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布诱鞠,位于F島的核電站挎挖,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏航夺。R本人自食惡果不足惜蕉朵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望阳掐。 院中可真熱鬧始衅,春花似錦、人聲如沸锚烦。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)涮俄。三九已至蛉拙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間彻亲,已是汗流浹背孕锄。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工吮廉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人畸肆。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓宦芦,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親轴脐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子调卑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容