一、特殊名词介绍

二、常用接口说明

接口主要场景备注
com.fr.report.fun.ExportOperateProvider新文件类型导出接口
com.fr.report.fun.ExportExtensionProcessor导出操作统一处理接口易冲突!禁止新商城插件使用,仅用于单个客户条件允许下的定制
com.fr.report.fun.FormatActionProvider不预览(format)导出处理接口易冲突!禁止新商城插件使用,仅用于单个客户条件允许下的定制
com.fr.form.stable.FormExportProcessor决策报表导出接口
com.fr.report.fun.ExcelExportAppProviderexcel细分导出类型接口
com.fr.io.exporter.PDFExporterCreatorPDF细分导出类型接口易冲突!禁止新商城插件使用,仅用于单个客户条件允许下的定制
com.fr.stable.fun.ExcelExportCellValueProvider导出excel单元格值处理接口
com.fr.report.fun.CommentExcelProcessor导出excel对每个sheet的处理接口易冲突!禁止新商城插件使用,仅用于单个客户条件允许下的定制
com.fr.design.fun.ToolbarItemProvider报表工具栏按钮扩展接口配合实现新导出类型
com.fr.report.fun.ExtensionButtonProvider报表工具栏导出菜单按钮扩展接口配合实现新导出类型
com.fr.stable.fun.LocaleFinder国际化配合实现新导出类型
三组常见引入JS和CSS的插件接口对比引入JS/CSS资源配合实现新导出类型
三组开放web服务接口的插件接口对比提供web服务配合实现新导出类型

帆软报表中涉及导出的接口比较多,目前已知被插件中使用的导出功能接口主要有8个(上表中的前8个)。接口开放年代都比较久远,存在相互耦合和包含的关系,所以使用时开发者需要先理清楚这些接口的关系,并根据实际需要解决的需求场景和环境合理的选择适合的接口。否则极易引起冲突,导致后期维护风险极高。

帆软报表中导出根据触发条件一共分为以下几种场景:

1.预览导出按钮导出:这部分链路稍微有点长,将采用删减代码注释对各接口的调用逻辑进行阐述【为了方便理解,代码中只保留了跟导出接口直接相关的部分代码,接口的注意点也直接通过注释体现在代码中】。

package com.fr.web.core.reserve;

...

public class ExportService extends NoOPService {

    ...
  
    public String actionOP() {
        return "export";
    }

    /**
     * 执行  hugh:这里就是导出的入口点
     *
     * @param req       http请求
     * @param res       http应答
     * @param sessionID 会话ID
     * @throws Exception
     */
    public void process(HttpServletRequest req, HttpServletResponse res, String sessionID) throws Exception {
        ...
        dealWithExport(req, res, sessionID, false);
    }

    /**
     * 进行导出
     *
     * @param req       http请求
     * @param res       http 应答
     * @param sessionID 会话ID
     * @param isEmbbed  嵌入
     * @throws Exception
     */
    public static void dealWithExport(HttpServletRequest req,
                                      HttpServletResponse res, String sessionID, boolean isEmbbed) throws Exception {
        ...

        ExportExtensionProcessor processor = getExportExtensionProcessor();
		//hugh: ExportExtensionProcessor接口的fileName 方法执行
        String fileName = processor.fileName(req, sessionIDInfor);

        String format = WebUtils.getHTTPRequestParameter(req, "format");
		//hugh: ExportExtensionProcessor接口的createCollection 方法执行
        ExportCollection exportCollection = processor.createCollection(req, res, sessionIDInfor, format, fileName, isEmbbed);

        exportCollection.doExport(req, res, sessionIDInfor, format);

        ...
    }

    private static ExportExtensionProcessor getExportExtensionProcessor() {
		//hugh:读取插件中的导出接口,如果读取不到就采用产品内置的导出接口DefaultExportExtension
		//ExportExtensionProcessor本身是独占的接口,也就是最终不论有多少插件实现了这个接口,最终都只会生效一个。
		//同时插件实现的生效后产品本身的DefaultExportExtension就不再返回。所以开发者在使用这个接口时,最好是直接继承DefaultExportExtension实现
		//然后把后续的接口兼容进去。否则这里是一个高冲突的风险点。
        ExportExtensionProcessor processor = ExtraReportClassManager.getInstance().getSingle(ExportExtensionProcessor.MARK_TAG);
        if (processor == null) {
            processor = new DefaultExportExtension();
        }
        return processor;
    }

