UReport2+SpringCloud的集成,实现报表引擎

该文档用于讲解如何实现UReport2在SpringCloud的集成

概要

由于目前实行的Saas化业务系统,各租户都有通用的以及个性的报表需求。而这些报表需求如果按照传统的开发模式,会出现研发周期长的问题;且部分用户会提出短期的数据报表需求,并且会反复变更。这更加加大了整个研发成本

所以我们寻找了相关的替代方案用于解决该问题,此处使用了UReport2

适用场景

针对客户需求,完成数据简单(靠SQL可完成)对UI要求不高(缺少绚丽报表)的基础数据呈现工作,用于报表展示。

不适用以下场景:

  • 高度自由化的报表呈现(复杂布局)
  • 需要深度挖掘统计的数据(大数据)
  • 数据大屏展示(UI绚丽)

PS: 以上内容会与Ant-design的UI存在差异

可实现效果
  • 查询表格
  • 统计表格
  • 统计图表(柱状/折线等等)
  • 下载打印

教程: https://www.w3cschool.cn/urep...

参考文档:
https://www.jianshu.com/p/652...
https://blog.csdn.net/qq_3517...
https://www.cnblogs.com/Seven...

本文除简单讲解在SpringBoot+SpringCloud集成外,会额外讲解最终业务应用场景。主要包含以下

  • 个人使用的案例分享
  • 报表引擎在前端React项目应用
  • 自定义xml存储
  • 租户下如何应用报表引擎

基础配置

POM.xml
  
  
 com.bstek.ureport  
 ureport2-console  
 2.2.9  


  
  
 org.springframework.boot  
 spring-boot-starter-jdbc  
  
  
  
 mysql  
 mysql-connector-java  
 8.0.16  
  
  
  
 org.mybatis.spring.boot  
 mybatis-spring-boot-starter  
 2.0.0  
  
  
  
 com.alibaba  
 druid-spring-boot-starter  
 1.1.10  


  
 org.springframework.cloud  
 spring-cloud-starter-eureka  
  
  
 org.springframework.cloud  
 spring-cloud-starter-config  

  
 org.glassfish.jersey.core  
 jersey-common  
 2.27  
ureport.properties
# 将ureport.disableFileProvider改成true,即可禁用默认提供的文件存储机制
ureport.disableHttpSessionReportCache=true
ureport.disableFileProvider=false
ureport.fileStoreDir=/WEB-INF/ureportfiles
ureport.debug=true
application.properties
\# 指定springboot内嵌容器启动的端口  
server.port\=8080  
server.servlet.context-path\=/  
server.uri-encoding\=utf-8  
\# datasource参数配置  
spring.datasource.druid.dbType\=com.alibaba.druid.pool.DruidDataSource  
spring.datasource.druid.url\=${xxx.datasource.business.url}  
spring.datasource.druid.username\=${xxx.datasource.business.username}  
spring.datasource.druid.password\=${xxx.datasource.business.password}  
spring.datasource.druid.driverClassName\=${xxx.datasource.business.driver-class-name}  
\# 连接池配置  
\# 初始化大小,最大连接数,最小连接数  
spring.datasource.druid.initial-size\=1  
spring.datasource.druid.max-active\=20  
spring.datasource.druid.min-idle\=1  
\# 配置获取连接等待超时的时间  
spring.datasource.druid.max-wait\=60000  
\# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒  
spring.datasource.druid.time-between-eviction-runs-millis\=60000  
\# 配置一个连接在池中最小生存的时间,单位是毫秒  
spring.datasource.druid.min-evictable-idle-time-millis\=300000  
\# config,stat,wall,log4j 配置监控统计拦截的filters,去掉后监控界面SQL无法进行统计,'wall'用于防火墙  
spring.datasource.druid.filters\=stat  
\##### druid 监控 ####\## WebStatFilter配置  
spring.datasource.druid.web-stat-filter.enabled\=true  
spring.datasource.druid.web-stat-filter.url-pattern\=/\*  
spring.datasource.druid.web-stat-filter.exclusions\=\*.js,\*.gif,\*.jpg,\*.png,\*.css,\*.ico,/druid/\*  
spring.datasource.druid.web-stat-filter.session-stat-enable\=false  
spring.datasource.druid.web-stat-filter.session-stat-max-count\=1000  
spring.datasource.druid.web-stat-filter.principal-session-name\=admin  
spring.datasource.druid.web-stat-filter.principal-cookie-name\=admin  
spring.datasource.druid.web-stat-filter.profile-enable\=true  
\## StatViewServlet配置  
spring.datasource.druid.stat-view-servlet.enabled\=true  
spring.datasource.druid.stat-view-servlet.url-pattern\=/druid/\*  
spring.datasource.druid.stat-view-servlet.reset-enable\=true  
spring.datasource.druid.stat-view-servlet.login-username\=admin  
spring.datasource.druid.stat-view-servlet.login-password\=admin  
  
