AOP实现统一导出报表——原来做报表也能这么有趣

前言

注:之前博客的功能都是在业余时间开发,所以博客有带些代码,这个功能思路在业余构造,而后台配置方案源码在上班期间编写,故不作代码开源,以避免面对牢狱编程。

对于开发而言,报表流是最不愿意碰的,无趣,没有逻辑难度,却又极其繁琐。(这话有点得罪专门搞工作流的)不过,无论做哪个行业多多少少都会涉及报表流,躲也躲不掉的。在此,我通过切面思想实现统一报表导出功能,避免单一重复的报表接口编写——原来做报表也能这么有趣。

■ 需求

支持导出本页和导出所有数据接口

■ 解析

看到这个需求
一类码畜——CV开发;先是暗自吐槽一波,然后列出所有需要导出的报表,接着就立马下键盘,一个一个的CV,删删改改,好不辛苦,一问工资2500。这么搞,开发的繁琐不说,后期的新报表估计也要归你编写,而且一旦有新变动,就得一个个改;

二类码畜——抽象开发;嗯?先抽离出共同点,写个工具类,然后一个个写接口,调用工具类。高级一点的就选用某种设计模式,只用一个接口就可供应各种报表下载,抽离出了共同点,但仍然需要写接口,然后引用工具类,后期新报表也需要开发人员进行编写。这么搞,简单了不少,但新报表需求仍然需要进行少量代码编写。后续导出报表都落你手上,只有少量CV,还好说,但落同事手上,又得浪费开发资源咯,因为你写的工具类,同事不一定会用,也不一定会采用。

三类码畜——切面开发;我认为万物皆可切,不滥用切面编程的前提下,切面开发是一个非常棒的解决问题的思想。观摩有报表导出需求的页面,从而提取这些报表导出的共同点——都是数据查询、有分页。
先找切面——嗯。。不就是对数据查询做个增强吗,改数据查询显示前端为数据查询存入Excel;
再找切点——我最早的设想是设定一个注解,把注解作为切入点,标记注解的接口进行织入,对返回的json数据进行解析。接着思考如何区分接口查数据和导出报表,用request带参数?给查询接口加个后缀?思考了很久,一直在想怎样低侵入性嵌入这个参数,直到第二天,突然灵感一闪,嗯?分页,对,分页对象,切service层的特征点返回值分页对象,有的时候懵了,休息一会儿,跳出来看才发现自己钻牛角尖了。这样切的准确,切到的基本都是需求点,并且切面返回值还更接近原始数据格式(分页对象的list属性正是报表所需要的参数),更完美的是新增新报表基本不需要改动代码。

分解步骤

①、织入分页查询
②、获取分页返回值的list
③、筛选字段,进行数据处理
④、返回数据

开切

■ 1.织入切点

①设定一个注解

设定一个注解,供特殊情况织入使用(不一定使用,作预留)

/**
 * @author bbq
 */
