1. 概述
1.1 版本
报表服务器版本 | JAR 包最低版本支持 | 插件版本 | 功能变更 |
---|---|---|---|
11.0 | 2023-08-27 | 2.5.24 | 内置HTTP装载器支持GET通过BODY传参 |
11.0 | 2023-08-27 | 2.5.22 | 适配参数控件联动不重新计算的问题 |
11.0 | 2020-12-30 | 2.5 | |
10.0 | 2020-12-30 | 2.5 |
1.2 应用场景
企业数据源格式多种多样,部分数据格式无法直接应用于FineReport中。本插件提供数据格式转换能力,可对接JSON、WebService、网络爬虫、自主开发的数据中心等,并将其转换成FineReport可用的数据类型。同时,该插件具有高度扩展性,也可以通过二次开发来适应更复杂的场景。
注:1.基于此插件进行扩展开发需要具备最基础的 FineReport 插件开发能力。
2.使用子插件时升级,需要先禁用子插件,或升级后重启服务器。
1.3 功能介绍
插件通过不同的装载器和解析器实现个性化的数据装配,将从外部获取的数据转换成 FineReport 可用的数据格式。
2. 插件介绍
2.1 插件安装
点击下载插件:数据工厂数据集
设计器插件安装方法参照:设计器插件管理
服务器安装插件方法参照:服务器插件管理
2.2 操作方法
1)插件安装后,数据集管理面板新增数据工厂数据集,如下图所示:
2)点击进入数据工厂主面板,使用主面板可以应对绝大多数的场景。主面板具体功能介绍如下:
①名字:在名字栏可以设置该数据集的名称。
②装载器和解析器:装载器负责从数据源装载数据,解析器则负责将装载的数据转换成 FineReport 支持的数据模型。
插件内置了两种装载方式(Http、单参数)和三种解析方式(Json 、Js 解析器、原始数据)。
在下拉框中选择装载方式和解析方式,具体的装载器、解析器配置方法将在下文第3节进行详细介绍。
③高级设置:当内置的装载器和解析器无法满足用户需求时,可以使用高级设置功能进行二次开发,以实现功能扩展。该功能将在下文第4节进行详细介绍。
④缓存:缓存的意思是指将外部读取的数据暂存到缓存中,直接从缓存中取数,而不通过接口取数,以提高数据读取的速度。缓存有 3 个选项:无、磁盘、内存。
选择“无”表示不启用缓存功能
选择“磁盘”表示缓存到本地磁盘
选择“内存”表示缓存到内存
n分钟:表示n分钟内从缓存中读取静态数据,n分钟后再次从接口重新读取数据,重新读取的数据依然在缓存中保存n分钟。
⑤参数栏:参数栏里面的参数会在后台被分为两类:
第一类是装载器界面出现过的参数( ${参数名} ),该类参数名可被参数面板自动识别,同时解析到装载器配置的字符串里。 参数语法:用 ${参数名}获得参数的值,支持文本、公式等多种形式的参数,也支持从报表中读取参数。
第二类则是装载器界面未出现过的,用户可通过参数面板的插入按钮来添加新的参数作为请求参数。
在参数面板中,“参数”列中记录的是参数名;“值”列中点击按钮可以选择参数数据类型(包括字符串、整型、双精度型、日期、布尔型、公式),可以在该列中输入参数的取值。分别可以实现参数所在行的删除、上移和下移。
⑥预览:点击预览按钮,可以预览格式转换后的数据。
3)完成配置后,在FineReport设计器中,可看到模板数据集中新增的数据工厂数据集,该数据集可直接应用于报表制作。
3. 装载器和解析器使用示例
3.1 内置装载器
插件内置了两种装载方式,分别为:
单参数:可以实现将装载器界面的数据参数,经过计算后原样传给解析器,主要是供调试使用。( 比如 JSON、XML 等结构化数据 )
3.1.1 Http
当选择Http装载方式时,装载器的内容是一个JSON,里面有4个字段,分别是
参数名称 | 参数说明 | 备注 |
---|---|---|
url | 请求地址 | |
type | 请求类型 | 枚举值:GET,POST,JSON |
charset | 编码 | |
header | 请求头,以json键值对形式写入 | |
body | 当请求类型为JSON时使用 | |
connectTimeout | 请求连接超时时间 | 默认5秒,可通过配置该属性调整连接超时 |
readTimeout | 等待响应超时时间 | 默认60秒,可通过配置该属性调整响应超时时间 |
具体使用示例如表所示:
请求类型 | 装载器内容 | 参数面板 取值 | 实际请求 | 备注 |
GET |
| 参数 值 | url: http://example.com/?cc=p_CC type: get header:{aa:p_BB} | 用户在参数面板中添加的第二类额外参数会作为query参数拼接到url后。 |
POST |
| 参数 值 | url: http://example.com/ type: post header: {aa:p_BB} body: | 用户在参数面板中添加的第二类额外参数在请求体body里。body为x-www-form-urlencoded模式,内容为cc=p_CC。 |
JSON |
| 参数 值 bb p_BB cc p_CC | url: http://example.com/ type: post header: {aa:p_BB, content-type: json} body: | JSON 实际上是post raw模式,第二类参数会被装载成JSON的格式作为请求体发送。对于POST请求这种方式使用的更普遍。 body有两种装载方式:一种是类似用post的。第二种是直接写body键。 |
| 参数 值 bb p_BB dd p_DD | url: http://example.com/ type: post header: {aa:p_BB, content-type: json} |
注:示例中的http://example.com/仅为演示,实际请按照真实接口进行调整。
2.5.24版本新增支持GET请求通过BODY传递参数(POST请求不适用),具体说明如下:
当请求类型为GET时,提供Content-Type参数及body配合使用
配置名称 | 说明 | 示例 |
---|---|---|
Content-Type | 可选参数,搭配body参数使用。枚举值:application/x-www-form-urlencoded、application/json、multipart/form-data。如果存在body但是没有传递Content-Type参数,则Content-Type默认使用x-www-form-urlencoded | { "url": "http://example.com", "type": "GET", "charset": "UTF-8", "Content-Type": "application/x-www-form-urlencoded", "header": {}, "body": { "age": "123" } } |
body | 通过键值对形式配置,配置自动转换为相应content-type的报文格式,比如x-www-form-urlencoded下报文会被转换成age=123;参数面板的参数只会作为query传递 |
3.1.2 单参数
示例
在装载方式下拉框中选择“单参数”装载方式,在装载器内容框中输入 ${data}${today()} ,在解析方式下拉框中选择“原始数据”解析方式(表示原样返回数据,将在下文3.2.3进行介绍),在参数面板配置参数如下:
参数 值
data myData
点击预览按钮,得到如下结果:
3.2 内置解析器
插件内置了三种解析方式,分别为:
Json:对结构化的 Json数据进行解析,生成 FineReport 支持的数据模型。
注:只能解析结构型描述的 JSON,业务型描述的 JSON 无法解析,业务型 JSON 解析依赖具体的业务的描述,需要单独实现解析器。
Js解析器:通过Js脚本解析数据,生成 FineReport 支持的数据模型。
注:当数据量较大时要谨慎使用。
原始数据:原样返回数据,主要是供调试使用。
3.2.1 Json
提供了不完整的JPATH语法。字段含义如下:
dataPath:取值默认为root,表示处理全部数据;取值root.key1.key2.arr[1] 表示定位到{ key1 : { key2 : { arr:[ {},{<这里>} ] }} },这种情况下只解析定位路径下的json内容。
showmap:提供列名映射,将原始的列名(path1)转换为自定义的列名(value1)。
3.2.2 Js解析器
参照JSON数据集插件中Js语法,$data变量为接受到的原始数据,最终需要通过Js生成如下一个数据表对象
返回的对象样例,column为列名,content为二维数组,保存数据集单元格的值
{
"column":["col1", "col2", "col3"],
"content":[
[1, 2, 3],
[4, 5, 6]
]
}
示例:具体代码请按照实际场景开发
装载器返回数据如下:
{
"monthFactoryWaitingRateDataList": [
{
"factory": "HZC01",
"dayFactoryWaitingRateList": [0,0,0],
"days": ["2023-04-01","2023-04-02","2023-04-03"],
"dayStrs": ["04-01","04-02","04-03"]
},
{
"factory": "HZC02",
"dayFactoryWaitingRateList": [0,0,0],
"days": ["2023-04-01","2023-04-02","2023-04-03"],
"dayStrs": ["04-01","04-02","04-03"]
}
]
}
JS解析器填写示例:
var x =$data;
var column = Object.keys(x.monthFactoryWaitingRateDataList[0]);
var content = [];
for (var i = 0; i < x.monthFactoryWaitingRateDataList.length; i++) {
for (var j = 0; j < x.monthFactoryWaitingRateDataList[i].dayFactoryWaitingRateList.length; j++) {
var contentLine = [];
contentLine.push(x.monthFactoryWaitingRateDataList[i].factory, x.monthFactoryWaitingRateDataList[i].dayFactoryWaitingRateList[j], x.monthFactoryWaitingRateDataList[i].days[j], x.monthFactoryWaitingRateDataList[i].dayStrs[j]);
content.push(contentLine);
}
}
var result = {};
result.column = column;
result.content = content;
return result;
预览数据,效果如下:
Js解析器性能比较低,数据量大的时候要慎用。
插件2.5.13版本Js解析器开始使用JDK内置的Nashorn引擎,默认支持ES5语法,高版本语法暂不支持。
3.2.3 原始数据
类比单参数装载器,返回一个只有一个单元格的数据集,数据集的内容是从装载器接受到的原始数据,参数面板无意义。具体示例参照3.1.2。
3.3 使用案例
3.3.1 案例一
获取帆软市场的插件列表
装载方式:Http
选择Http装载方式以实现从 Web 端装载数据。
{
url:"https://market.fanruan.com/plugins/commodity?cid=&fee=&seller=&version=&language=&searchKeyword=&_=1650781600900",
type:"GET",
charset:"UTF-8",
header:{}
}
注:1. 具体的装载器逻辑可以单独客制,并没有什么统一的写法规范
2. 接口实际返回Json格式的数据如下(此处做对比说明,实际插件使用过程中,不会出现该步骤)
解析方式 :Json
选择Json解析方式以对结构化的 Json数据进行解析。
{
dataPath:"root.result",
showmap:"path1,value1,path2,value2",
}
即主面板配置如下:
点击预览按钮,返回经格式转换后的数据结果如下:
3.3.2 案例二
假设有如下接口用来获取某城市天气 http://example.com?city=北京
如果我们希望根据模板控件内容动态获取城市的数据,可以在模板中设置一个控件 city
然后在数据工厂面板中进行如下配置:
装载方式:Http
方式一:在装载器中添加同名参数 参数默认值置空
{
url:"http://example.com?city=${city}",
type:"GET",
charset:"UTF-8",
header:{}
}
方式二:在参数面板中手动添加参数city,参数值置空
{
url:"http://example.com",
type:"GET",
charset:"UTF-8",
header:{}
}
假设接口返回值如下:
{
result:[
{
district:海淀区,
temperature:20,
},
{
district:朝阳区,
temperature:23,
}
]
}
解析方式:Json
{
dataPath:"root.result",
showmap:"district,辖区,temperature,温度"
}
即主面板配置如下:
点击预览按钮,返回经格式转换后的数据结果如下:
4.高级设置功能介绍
假如使用者认为内置的的装载方式、解析方式不足以满足使用场景,又不想重新开发一个装载器/解析器时,可以点击数据工厂主面板的“高级设置”按钮自定义辅助步骤。
高级设置面板的作用是用来添加装载前、装载后、解析后事件作为装载方式、解析方式的补足,支持多个同步骤事件的添加。
4.1 处理流程
在“高级设置”面板最上层下拉框中可以选择“为该数据集单独设置”/“使用模板设置”/“使用全局设置”。
选择“为该数据集单独设置”时下方会出现列表,支持事件的增删改。
选择“使用模板设置”/“使用全局设置”时下方不显示列表,具体设置项需打开右侧“全局设置”/“模板设置”进行编辑。
4.2 各个事件的作用与接口描述
目前各个事件并没有实装具体的应用,仅提供了接入框架,高级步骤是装载-解析模式的增强和补全,结合开发篇来使用。
装载前事件
提供额外的参数引入 比如说token获取的逻辑可以写在自定义装载前事件里。多个装载前事件参数可以累积,重复的参数前者会被后者覆盖。
在主面板使用,使用方法同常规参数调用,右侧参数栏内默认值无效。
场景示例: 获取token,可以开发一个token获取的装载前事件,将获取的token作为参数引入主逻辑中。
装载前事件preprocessor接口描述:
package com.tptj.plugin.hg.fun;
import com.fr.script.Calculator;
import com.fr.stable.ParameterProvider;
/**
* 预处理阶段
*/
public interface Preprocessor extends Configuration {
String XML_TAG = "TableDataPreprocessor";
int CURRENT_LEVEL = 1;
/**
* 预处理 可以改变参数
* @param calculator 算子
* @param config 面板配置内容
* @return 注入的参数
* @throws Exception
*/
ParameterProvider[] process(Calculator calculator, String config) throws Exception;
}
装载后事件
提供装载器获取的原始数据的整理 通常装载器返回的是一个字符串类型,可以在这个阶段进行额外的整理行为,多个事件从上到下顺序执行。
场景示例:返回的数据虽然是json但不适合直接用json解析器,用js解析器性能又不够好的时候,可以定制一个装载后事件,把结构格式化为json解析器能够识别的样式。
装载后事件formatter接口描述:
package com.tptj.plugin.hg.fun;
public interface Formatter extends Configuration{
String XML_TAG = "TableDataFormatter";
int CURRENT_LEVEL = 1;
/**
* 数据格式化
* @param data 装载器返回的数据
* @param config 配置
* @return 格式化后的数据
* @throws Exception
*/
Object format(Object data, String config) throws Exception;
}
解析后事件
提供解析器返回的数据集的进一步处理 比如增删列,修改列名,重排序,条件过滤等,可以在这个阶段进行额外的整理行为,多个事件从上到下顺序执行。
场景示例: 排序,筛选,取前n项,取奇数项,调整数据格式等等自由操作数据集内容。
解析后事件filter接口描述:
package com.tptj.plugin.hg.fun;
import com.tptj.plugin.hg.stable.SimpleDataModel;
/**
*
*/
public interface Filter extends Configuration {
String XML_TAG = "TableDataFilter";
int CURRENT_LEVEL = 1;
/**
* 整理数据
* @param dataModel 数据模型
* @param config 配置
* @return 整理后的数据
* @throws Exception
*/
SimpleDataModel doAction(SimpleDataModel dataModel, String config) throws Exception;
}
4.3 其他接口描述
装载事件loader接口描述
package com.tptj.plugin.hg.fun;
import com.fr.script.Calculator;
import com.fr.stable.ParameterProvider;
import com.fr.stable.fun.mark.Mutable;
public interface Loader extends Mutable {
String XML_TAG = "TableDataLoader";
int CURRENT_LEVEL = 1;
/**
* 装载数据
* @param cal 当前算子
* @param params 需要用到的参数
* @param others 其他你可能用到的但是又不希望通过参数控制的配置项,即装载器面板内容
* @return 保存数据的对象
*/
Object load( Calculator cal, ParameterProvider[] params, String others);
/**
* 装载器的名字【显示在数据工厂配置界面的下拉列表里面的】,唯一,支持国际化的key
* @return
*/
String getName();
/**
*
* @return 默认显示的配置【显示在数据工厂配置界面装载器配置文本域里面的,配置项自定义,其实就是放一些,不想写死在代码里,又不希望被参数篡改的部分】
*/
String getDefaultConfig();
}
解析事件resolver接口描述
package com.tptj.plugin.hg.fun;
import com.fr.stable.fun.mark.Mutable;
import com.tptj.plugin.hg.stable.SimpleDataModel;
public interface Resolver extends Mutable {
String XML_TAG = "TableDataResolver";
int CURRENT_LEVEL = 1;
/**
* 解析数据
* @param data 从装载器得到的原始数据
* @param others 解析时会使用,但是不希望是参数的配置,即解析器面板内容
* @return 解析出来的二维数据模型
*/
SimpleDataModel parse(Object data, String others );
/**
* 解析器的名字【显示在数据工厂配置界面的下拉列表里面的】,唯一,支持国际化的key
* @return
*/
String getName();
/**
*
* @return 默认显示的配置【显示在数据工厂配置界面解析器配置文本域里面的,配置项自定义,其实就是放一些,不想写死在代码里,又不希望被参数篡改的部分】
*/
String getDefaultConfig();
}
功能点注册
<extra-core>
<TableDataLoader class="com.fr.plugin.tptj.tabledata.factory.demo.DemoLoader"/>
<TableDataResolver class="com.fr.plugin.tptj.tabledata.factory.demo.DemoResolver"/>
<TableDataPreprocessor class="com.fr.plugin.tptj.tabledata.factory.demo.DemoPreprocessor"/>
<TableDataFormatter class="com.fr.plugin.tptj.tabledata.factory.demo.DemoFormatter"/>
<TableDataFilter class="com.fr.plugin.tptj.tabledata.factory.demo.DemoFilter"/>
</extra-core>
<dependence>
<Item key="com.tptj.plugin.hg.tabledata.factory.v10" type="plugin"/> <!-- 依赖于数据工厂主框架 -->
</dependence>
我们通常继承抽象类来开发
package com.fr.plugin.tptj.tabledata.factory.demo;
import com.fr.base.Parameter;
import com.fr.json.JSONObject;
import com.fr.script.Calculator;
import com.fr.stable.ParameterProvider;
import com.tptj.plugin.hg.impl.AbstractPreprocessor;
public class DemoPreprocessor extends AbstractPreprocessor {
@Override
public ParameterProvider[] process(Calculator calculator, String s) {
JSONObject jo = new JSONObject(s);
ParameterProvider[] results = new ParameterProvider[1];
results[0] = new Parameter("token", jo.get("token"));
//jo.get("token");
return results;
}
@Override
public String getName() {
return "Demo_PreProcess";
}
@Override
public String getDefaultConfig() {
return "{\n" +
" token:token\n" +
"}";
}
}
package com.fr.plugin.tptj.tabledata.factory.demo;
import com.tptj.plugin.hg.impl.AbstractFormatter;
public class DemoFormatter extends AbstractFormatter {
@Override
public Object format(Object o, String s) {
String data = (String) o;
String[] lines = data.split("\n");
StringBuilder result = new StringBuilder();
for (int i = 1; i < lines.length; i++) {
result.append(lines[i]).append("\n");
}
return result.toString();
}
@Override
public String getName() {
return "Demo_Formatter";
}
@Override
public String getDefaultConfig() {
return "remove first line";
}
}
package com.fr.plugin.tptj.tabledata.factory.demo;
import com.fr.general.data.TableDataException;
import com.fr.log.FineLoggerFactory;
import com.tptj.plugin.hg.impl.AbstractFilter;
import com.tptj.plugin.hg.stable.SimpleDataModel;
import java.util.ArrayList;
import java.util.List;
public class DemoFilter extends AbstractFilter {
@Override
public SimpleDataModel doAction(SimpleDataModel simpleDataModel, String s) {
try {
simpleDataModel.removeColumn(simpleDataModel.getColumnCount() - 1);
List<Object[]> newData = new ArrayList<Object[]>();
for(Object[] row : simpleDataModel.getDatas()) {
Object[] newRow = new Object[row.length - 1];
System.arraycopy(row, 0, newRow, 0, row.length - 1);
newData.add(newRow);
}
simpleDataModel.setDatas(newData);
//simpleDataModel.getDatas().remove(simpleDataModel.getColumnCount() - 1);
} catch (TableDataException e) {
FineLoggerFactory.getLogger().error(e, "{}", e.getMessage());
}
return simpleDataModel;
}
@Override
public String getName() {
return "Demo_Filter";
}
@Override
public String getDefaultConfig() {
return "remove last col";
}
}
package com.tptj.plugin.hg.tabledata.factory;
import com.tptj.plugin.hg.impl.AbstractResolver;
import com.tptj.plugin.hg.stable.SimpleDataModel;
public class DemoResolver extends AbstractResolver {
@Override
public SimpleDataModel parse(Object jsonStr, String others) {
return new SimpleDataModel();
}
public static final String KEY = "Demo";
@Override
public String getName() {
return KEY;
}
@Override
public String getDefaultConfig() {
return "Is a demo";
}
}
package com.tptj.plugin.hg.tabledata.factory.core.loader;
import com.fr.json.JSONObject;
import com.fr.script.Calculator;
import com.fr.stable.ParameterProvider;
import com.tptj.plugin.hg.impl.AbstractLoader;
public class SingleParamLoader extends AbstractLoader {
@Override
public Object load( Calculator calculator, ParameterProvider[] parameters, String others ) {
try{
JSONObject obj = new JSONObject(others);
if( obj.has("data") ){
return obj.optString("data", "{}");
}
}catch(Exception e){
e.printStackTrace();
}
return "";
}
public static final String KEY = "Plugin-Factory_Data_Set_Loader_Type_Single_Parameter";
@Override
public String getName() {
return KEY;
}
@Override
public String getDefaultConfig() {
StringBuilder sb = new StringBuilder();
sb.append("{\r\n")
.append(" data:\"待解析的json字符串,支持${p1}这样的参数和公式\",\r\n")
.append("}\r\n");
return sb.toString();
}
}
SimpleDataModel继承的标准数据集格式。负责存储数据的结构为一个二维表datas和一个一维表cols,分别是具体内
容和列名。调用对应的set,add方法即可向数据集写入数据。
自定义面板接口,很多时候,默认的文本框对于配置来说过于简陋了。如果想要更丰富的UI可以使用这个接口替换默
认的文本框。
package com.tptj.plugin.hg.fun;
import com.fr.stable.fun.mark.Mutable;
import javax.swing.*;
import java.awt.event.FocusListener;
public interface ConfigTable<T extends Configuration> extends Mutable {
String XML_TAG = "TableDataConfigTable";
int CURRENT_LEVEL = 1;
String getName();
/**
* 写入数据到JPanal
* @param config
*/
void setValue(String config);
/**
* 读取数据到
* @return
*/
String getValue();
JPanel getTable(FocusListener listener);
}
使用时继承抽象类
package com.tptj.plugin.hg.fun;
import com.fr.stable.fun.mark.Mutable;
import javax.swing.*;
import java.awt.event.FocusListener;
/**
* 需要泛型继承对应的Configuration进行绑定
* @param <T>
*/
public interface ConfigTable<T extends Configuration> extends Mutable {
String XML_TAG = "TableDataConfigTable";
int CURRENT_LEVEL = 1;
String getName();
/**
* 写入数据到JPanal
* @param config
*/
void setValue(String config);
/**
* 读取数据到
* @return
*/
String getValue();
/**
* 获取配置面板
* @deprecated use {@link ConfigTable#getTable(ParameterRefreshAction action)} instead
* FocusListener触发条件有限,不足以满足所有的需求,建议使用ParameterRefreshAction手动绑定事件
* @param listener FocusListener 事件回调
* @return
*/
JPanel getTable(FocusListener listener);
/**
* 获取配置面板
* @param action 通过{@link ParameterRefreshAction#doAction()} 进行获取刷新事件,自己绑定
* @return
*/
JPanel getTable(ParameterRefreshAction action);
}
功能点注册
<extra-designer>
<TableDataConfigTable class="com.tptj.plugin.hg.tabledata.factory.core.filter.DefaultFilterTextTable"/>
</extra-designer>
完整开发demo见 数据工厂二次开发demo示例
5.数据工厂数据集取数耗时分析
遇到模版加载时间长的问题时,如果怀疑是数据工厂数据集执行慢的话,可以通过分析日志来分析数据工厂数据集的执行时间,包括(装载器执行时间,解析器执行时间,单个数据工厂数据集的总执行时间)
测试步骤:
日志调整成debug级别,预览模版或者数据集,在fanruan.log中搜索如下日志:
1)table data factory eval start ... 含义:---开始执行数据工厂数据集
2)start loading data,config is 含义:----开始执行装载器
3)loading data end,cost 含义:---装载器执行结束
4)start json resolving data ... 含义:--开始执行JSON解析器
5)resolving data end,cost 含义:--解析器执行结束
6)table data factory eval end,cost 含义:–数据工厂执行结束
日志中可见装载器、解析器、数据工厂数据集整体的取数耗时,若装载器执行时间较长时,一般为调用的接口响应时间过长,可通过其他方式调用接口查看响应时间进行问题定位
插件版本要求:2.5.21及以上