\# mybatis实体类包  
mybatis.typeAliasesPackage\=com.xxx.report.entity  
\# mybatis mapper 文件的位置  
mybatis.mapperLocations\=classpath:mapper/\*.xml  
  
\# 日志配置  
logging.level.root\=INFO  
logging.level.org.springframework.web\=DEBUG  
logging.level.org.mybatis\=DEBUG  
logging.level.org.apache.ibatis\=DEBUG  
logging.level.java.sql.Connection\=DEBUG  
logging.level.java.sql.Statement\=DEBUG  
logging.path\=/logs/myreport  
\# ureport Mysql 存储  
\# 前缀  
ureport.mysql.provider.prefix\=report-  
\# 是否开启mysql存储,false为开启  
ureport.mysql.provider.disabled\=false  
  
\# SpringCloud  
spring.application.name\=skp-report  
info.owner\= [email protected]  
info.version\= @project.version@
bootstrap.yml
eureka:  
  instance:  
    preferIpAddress: true  
    lease-renewal-interval-in-seconds: 10  
    lease-expiration-duration-in-seconds: 30  
    instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${server.port}  
  client:  
    serviceUrl:  
      defaultZone: http://root:[email protected]:38761/eureka  
    healthcheck:  
      enabled: false  
spring:  
  cloud:  
    inetutils:  
      ignoredInterfaces:  
        - docker0  
        - veth.\*  
    config:  
      discovery:  
        enabled: true  
        service-id: discovery  
      label: master  
      profile: ${spring.profiles.active:dev}  
      name: ${spring.application.name:gateway}  
      username: admin  
      password: admin  
  http:  
    encoding:  
      charset: UTF-8  
      force: true  
      enabled: true  
    multipart:  
      enabled: true  
      max-request-size: 350MB  
      max-file-size: 350MB  
  application:  
    name: skp-report  
  boot:  
    admin:  
      username: ${security.user.name:admin}  
      password: ${security.user.password:admin}  
server:  
  port: 8080  
  tomcat:  
    uri-encoding: UTF-8  
    max-http-post-size: 10485760  
management:  
  security:  
    enabled: false  
info:  
  owner: [email protected]  
  version: 1.0  
hystrix:  
  propagate:  
    request-attribute:  
      enabled: true  
  command:  
    default:  
      execution:  
        timeout:  
          enabled: true  
        isolation:  
          thread:  
            timeoutInMilliseconds: 360000  
ribbon:  
  ReadTimeout: 300000  
  ConnectTimeout: 300000  
  MaxAutoRetries: 0  
  MaxAutoRetriesNextServer: 1  
logging:  
  level.root: info  
  path: logs/  
  file: ${spring.application.name}.log  
redis:  
  database: 0  
  host: ${spring.redis.host}  
  port: ${spring.redis.port}  
  password: ${spring.redis.password}  
  pool:  
    max-active: ${spring.redis.jedis.pool.max-active}  
    max-wait: ${spring.redis.jedis.pool.max-wiat}  
    max-idle: ${spring.redis.jedis.pool.max-idle}  
    min-idle: 0  
  timeout: 0
ReportApplication
@ImportResource("classpath:ureport-console-context.xml")  
@SpringBootApplication(scanBasePackages \= "com.xxx")  
@EnableDiscoveryClient  
public class ReportApplication {  
  
   public static void main(String\[\] args) {  
        SpringApplication.run(ReportApplication.class, args);  
  }  
  
  @Bean  
  public ServletRegistrationBean reportServlet() {  
        ServletRegistrationBean bean=new ServletRegistrationBean(new UReportServlet());  
  bean.addUrlMappings("/ureport/\*");  
 return bean;  
  }
}

以上配置完毕,访问以下网址即可

http://localhost:8080/ureport/designer

个人使用的案例分享

什么是数据源