@Target({
    
    ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface XXAnnotation {
    
    
    String value() default "";
}

注:元注解
@Target —— ElementType.METHOD,ElementType.TYPE 表示可打在类和方法上
@Retention —— RetentionPolicy.RUNTIME jvm加载class文件之后,仍然存在
@Inherited —— 作用于类,注解会被子类继承
@Documented —— 标识生成的javadoc

扫描二维码关注公众号,回复: 11826180 查看本文章

②设定一个切面

设定一个切面,切点为service层并且返回值为分页对象的方法 或标有 ①注解的方法

/**
 * @author bbq
 */
@Aspect
public class XXAspect {
    
    
    public static final int MAX_EXPORT_COUNT = 10_000;
    
	// 切点 为service层并且返回值为分页对象的方法 或标有 ①注解的方法
    @Around(value = "@annotation(XXAnnotation) || " +
            "execution (包名.Page 包名.*.service层.*.*(..))")
    public Object XX(ProceedingJoinPoint joinPoint) throws Throwable {
    
    
        try {
    
    
            Object[] args = joinPoint.getArgs();
            
            // 判断切点方法的参数,找到参数分页对象,判断请求参数是查询数据还是下载报表
            for (int i = 0; i < args.length; i++) {
    
    
                if (args[i] != null && Page.class.isAssignableFrom(args[i].getClass()) && "1".equals(((Page<T>)args[i]).getIsExportExcel())) {
    
    
                    return excute(joinPoint, (Page<T>) args[i]);
                }
            }
        } catch (Exception e) {
    
    
            。。。
        }
        return joinPoint.proceed();
    }

    public Object excute(...){
    
    ...}
}

■ 2.整理参数与数据

如果是下载报表进入该方法。

public Object excute(ProceedingJoinPoint joinPoint, Page<T> page) throws Throwable {
    
    
		// 判断是否下载全部,查询全部则设置查询上限(防止报表行数太多)。
        if(page.getPageSize() == -1) {
    
    
            page.setPageSize(MAX_EXPORT_COUNT);
            page.setPageNo(0);
        }
        // 然后才执行分页查询,并可得出list列表。
        Page pageResult = (Page)joinPoint.proceed();

        String fileName = "占位符" + DateUtils.getDate("yyyyMMdd-HHmmss") + ".xlsx";
        try {
    
    
        	// list有数据即可通过反射获取数据的类,没有数据返回空json,前端作无数据弹窗
            Class format = Object.class;
            if(pageResult != null && pageResult.getList() != null && pageResult.getList().size() > 0) {
    
    
                format = pageResult.getList().get(0).getClass();
            }
            new ExportExcel(format).setDataList(pageResult.getList()).write(page.getResponse(), fileName).dispose();

        } catch (IllegalArgumentException e) {
    
    
            。。。
        }
        return null;
    }

■ 3.得到报表数据-上

得出list列表后,即可调用报表导出工具类,然后就是判断需要导出哪些字段,提供3种方法供你选择。

① 实体类配置注解

这个方法被广泛应用,基本开源报表功能都有带这个功能,大同小异,就是在属性或方法上打注解,然后报表导出工具类就可获取字段信息,一般都是设定注解不可重复,给出可重复注解方法
定义一个新注解ExcelFields

/**
 * @author bbq
 */
@Target({
    
    ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelFields {
    
    
	ExcelField[] value();
}

然后在原注解ExcelField加上这句即可实现多注解。就可在类上放打上一堆注解来映射报表字段

@Repeatable(ExcelFields.class)

再定义一个注解供获取标题使用

/**
 * @author bbq
 */
@Target({
    
    ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelTitle {
    
    
	String value();
}

②前端传递查询字段(不推荐)

我曾考虑过这种导出的字段有前端传参决定,后来认为不太合理便放弃了。

③ 后台配置方案(力荐)

最后我选用了后台配置方案,开发虽然麻烦了点,但一次解决,后期不用作代码改动。在权限最高的后台管理页面加上这个报表配置功能。两个配置管理,这有一个主子表关系,
主表设有以下字段——全路径类名,报表文件名,版本号;
子表设有——字段名,映射的报表标签名,排序。
字段可通过类名反射获取,可抓字段也可抓方法,我抓的是字段和方法都有的,如果抓到的属性是基本类型作为节点返回,抓到的是类则作为父节点返回,注意需要设为懒加载树,如果递归的话会陷入死循环

如果嫌一个一个配置麻烦的话,可在导出报表切面中增加一个方法,用于通过接口自动生成配置字段信息(得织到json数据上),配置人员只需要勾选需要导出的字段,编写标签名和排序即可。

如果报表导出频繁的话,可以考虑把这些信息加入redis缓存。

■ 4.得到报表数据-下

在之前的切面中,通过反射获取分页对象,即可得list,list有数据即可通过反射获取数据的类。然后就可以通过反射获得的类去寻找我们配置的类,即可获取配置的一切信息,如果一个类不止对应一个报表的话,可引入版本号机制来区分。
有了标题名,字段,字段对应的标签,字段排序等信息,一个报表的配置也就完成了。
通过配置的信息利用报表导出工具类即可导出对应报表。

最后

这样就可通过aop实现一个低维护成本的统一导出简单报表的功能。

猜你喜欢

转载自blog.csdn.net/qq_24054301/article/details/106887155