一、特殊名词介绍

二、常用接口说明

接口主要场景备注
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导出excel2007对每个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.预览导出按钮导出

2.web端不预览导出

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

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

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() {
			...
			private AppExporter getPDFExporter(HttpServletRequest req, SessionProvider sessionIDInfor) {
                ...
                //插件扩展点,PDFExporterCreator接口将从这里被创建并在后续导出逻辑生效
                return PDFExporterFactory.getPDFExporter(isPDFPrint);
            }
        });

        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#doExport(HttpServletRequest req, HttpServletResponse res, ReportSessionIDInfor sessionIDInfor, String format)throws Exception {
	...
        try {
            // 导出并且记录下来
            LogUtils.exportAndLogRecordType(exporter, outputStream, new ReportRepositoryDeal(req, sessionIDInfor), recordType);
        } catch (Exception e) {
            ...
        }
        ...
    }
}

LogUtils.exportAndLogRecordType(AppExporter exporter, OutputStream out,ReportRepositoryDeal repo, RecordType exportType) throws Exception {
        ...
        exportAndLogRecordType(exporter, out, repo, exportType, sessionIDInfor,
                sessionIDInfor.getRelativePath(), sessionIDInfor.getFitBook2Show(),
                new PageSetCreator() {
                    public PageSetProvider createPageSet() {
                        return sessionIDInfor.getPrintPreviewPageSet4Traversing();
                    }
                });
}

LogUtils.exportAndLogRecordType(AppExporter exporter, OutputStream out, ReportRepositoryDeal repo, RecordType exportType, SessionProvider sessionIDInfor, String bookPath,
                                              ResultWorkBook book, PageSetCreator pageSet) {
        ...
        try {
            ...
            export(exporter, out, repo, sessionID, bookPath, book, pageSet, sessionManager, sheets, exportType);
            ...
        } catch (Exception e) {
            throw SessionLocalManager.createLogPackedException(e);
        }
}

LogUtils.export(AppExporter exporter, OutputStream out, ReportRepositoryDeal repo, String sessionID, String bookPath,
                               ResultWorkBook book, PageSetCreator pageSet, ExportSessionManager sessionManager, int[] sheets, RecordType exportType) throws Exception {
        ...
        try {
			//hugh:最终导出对象是通过这个方法调用的
            exporter.export(out, book, pageSet, repo, sheets);
        } finally {
            sessionManager.removeExportSession(sessionID, exportType.getTypeString());
        }
}

而实际生效的具体的AppExporter又是通过操作器(Operate)生成ExportCollection对象的时设置进去的。对于其中的excel导出逻辑部分又涉及到以下的接口处理

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;
	}

	...

}

ExcelExporter在导出excel2007时,又单独抽象了StreamExcel2007Exporter导出类

package com.fr.io.exporter;

...
public class ExcelExporter extends AbstractExcelExporter<Boolean> {
	
	...

	public void export(OutputStream out, ResultWorkBook workBook, PageSetCreator pageSet, ReportRepositoryDeal repo, int[] sheets) throws Exception {
		export(out, workBook, true, sheets);
	}

	
	public void export(OutputStream out, ResultWorkBook book) throws Exception {
		export(out, book, false);
	}
	
	
    public void export(OutputStream out, ResultWorkBook book, boolean reUse) throws Exception {
        export(out, book, reUse, null);
    }

    
    public void export(OutputStream out, ResultWorkBook book, boolean reUse, int[] sheets) throws Exception {
        ...
        ResultWorkBook book2Export = removeUselessSheet(book, sheets, book);
		try {
			if (checkExcelExportVersion()) {
				PerformanceManager.getRuntimeMonitor().setCurrentSessionStatus(ReportStatus.EXPORT_EXCEL_2007);
				exportFor2007(out, book2Export);
				return;
			}
			PerformanceManager.getRuntimeMonitor().setCurrentSessionStatus(ReportStatus.EXPORT_EXCEL_2003);
			exportFor2003(out, book2Export, reUse, false);
		}finally {
			PerformanceManager.getRuntimeMonitor().setCurrentSessionStatus(ReportStatus.COMPLETE);
		}
	}

	protected void exportFor2007(OutputStream out, ResultWorkBook book) throws Exception {
		//hugh:实质导出2007发生在这里,ReportUtils.getPaperSettingListFromWorkBook方法会得到PageExcel2007Exporter导出对象,再调用PageExcel2007Exporter#export(OutputStream out, ResultWorkBook book)进行导出
		//PageExcel2007Exporter又继承于StreamExcel2007Exporter,导出2007对每个sheet的处理就在这里面的逻辑被调用
		getExporterFor2007(
			ReportUtils.getPaperSettingListFromWorkBook(book)
			).export(out, book);
		return;
	}

    protected void exportFor2003(OutputStream out, ResultWorkBook book, boolean reUse, boolean layerEngine) throws Exception {
    	...
		//hugh:实质导出2003发生在这里
        exportBook(book, new HssfWorkbookWrapper(hssfWorkbook), hssfCellList,
                hssfCellFormulaList, reportList, reUse);

        ...
    }
	...

}


package com.fr.io.exporter.excel.stream;
...
public class StreamExcel2007Exporter<T> extends AbstractExcelExporter<T> {

	...