    ...

}

接下来假设所有的接口都已实现了对其他接口兼容,那么开发者对ExportExtensionProcessor的实现也就兼容DefaultExportExtension自身的逻辑。

package com.fr.web.core.reserve;

...

public class DefaultExportExtension extends AbstractExportExtension {

    public String fileName(HttpServletRequest req, TemplateSessionIDInfo sessionIDInfor) throws Exception {
        ...
	//hugh:该方法与ExportExtensionProcessor接口的fileName功能一致,都是确定本次导出的文件名
    }

    public ExportCollection createCollection(HttpServletRequest req, HttpServletResponse res,
                                                   ReportSessionIDInfor sessionIDInfor, String format,
                                                   String fileName, boolean isEmbed) throws Exception {
		//hugh:这里本质执行的就是ExportOperateProvider的operate接口方法,具体我们可以再看ExportFactory的关键逻辑
        Operate operate = ExportFactory.getOperate(format.toLowerCase());
        if (operate != null) {
            operate.setContent(req, res, sessionIDInfor, fileName, isEmbed);
			//hugh:实际导出对象就是在这里产生的,也是ExportOperateProvider接口中返回的Operate接口的一个方法,最终也是利用这个导出对象进行导出的
            return operate.newExportCollection(req, res, sessionIDInfor, fileName);
        }
        return ExportCollection.create();
    }
}


package com.fr.web.core.reserve;

...

public class ExportFactory {

    private static final String FORMAT_EXCEL = "excel";
    ...
    private static final String FORMAT_PDF = "pdf";
    ...

    private final static Map<String, Operate> OPERATE_MAP = new HashMap<String, Operate>();

    static {
		//hugh:优先会把产品自身的导出操作器注册进去
        OPERATE_MAP.put(FORMAT_PDF, new DefaultOperate() {...});

        OPERATE_MAP.put(FORMAT_EXCEL, new ExcelOperate());
        ...
		//hugh:最后才会注册插件中定义的操作器,注意,这里OPERATE_MAP是直接覆盖的,也就是开发者可以通过这个接口做新类型导出,也可以替换原本产品的某一种导出
        Set<ExportOperateProvider> providers = ExtraReportClassManager.getInstance().getArray(ExportOperateProvider.MARK_STRING);
        for (ExportOperateProvider provider : providers) {
            OPERATE_MAP.put(provider.markType(), provider.operate());
        }
    }

    /**
     * 根据格式获取操作方法
     *
     * @param format 格式
     * @return 操作方法
     */
    public static Operate getOperate(String format) {
        return OPERATE_MAP.get(format);
    }
}

通过前文可以了解到,最终的导出都是通过ExportCollection#doExport(req, res, sessionIDInfor, format);方法执行的,具体的操作器生成ExportCollection对象的逻辑中又涉及到以下的接口处理

package com.fr.web.core.reserve;

...

public class ExcelOperate extends DefaultOperate {

    ...

    public void setContent(HttpServletRequest req, HttpServletResponse res, SessionProvider sessionIDInfor, String fileName, boolean isEmbed) {
        //hugh:处理导出请求头
    }


    public ExportCollection newExportCollection(HttpServletRequest req, HttpServletResponse res, SessionProvider sessionIDInfor, String fileName) {
        return createExcelExportCollection(req, res, sessionIDInfor, fileName);
    }

   
    ...