数据源可以理解为数据库,但不建议直接使用业务库进行报表统计。会严重拖慢业务进行速度,建议使用报表库。

配置数据库

在右侧选择浮层,选择数据源,在点击最左边数据库图表。添加数据库连接

后续填入以下内容

内容 描述 参考
数据源名称 数据源别名 test
连接用户名 数据库账户 root
连接密码 数据库密码 xxxx
驱动名称 数据库驱动 com.mysql.cj.jdbc.Driver
连接URL 数据库连接 jdbc:mysql://xxx:3306/xxx

填写完成后,可以看到右侧出现了该别名的数据源

配置数据源

右键点击该数据源,在菜单中点击添加数据集

  • 此时左侧为表名一栏
  • 右上角填写数据集名称
  • 右中间添加SQL
  • 右下方定义传入的参数字段(例如分页,按某条件查询,支持从输入框和下拉框联动过来)

以下以'总条码数查询'为例

分组 字段 内容 备注
头部 数据集名称 总条码数查询 -
查询 SQL select count(id) as count from xxx -
查询参数 - - -

可以点击预览数据获取结果

保存之后,可以在右侧看到结构数据

  • dev
    • 总条码数查询
      • count
配置界面

左侧有类似于excel表格的内容,他就是我们用于进行呈现的地方。

一般选中单元格,在右侧属性栏进行操作

例子

单元格 单元格类型 属性
A1 普通文本 {文本内容:总条码数查询}
A2 数据集 {数据集:总条码数查询,属性:count}

PS:也可以使用右侧数据源-数据集-属性 双击,快速填充到单元格。

预览报表

在左上角预览报表,点击预览可以浏览,可以在新窗口查看到结果

例子如下

总条码数查询
53
保存报表

在左上角保存报表,点击保存可以存储到数据库,例如文件名 barcodecount。

下次可以在打开报表文件中找到该文件,例如barcodecount.ureport.xml

PS:保存的同时,会将数据源一并保存

应用报表

一般使用如下格式进行显示html,我们会将它嵌入到我们的业务系统中使用。

http://127.0.0.1:8080/ureport/preview?_u=report-barcodecount.ureport.xml

参数内容如下

参数 描述 参考
_u 报表文件 report-barcodecount.ureport.xml
_t 显示按钮 1,3 or 0

参考资料:

报表引擎在前端React项目应用

一般该界面会嵌入到一些业务系统中使用,此处以React项目为例

建立以下组件,用于替代所有动态表格的使用

主要实现思路为

  • 由后台配置菜单,在启动系统时,将该url以及对应的ureport的url一并传给前端
  • 将ureport的url,使用iframe打开
  • 追加token在该url的参数中,用于用户身份的判断
ReportFrame.tsx
import React, { Fragment } from 'react';
import { connect } from 'dva';
import { Row, Col, Form, Button, Select, Input, Card, Divider, Popconfirm, message } from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import { FormComponentProps } from 'antd/lib/form/Form';
import ReactDOM from 'react-dom';

const styles = require("@/pages/common.less");

const FormItem = Form.Item;

// 查询表格的属性
interface IReportFrameProps extends FormComponentProps {
    dispatch: IDispatchDefine,
    loading: boolean,
    loginStatus: boolean,
    // 菜单数据
    menuData: any
    // 路由数据
    match?: any
}

// 查询表格的状态
interface IReportFrameState {
    iFrameHeight: string,
    // 嵌入的路径
    frameUrl: string,
}
@connect(({ menu, login }) => ({
    loginStatus: login.status,
    menuData: menu.menuData
}))
class ReportFrame extends React.Component{

    constructor(props) {
        super(props);

        this.state = {
            iFrameHeight: '600px',
            frameUrl:null
        }

    }

    public componentDidMount() {
        const nodes=TreeDataUtils.loopTreeToList(this.props.menuData,'children',(data)=>data.path===this.props.match.url)
        if(nodes&&nodes.length>0){
            let frameUrl=nodes[0].frameUrl
            if (localStorage.getItem("user-identity") && JSON.parse(localStorage.getItem("user-identity")) && JSON.parse(localStorage.getItem("user-identity")).token) {
                frameUrl+= "&token="+JSON.parse(localStorage.getItem("user-identity")).token;
            }
            this.setState({
                frameUrl
            })
        }
    }

    public findTreeNode(){

    }

    public render() {
        const frameUrl = this.state.frameUrl;
        return (
            
                
                    {frameUrl ?