Spring AOP + 自定义注解 实现千万Excel数据统一导出功能(CSV无格式数据)

1.自定义注解3个

1.ExportEntity

/**
 * @Author: chenxiaoqing9  微信:weixin1398858069
 * @Date: Created in 2019/1/16
 * @Description: 导出实体注解
 * @Modified by:
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportEntity {
}

2.ExportHandler

/**
 * @Author: chenxiaoqing9
 * @Date: Created in 2019/1/15
 * @Description: 方法注解
 * @Modified by:
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportHandler {
    Class value();
}

3.ExportParam

/**
 * @Author: chenxiaoqing9
 * @Date: Created in 2019/1/16
 * @Description: 导出实体的字段注解
 * @Modified by:
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportParam {
    String value();
    int length() default 0; /*字段限制的长度*/
    String pattern() default ""; /*正则过滤*/
    String dateFormat() default ""; /*时间格式过滤*/
}

2.导出CSV工具类 CSVUtils.java


/**
 * @Author: chenxiaoqing9
 * @Date: Created in 2019/1/17
 * @Description: 导出CSV工具类
 * @Modified by:
 */
@Slf4j
public class CSVUtils {

    /**
     * 取得导出内容的最终值
     *
     * @return 最终值
     */
    public static String getCSVContent(CSVExportConfig exportConfig) {

        boolean isFirstRow = isFirstRow(exportConfig);
        StringBuilder sb = new StringBuilder();
        if (isFirstRow) {//第一条数据添加头部信息
            sb.append(exportConfig.getHeader()).append("\r\n");
        }
        List<String> rows = exportConfig.getRows();
        Iterator<String> iterator = rows.iterator();
        for (int i = 0; i < exportConfig.MAX_CACHE_ROW_COUNT; i++) {
            if (iterator.hasNext()) {
                sb.append(iterator.next()).append("\r\n");
                iterator.remove();
            } else {
                break;
            }
        }
        return sb.toString();
    }

    public static boolean isFirstRow(CSVExportConfig exportConfig) {
        Collection data = exportConfig.getData();
        List<String> rows = exportConfig.getRows();
        return data.size() == rows.size();
    }

    /**
     * 获取头部信息并设置字段跟字段取值方法Map
     *
     * @return 头部信息
     * @throws NoSuchMethodException
     */
    public static void setHeaderAndFieldParams(CSVExportConfig exportConfig) throws NoSuchMethodException {
        Class clazz = exportConfig.getClazz();
        Field[] declaredFields = clazz.getDeclaredFields();
        Map<String, ExportParam> fieldFormatter = exportConfig.getFieldFormatter();
        Map<String, Method> fieldMethods = exportConfig.getFieldMethods();

        StringBuilder sb = new StringBuilder();
        for (Field each : declaredFields) {
            ExportParam exportParam = each.getAnnotation(ExportParam.class);
            if (null != exportParam) {
                String fieldName = each.getName();
                String eachHeader = exportParam.value();
                String methodName = "get" + firstOneToUpperCase(fieldName);
                //设置
                fieldMethods.put(fieldName, clazz.getMethod(methodName));
                fieldFormatter.put(fieldName, exportParam);

                sb.append(eachHeader).append(",");
            }
        }
        //装配头部String
        String tempHeader = sb.toString();
        exportConfig.setHeader(tempHeader.substring(0, tempHeader.length() - 1));
    }