    public ExportCollection createExcelExportCollection(HttpServletRequest req, HttpServletResponse res,
                                                        SessionProvider sessionIDInfor, String fileName) {

        ExcelExportType exportType = createExcelExportType(req, sessionIDInfor);

		//hugh:大数据量导出excel的细分是单独处理的,而且产品中也没有提供显示的调用,这里是通过ExcelExportAppProvider#newLargeDataExportCollection接口实现
        if (ExportConstants.TYPE_LARGEDATA_PAGE.equalsIgnoreCase(exportType.getExportType())) {
            Set<ExcelExportAppProvider> providers = ExtraReportClassManager.getInstance().getArray(ExcelExportAppProvider.MARK_STRING);
            for (ExcelExportAppProvider provider : providers) {
                if (provider.exportType().equalsIgnoreCase(exportType.getExportType())) {
                    ExportCollection collection = provider.newLargeDataExportCollection(req, res, sessionIDInfor, fileName, exportType);
                    if (collection != null) {
                        return collection;
                    }
                }
            }
            return createLargeDataExportCollection(req, res, sessionIDInfor, fileName, exportType);
        } else {
            ...
            AppExporter<Boolean> exporter = createExcelExporter(collection, exportType, sessionIDInfor);
            ...
            return collection;
        }
    }

    public AppExporter<Boolean> createExcelExporter(ExportCollection collection, ExcelExportType exportType, SessionProvider sessionIDInfor) {
        AppExporter<Boolean> exporter;
		//hugh:从插件中读取导出excel的细分对象,ExcelExportAppProvider#newAppExporter接口执行
        Set<ExcelExportAppProvider> providers = ExtraReportClassManager.getInstance().getArray(ExcelExportAppProvider.MARK_STRING);
        for (ExcelExportAppProvider provider : providers) {
            if (provider.exportType().equalsIgnoreCase(exportType.getExportType())) {
                return provider.newAppExporter(collection, exportType, sessionIDInfor);
            }
        }
        //hugh:创建产品自身的excel导出细分对象,具体可以看ExcelExportAppProvider接口的demo示例
		...
        return exporter;
    }
}


package com.fr.io.exporter;

...

public abstract class AbstractExcelExporter<T> extends AbstractAppExporter<T> {
	...
    
	public Object evalCellValue(CellElement cellElement, boolean exHiddenRow, boolean exHiddenColumn,
			List hssfCellList, POICellAction hssfCell, Calculator cal, Style style, List hssfCellFormulaList,
			CellGUIAttr cellGUIAttr, DynamicUnitList rowHeightList, DynamicUnitList columnWidthList, int column,
			int row, int columnSpan, int rowSpan, POIWorkbookAction wb) {
		//hugh:优先按照当前导出对象自身的逻辑计算单元格的值,之后再使用ExcelExportCellValueProvider接口对excel的值进行修改,也就是如果开发者自己定义了ExcelExporter,一定要实现这段代码逻辑,否则ExcelExportCellValueProvider就失效了
		//所以,建议在实现ExcelExporter时,都直接继承AbstractExcelExporter的相关子类。极端情况下开发者需要全部重制整个excel导出时,也至少要继承AbstractExcelExporter,并保障这段代码的执行
		...
		return getValueFromExcelExportCellValueProvider(cellElement, cal, value);
	}

	//抽一下逻辑
	private Object getValueFromExcelExportCellValueProvider(CellElement cellElement, Calculator cal, Object value) {
		Set<ExcelExportCellValueProvider> providers = ExtraClassManager.getInstance().getArray(ExcelExportCellValueProvider.XML_TAG);
		for (ExcelExportCellValueProvider provider : providers) {
			value = provider.getCellValue(cellElement, value, cal);
		}
		return value;
	}

	...

}

2.web端不预览导出

3.定时调度/预览邮件附件导出

4.设计器导出:此处导出类全部由DesignReportExportType类加载和初始化,通过IDEA自带的反编译开发者可以了解到设计器导出导出PDF会执行PDFExporterCreator接口,导出excel会执行CommentExcelProcessorExcelExportCellValueProvider接口其他接口不会执行。

5.后台自定义的其他导出


三、开源案例

免责声明:所有文档中的开源示例,均为开发者自行开发并提供。仅用于参考和学习使用,开发者和官方均无义务对开源案例所涉及的所有成果进行教学和指导。若作为商用一切后果责任由使用者自行承担。