Bean数据集

教学视频

概述

Bask Report 支持 Bean 数据集:你可以在当前集成 BaskReport 的 Java 项目中,通过 Java API 为 Dataset 提供 Bean 类型的数据源。

如果数据可以通过 Restful 接口获取,强烈建议使用统一数据源中的 API 数据源 代替 Bean 数据集。

Bask Report 基于 java.util.ServiceLoader 机制加载自定义的 Bean 数据源,对应的抽象类全路径为 com.basksoft.report.core.model.dataset.BeanDataset。

核心抽象类

自定义数据集需继承 BeanDataset 并实现以下方法:

public abstract class BeanDataset extends Dataset {
    public BeanDataset() {
        super(null,null);
    }
    /**
     * @param context 上下文对象,从中可以获取报表参数,和需要的单元格值
     * @return 返回数据集数据
     */
    public abstract List<?> getData(BeanContext context);

    /**
     * @return 返回当前数据集在计算时需要依赖的单元格名称,如果不依赖单元格返回null或空的List
     */
    public abstract List<String> getDependCells();

    /**
     * @return 返回当前数据集名称,不能返回空
     */
    protected abstract String name();

    /**
     * @return 返回当前数据集对应的字段集合,不能为空
     */
    public abstract List<Field> getFields();

    @Override
    public final String getName() {
        return name();
    }
}

其中,BeanContext 提供了以下上下文信息:

  • parameters:报表参数集合变量,可用于查询过滤数据的参数判断。参数定义可参考:报表参数。
  • cellsMap:当前报表计算所需的单元格值集合。
  • tenantId:当前请求的租户标识(由框架在构造 BeanContext 时通过 ContextHolder.getTenantId() 注入),用于在 getData 内区分不同租户的数据。

在 getData 中按租户取数的示例:

@Override
public List<?> getData(BeanContext context) {
    String tenantId = context.getTenantId();   // 当前租户标识
    Object param = context.getReportParameter("date"); // 报表参数
    // 根据 tenantId 与 param 查询并返回数据
    return queryByTenant(tenantId, param);
}

由于 Bean 数据集是全局共享、与租户定义无关(见「特别说明(全局性)」),不要在实现类中缓存租户状态,而应每次取数时通过 context.getTenantId() 获取。

分类

在设计器的数据集选择列表中,Bean 数据集会按 分类 进行分组展示,便于在数据集较多时快速定位。每个数据集的分类由以下规则确定(优先级从高到低):

  1. SPI 配置文件声明(推荐):在 META-INF/services/com.basksoft.report.core.model.dataset.BeanDataset 文件中,每行登记一个实现类,并可附带 # 分类名 声明其所属分类。例如:

    # BeanDataset SPI 注册文件,每个实现类占一行
    # 格式:<实现类全限定名> [ # <分类名> ]
    
    com.demo.TaskDataset # 任务统计
    com.demo.UserDataset # 用户中心
    com.demo.BillDataset # 海运
    com.demo.SeaBookingDataset # 海运
    com.demo.AirCargoDataset # 空运
    
    • 以 # 开头的行视为注释,空行会被忽略;# 前后允许有空格。
    • 同一分类名下可登记多个数据集,设计器会将其归并为同一分组(如 海运 (2))。
    • 同一实现类只需登记一次;重复登记(如示例文件中的 TestBeanDataset 出现两次)会被框架去重,建议保持唯一。
  2. 重写 getCategory() 方法:在 BeanDataset 子类中重写该方法,返回自定义分类名称。

  3. 默认分类:若既未在 SPI 中声明也未重写方法,则统一归入 默认分类。

public String getCategory() {
    return "默认分类";
}

三种方式的优先级为:SPI 声明 > getCategory() 返回值 > 默认分类。当同一分类下存在多个数据集时,设计器会以分组标题(如 任务统计 (3))的形式展示数量。

前期准备(ServiceLoader 定义)

按以下步骤使用 Bean 数据集:

  1. 在 resources 资源目录下创建 META-INF/services 文件夹。

  2. 在 services 文件夹中创建文件,以接口全名命名:com.basksoft.report.core.model.dataset.BeanDataset。

  3. 创建接口实现类,参考代码:

    public class TaskDataset extends BeanDataset {
     @SuppressWarnings("unused")
     private static int number = 1;
    
     @Override
     public List<?> getData(BeanContext context) {
         List<Map<String,Object>> datas=new ArrayList<Map<String,Object>>();
         Map<String, Object> data = new HashMap<String, Object>();
         data.put("collectionAll", RandomUtils.nextInt(5000000));
         data.put("category1", RandomUtils.nextInt(30));
         data.put("category2", RandomUtils.nextInt(3000));
         data.put("category3", RandomUtils.nextInt(30));
         data.put("category4", RandomUtils.nextInt(30));
         data.put("category5", RandomUtils.nextInt(30));
         data.put("category6", RandomUtils.nextInt(300000));
         datas.add(data);
         return datas;
     }
    
     @Override
     public List<String> getDependCells() {
         return null;
     }
    
     @Override
     protected String name() {
         return "执行任务统计";
     }
    
     @Override
     public List<Field> getFields() {
         List<Field> fields = new ArrayList<Field>();
         fields.add(new Field("collectionAll", "统计所有", FieldType.Integer));
         fields.add(new Field("category1", "分类1", FieldType.Integer));
         fields.add(new Field("category2", "分类2", FieldType.Integer));
         fields.add(new Field("category3", "分类3", FieldType.Integer));
         fields.add(new Field("category4", "分类4", FieldType.Integer));
         fields.add(new Field("category5", "分类5", FieldType.Integer));
         fields.add(new Field("category6","分类6", FieldType.Integer));
         return fields;
     }
    
     }
    
  4. 在 META-INF/services/com.basksoft.report.core.model.dataset.BeanDataset 文件中登记实现类,每行一个,如需指定分类则追加 # 分类名:

    xxx.xxx.xxx.TaskDataset # 任务统计
    xxx.xxx.xxx.BillDataset # 海运
    

    详见上文「分类」一节:未声明 # 分类名 时归入 默认分类;分类按 SPI 声明 > getCategory() 返回值 > 默认分类 的优先级确定。