    /**
     * 获取row 内容
     *
     * @return 行内容
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    public static void setRowDataByFieldMethods(CSVExportConfig exportConfig) throws InvocationTargetException, IllegalAccessException {
        Collection list = exportConfig.getData();
        Map<String, Method> fieldMethods = exportConfig.getFieldMethods();
        Map<String, ExportParam> fieldFormatter = exportConfig.getFieldFormatter();

        List<String> exportRows = new LinkedList<>();
        for (Object each : list) {
            StringBuilder row = new StringBuilder();
            for (String fieldName : fieldMethods.keySet()) {
                // S=过滤数值
                Object invokeVal = fieldMethods.get(fieldName).invoke(each);
                String val = null;
                if (null != invokeVal) {
                    if (invokeVal instanceof Date) {//时间类型
                        Date dateVal = (Date) invokeVal;
                        val = formatDateValue(dateVal, fieldFormatter.get(fieldName));
                    } else { // 简单数据类型
                        try {
                            val = formatStringValue(invokeVal.toString(), fieldFormatter.get(fieldName));
                        } catch (Exception e) {
                            throw new ExportException("导出实体属性只能是简单类型或者时间类型");
                        }
                    }
                }
                // E=过滤数值
                row.append(null == val ? "" : val).append(",");
            }
            String exportRowsStr = row.toString();
            log.info("row为===" + exportRowsStr);
            if (exportRowsStr.length() > 1) {
                String substring = exportRowsStr.substring(0, exportRowsStr.length() - 1);
                exportRows.add(substring);
            }
        }
        exportConfig.setRows(exportRows);
        log.info("所需导出数据大小为" + exportRows.size() + "行");
    }

    /**
     * 格式化时间类型
     *
     * @param dateVal
     * @param exportParam
     * @return
     */
    private static String formatDateValue(Date dateVal, ExportParam exportParam) {
        String formatter = exportParam.dateFormat();

        // 过滤时间格式
        if (StringUtils.isNotBlank(formatter)) {
            SimpleDateFormat sdf = new SimpleDateFormat(formatter);
            return sdf.format(dateVal);
        }
        return dateVal.toString();
    }

    /**
     * 值过滤,可迭代
     *
     * @param val         值
     * @param exportParam 过滤属性
     * @return 返回过滤完之后的值
     * @throws ParseException
     */
    public static String formatStringValue(String val, ExportParam exportParam) {
        int length = exportParam.length();
        String patternStr = exportParam.pattern();

        if (StringUtils.isNotBlank(val)) {
            // 过滤长度
            if (0 != length && val.length() > length) {
                val = val.substring(0, length);
            }
            // 过滤正则
            if (StringUtils.isNotBlank(patternStr)) {
                Pattern pattern = Pattern.compile(patternStr); //中文括号
                Matcher matcher = pattern.matcher(val);
                if (matcher.matches()) {
                    val = matcher.replaceAll("");//全替换成""
                }
            }
        }
        return val;
    }

    /**
     * 转换第一个字母大写
     *
     * @param value 值
     * @return value的头文字大写值
     */
    private static String firstOneToUpperCase(String value) {
        return value.substring(0, 1).toUpperCase() + value.substring(1);
    }

    /**
     * 根据csv内容,通过流导出文件
     *
     * @param outputStream 流
     * @throws IOException
     */
    public static void exportCSV(CSVExportConfig exportConfig, OutputStream outputStream) throws IOException {
        /*S=处理乱码问题*/
        OutputStreamWriter out = new OutputStreamWriter(outputStream, Charset.forName("UTF-8"));
        out.write(new String(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}));
        /*E=处理乱码问题*/
        int cycleCount = exportConfig.getCycleCount();
        for (int i = 0; i < cycleCount; i++) {
            String content = getCSVContent(exportConfig);
            out.write(content);
            out.flush();
        }
        out.close();
    }

    /**
     * 重设响应头
     *
     * @param response
     */
    public static void resetResponse(HttpServletResponse response) {
        response.reset();
        response.setHeader("Content-disposition", "attachment; filename=file.csv");
        response.setContentType("application/octet-stream; charset=UTF-8");
    }

    /**
     * 导出CSV
     * 1.验证配置
     * 2.取得头部内容信息
     * 3.导出内容文件
     *
     * @param response 相应对象,为了获取响应流
     * @param clazz    导出的实体对象
     * @param data     导出的实体对象数据集合
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws NoSuchMethodException
     * @throws IOException
     */
    public static void exportCSV(HttpServletResponse response, Class clazz, Object data) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, IOException, NoSuchFieldException, ParseException {
        //验证配置完整性
        validateConfig(data, clazz);

        Collection list1 = (Collection) data;

        List list = new ArrayList();
        for (int i = 0; i < 1000; i++) {
            list.addAll(list1);
        }

        CSVExportConfig exportConfig = new CSVExportConfig(clazz, list);

        setHeaderAndFieldParams(exportConfig);
        log.info("header解析结果" + exportConfig.getHeader());

        //获取行数据
        setRowDataByFieldMethods(exportConfig);

        //重新设置response对象
        resetResponse(response);

        exportCSV(exportConfig, response.getOutputStream());
    }

    /**
     * 验证配置信息
     *
     * @param data  数据集合
     * @param clazz 导出实体
     */
    private static void validateConfig(Object data, Class clazz) {
        if (!(data instanceof Collection)) {
            throw new ExportException("接口返回数据类型应为集合");
        }
        if (null == clazz) {
            throw new ExportException("未知导出实体类");
        }
        Annotation annotation = clazz.getAnnotation(ExportEntity.class);
        if (null == annotation) {
            throw new ExportException("导出实体类未标示注解");
        }
    }
}