	/**
	 * Export report,报表不需要再次Execute.
	 *
	 * @param report 需要导出的报表
	 * @param exportAttr 报表导出属性
	 * @param sheetName 当前sheet的名称
	 * @param wb poi处理excel的Workbook
	 * @param xssfCellList poi处理excel格子的List
	 * @param xssfCellFormulaList poi处理excel中公式的List
	 * @param reportIndex 当前报表的Index
	 */
	protected void innerExportReport(Report report, ReportExportAttr exportAttr, String sheetName,
									 SXSSFWorkbook wb, List xssfCellList, List xssfCellFormulaList, int reportIndex) throws Exception {
		ExcelExportAttr eea = exportAttr == null ? new ExcelExportAttr() : exportAttr.getExcelExportAttr();
		//hugh:通过再次封装的导出器进行导出
		StreamExcelReportExporter exporter = new StreamExcelReportExporter((ElementCase)report, sheetName, wb, xssfCellList, xssfCellFormulaList, eea, reportIndex, paperSettingList, columnRowPostileMaps, this);
		exporter.export();
	}
	...
}


package com.fr.io.exporter.excel.stream;

...
public class StreamExcelReportExporter {
    ...

    public void export() throws Exception {
		//hugh:导出涉及的上下文,在StreamExcelReportExporter构造的时候就全部带入
        ...
        CommentExcelProcessor processor = ExtraReportClassManager.getInstance().getSingle(CommentExcelProcessor.MARK_STRING);
        if (processor != null) {
            if (patr == null) {
                patr = xssfSheet.createDrawingPatriarch();
            }
			//hugh:具体对excel2007的每个sheet的处理接口就在这里生效了
            processor.addCellComment(this.xssfSheet, this.report, patr);
        }
    }
	...

}

到这里可以看出,在预览cpt模板使用产品自带的导出按钮和服务的执行逻辑就涉及8个常用的接口中的6个(只有FormExportProcessorFormatActionProvider接口还未被调用)。

因为这两个接口一个作用于决策报表,一个作用于不预览导出(直接通过format参数方式导出),接下来我们先看不预览导出的执行逻辑

package com.fr.web.core.reserve;

...

public class ReportletDealWith {

    ...

    /**
     * 处理模板
     *
     * @param req    http请求
     * @param res    http应答
     * @param weblet 模板
     * @throws Exception e
     */
    public static void dealWithReportlet(HttpServletRequest req, HttpServletResponse res, Weblet weblet) throws Exception {
        ...
		//hugh:这里是访问viewlet=xxx.cpt&format=xxx的入口
        if (WebUtils.getHTTPRequestParameter(req, "format") != null) {
            turnToExportWithSessionKept(req, res, sessionID);
        } else {
            ...
        }
    }


    public static void turnToExportWithSessionKept(HttpServletRequest req, HttpServletResponse res, String sessionID) throws Exception {
        ExportSessionKeeper.getInstance().keepAlive(sessionID);
        turnToExport(req, res, sessionID);
        ExportSessionKeeper.getInstance().close(sessionID);
    }


    /**
     * 转向导出
     *
     * @param req       request
     * @param res       response
     * @param sessionID session id
     * @throws Exception e
     */
    public static void turnToExport(HttpServletRequest req, HttpServletResponse res, String sessionID) throws Exception {
        // 这里检查是不是输出成其他的形式,比如PDF, PDF_Activex, Excel等等..
        String format = WebUtils.getHTTPRequestParameter(req, "format");
		//FormatActionFactory中会对FormatActionProvider接口进行注册,具体的逻辑在FormatActionProvider接口的接口文档中已单独介绍,这里不再赘述
        FormatActionProvider requestProcessor = FormatActionFactory.getReqProcessor(format);
        if (requestProcessor != null) {
            requestProcessor.doAction(req, res);
        } else {
            String embedParameter = WebUtils.getHTTPRequestParameter(req, ParameterConstants.EXPORT_PDF_EMBED);
            boolean embed = "true".equals(embedParameter); // 当且仅当参数值是true时,才嵌入
			//hugh:如果插件没有申明FormatActionProvider接口那么,将会转到产品自身的ExportService逻辑执行,也就回到了我们最开始介绍的导出逻辑的流程中
			//从这里我们可以看出,如果在使用FormatActionProvider接口进行导出的干预时,一定要考虑到这段逻辑的兼容,从而保障后续接口的正常生效。
            ExportService.dealWithExport(req, res, sessionID, embed);
        }
    }
	...
}

FormExportProcessor执行逻辑是独立生效的,在接口介绍文档中已单独说明,本文不再赘述。

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

5.后台自定义的其他导出:这部分全部是用户根据自身需求单独开发和调用,没有固定的执行逻辑,需要支持哪些接口,也需要由我们的开发者根据实际的场景进行选择。


从上文的代码片段中,能够看出,导出接口顶层一共是FormatActionProviderExportExtensionProcessorExportOperateProvider 三个cpt报表导出接口和一个独立的FormExportProcessor 决策报表导出接口共四个接口组成。

FormatActionProvider接口逻辑隐式的包含了ExportExtensionProcessor接口,而ExportExtensionProcessor接口逻辑又隐式的包含了ExportOperateProvider接口。开发时需要注意兼容性设计

而其他四个细分接口和局部处理接口,均受到顶层cpt导出的三个接口的影响。


在实际开发过程中希望开发者按照以下的规则进行接口的选择:

1.能选择Provider类型接口的就不要选择Processor接口

2.能选择细分和局部处理接口的就不要选择顶层接口

3.FormatActionProviderExportExtensionProcessorExportOperateProvider 三个接口,能选择后者实现的场景,就不要使用前者。

4.除非导出场景非常简单,否则轻易不要去实现FormExportProcessor接口

5.新导出类型一定选择ExportOperateProvider,对原有导出逻辑大大规模(指的是功能,不是单元格数量)调整也选择ExportOperateProvider


导出逻辑实现后,配套的往往就是设计器配置和前端服务和UI的调整。这就需要开发者选择性的根据场景需要选择文章开头的配合接口了。

三、开源案例

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