设计器使用

在数据源设置面板中,选择第一个数据集标签页,并单击添加按钮:

image-20210412123947359

在弹出菜单中选择创建自定义 Bean 数据集:

image-20210412144304563

在弹出向导窗口中选择需要添加的 Bean 数据集对象:

BeanDataset

选中 Bean 数据集对象,并单击确定按钮,即完成 Bean 数据集的添加。

特别说明(全局性)

Bean 数据集是通过 java.util.ServiceLoader 在 JVM 进程内 加载的,属于 全局共享 资源,与租户无关:

  • 一经注册,所有租户在设计器中选择 Bean 数据集时,看到的都是同一份实现类列表,不存在按租户隔离的 Bean 数据集。
  • 数据集的 getData(BeanContext) 等方法在每次取数时都会重新调用,因此不要在实现类中缓存与租户相关的状态(如通过 ThreadLocal、静态变量保存租户数据),否则会造成租户间数据串扰。
  • 若不同租户需要不同的取数逻辑,应在方法体内根据 BeanContext 中的报表参数自行区分,而非依赖 Bean 数据集层面的租户隔离。

实例全局唯一:BeanDataset 实现类经 ServiceLoader 加载后,由 BeanDatasetUtils 以 static 集合缓存且仅创建一个实例,所有报表、所有请求通过 getBeanDataset(name) 拿到的都是同一个对象引用。因此:

  • 不同报表使用同一个 Bean 数据集时,会共用同一个实例;若实现类中保存了可变实例字段(或静态字段),报表之间会相互覆盖、并发时还会产生数据串扰。
  • 实现类必须保持无状态:所有可变数据(查询结果、临时变量等)都应在 getData(BeanContext) 方法体内以局部变量产生,不写入实例字段或静态字段。
  • 若确实需要缓存(如耗时查询),缓存 key 必须包含 tenantId 与报表参数,并注意并发安全。

设计器计算字段相互隔离:在报表设计器中为 Bean 数据集添加的计算字段不会在不同报表间相互影响。原因是框架在构建报表时会通过 new BeanDatasetWrapper(beanDataset) 每次新建一个包装器对象,并将该报表配置的计算字段 regist 到这个 Wrapper 上(DatasetBuilder 中的 registComputedFieldMap),而非写入那个全局唯一的 BeanDataset 实例。由于每个报表构建都会生成各自独立的 Wrapper,因此报表 A 的计算字段只存在于报表 A 的 Wrapper 中,不会串到报表 B。

注意:上述隔离成立的前提是计算字段由设计器配置、经框架注入。若你在 BeanDataset 实现类的 getFields() 中自行动态返回计算字段并依赖了可变状态,则仍会受"实例全局唯一"影响而相互干扰,应避免这种做法。

简而言之:Bean 数据集是"全局定义、实例唯一、按调用隔离数据、按报表隔离计算字段",而非"按租户或按报表隔离的定义"。

获取当前租户

该功能自 2.3.2+ 版本开始支持,低版本 BeanContext 无 getTenantId() 方法。

虽然 Bean 数据集的定义是全局共享、与租户无关的,但在一次具体的取数调用中,框架会自动将当前租户标识注入到 BeanContext 中,供实现类使用:

  • 租户标识来源于 com.basksoft.core.ContextHolder.getTenantId(),由框架在构造 BeanContext 时自动传入(构造方法签名为 BeanContext(parameters, cellsMap, tenantId))。
  • 实现类通过 context.getTenantId() 获取,无需自行解析 ContextHolder。
  • 若取数发生在异步线程或由定时任务触发,ContextHolder 中的租户上下文可能为空,此时可在调用处显式传入租户标识:

    // 调用方显式指定租户,避免异步场景丢失
    dataset.getData(new BeanContext(parameters, cellsMap, currentTenantId));
    

注意事项

如果项目整合了 Spring,需要注意的是,在通过 SPI 创建 ServiceLoader 时,Spring 尚未完成初始化,因此以下用法不会生效:

public class TaskDataset extends BeanDataset {
    @SuppressWarnings("unused")
    private static int number = 1;

    // spring中的annotation标记不会起作用
    @Value("smtp.user")
    private String smtpUser;

    // spring中的annotation标记不会起作用
    @Autowired
    private DataService dataservice;

    // 直接通过Spring的ApplicationContext获取Bean也是不行的
    private UserService userService = SpringUtils.getBean(UserService.class);    

    ...

}

正确的做法是在方法内部访问 Spring 资源,例如:

public class TaskDataset extends BeanDataset {
    @SuppressWarnings("unused")
    private static int number = 1;

    @Override
    public List<Field> getFields() {
        FieldService fieldService = SpringUtils.getBean(FieldService.class)
        ...
    }

    ...

}

results matching ""

    No results matching ""