3.导出配置类

/**
 * @Author: chenxiaoqing9
 * @Date: Created in 2019/1/17
 * @Description: 导出数据配置类
 * @Modified by:
 */
public class CSVExportConfig {
    public final int MAX_CACHE_ROW_COUNT = 5000;//最多缓存5000条刷新到Response里面再继续执行

    private int cycleCount;//需要循环刷新几次
    /**
     * 导出实体类
     */
    private Class clazz;
    /**
     * 头部信息
     */
    private String header;

    private List<String> rows = new LinkedList<>();
    /**
     * 数据内容
     */
    private Collection data;

    /**
     * 获取带注解的实体:字段名称 -- 字段get方法
     */
    private Map<String, Method> fieldMethods = new LinkedHashMap<>();
    /**
     * fieldFormatter:做过滤字段值处理
     */
    private Map<String, ExportParam> fieldFormatter = new HashMap<>();

    public CSVExportConfig(Class clazz, Collection data) {
        this.clazz = clazz;
        this.data = data;
    }

    public int getCycleCount() {
        if(this.data.size() == 0){
            throw new ExportException("导出数据为空!");
        }
        int result = this.data.size() / MAX_CACHE_ROW_COUNT;
        int remainder = this.data.size() % MAX_CACHE_ROW_COUNT;
        result = result + (remainder == 0 ? 0 : 1);
        return result;
    }

    public Class getClazz() {
        return clazz;
    }

    public void setClazz(Class clazz) {
        this.clazz = clazz;
    }

    public String getHeader() {
        return header;
    }

    public void setHeader(String header) {
        this.header = header;
    }

    public List<String> getRows() {
        return rows;
    }

    public void setRows(List<String> rows) {
        this.rows = rows;
    }

    public Collection getData() {
        return data;
    }

    public void setData(Collection data) {
        this.data = data;
    }

    public Map<String, Method> getFieldMethods() {
        return fieldMethods;
    }

    public void setFieldMethods(Map<String, Method> fieldMethods) {
        this.fieldMethods = fieldMethods;
    }

    public Map<String, ExportParam> getFieldFormatter() {
        return fieldFormatter;
    }

    public void setFieldFormatter(Map<String, ExportParam> fieldFormatter) {
        this.fieldFormatter = fieldFormatter;
    }
}

4.切面配置

/**
 * @Author: chenxiaoqing9
 * @Date: Created in 2019/1/15
 * @Description: 导出接口 切面
 * @Modified by:
 */
@Aspect
@Slf4j
@Component
public class ExportHandlerAspect {

    @Pointcut("@annotation(***.***.***.ExportHandler)")
    public void pointcut(){}

    @Around("pointcut() && @annotation(handler)")
    public void doAround(ProceedingJoinPoint point, ExportHandler handler) throws Throwable {
        //获取执行结果
        Object data = point.proceed(point.getArgs());

        Class clazz = handler.value();

        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();

        CSVUtils.exportCSV(response, clazz, data);
    }
}

5.使用步骤

1.Controller加上 @ExportHandler(A.class)注解,并注明导出实体类。示例代码如下:

@GetMapping("/exportList")
    @ExportHandler(A.class)// 导出集合的实体
    public List<A> export(AQuery query, Page<A> page) {
        Page<A> data = AService.queryByPage(query, page);
        return data.getRows();
    }

2.导出的实体加上类注解跟需要导出的字段注解(a.实体注解:@ExportEntity;b.字段注解@ExportParam--注解内属性作用见下列详情)

<!--示例代码-->
@ExportEntity
public class A{

    @ExportParam("名字")
    private String name;

    @ExportParam(value = "部门名称")
    private String deptName;
}

3.前端代码接口访问方式这边提供三种

  1. 使用<a href='下载地址' target='_blank'></a>下载
  2. location.href='下载地址'
  3. 用异步xhr下载(有限制大小,太大会报安全问题)、
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,635评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,543评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,083评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,640评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,640评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,262评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,833评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,736评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,280评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,369评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,503评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,870评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,340评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,460评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,909评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,512评论 2 359

推荐阅读更多精彩内容