标签 二次开发 下的文章

前期,发表过多个系列文章关于如何无缝扩展 ThingsBoard 功能,其实在实施过程中,还是遇到很多问题,需要提前注意!

独立于 ThingsBoard 外的功能模块注意要点

  • 功能模块需要能够独立运行,不管是服务也好,controller 也罢,都要能够正常运行,就类似单元测试跑通,才可以融入到 ThingsBoard 中;
  • 数据表要独立成 sql 文件,便于移植到 ThingsBoard 库中,最好不要跟原有数据表有关联或冲突,尤其不能冲突;

    • 调用系统库表,新建的 Entity 要注意 column 名字于原有一样,使用 @Column(name = "foo_bar"),否则 Bean 建立失败;
    • 如果 Entity 有嵌套调用 POJO,需要将 Entity 设置成 @Proxy(lazy = false),关闭懒加载,否则无法查询数据;
  • 打包功能模块,需要将 Spring Boot 项目 target 中的 .original 文件安装到本地 maven 库,或者修改 pom.xml 文件来改变 maven 打包规则;
  • 功能模块中的附件,图片,pdf 模板,直接在功能模块 jar 包中,不需要单独导入 ThingsBoard 中;
  • Controller 要单独命名,不能与 ThingsBoard 冲突,API 路径可以复用原有路径,比如:原有是 /api/report/foo,可以增加新的 /api/report/bar;
  • Domain/POJO/Repository等,要独立命名,因为我们以 maven jar 包方式融入 ThingsBoard,都交给 spring 来管理,所以命名要独立;

本文所有内容都来自原创,思路开阔,敢想敢做,经过不懈努力,终于成功扩展 ThingsBoard 功能模块

先来看一段视频,体验一下新增的功能,前端增加的菜单和功能演示。
tb-v.mp4

前言

此次二次开发,主要是针对 ThingsBoard 新增功能模块,以报表功能为目的,提供一个可以独立与原有功能的 Report/报表 模块,咱们先这么来定义。就像 Device/设备、Asset/资产模块,增删改查必不可少,还需要兼容 ThingsBoard 原有操作习惯,还可以将 Report/报表 分配给特定的 Customer/客户。上图,这是开始的设想:
tb1.png

后端

代码结构

modified:   application/src/main/java/org/thingsboard/server/controller/BaseController.java
new file:   application/src/main/java/org/thingsboard/server/controller/ReportController.java
modified:   application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
modified:   application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
modified:   application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
modified:   application/src/main/resources/thingsboard.yml
new file:   common/dao-api/src/main/java/org/thingsboard/server/dao/report/ReportService.java
modified:   common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
modified:   common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/Report.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/ReportInfo.java
modified:   common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/id/ReportId.java
modified:   common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/query/ReportSearchQueryFilter.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/query/ReportTypeFilter.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/report/ReportSearchQuery.java
new file:   dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractReportEntity.java
new file:   dao/src/main/java/org/thingsboard/server/dao/model/sql/ReportEntity.java
new file:   dao/src/main/java/org/thingsboard/server/dao/model/sql/ReportInfoEntity.java
new file:   dao/src/main/java/org/thingsboard/server/dao/report/ReportDao.java
new file:   dao/src/main/java/org/thingsboard/server/dao/report/ReportServiceImpl.java
modified:   dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
modified:   dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java
new file:   dao/src/main/java/org/thingsboard/server/dao/sql/report/JpaReportDao.java
new file:   dao/src/main/java/org/thingsboard/server/dao/sql/report/ReportRepository.java
modified:   pom.xml

根据 git 提供的log,看出来新增文件比修改文件多,也是此次二次开发的难点所在。

  • 新增文件:新增的文件主要以扩展功能模块为主,大概是这样:POJO(DTO,ID),Entity,Repository,Dao,Service/Impl,Controller,基本都是依葫芦画瓢;
  • 修改文件:因为要达到 ThingsBoard 原有的一些功能体验,势必要满足其实现逻辑,那就是找到所有的 Device 功能模块的做法,在所有需要的地方增加 Report 功能,无非就是一些新增枚举值,方法调用,新增 case 判断;

这个开发过程中,我发现,频繁读取一些 API,且都是需要数据库交互,如果用户数量大,不是一个好方法,这里举一个例子:

  • 获取实体类型频繁数据库交互
    types.png

调用后台 API 为: /api/report/types,追查代码发现其实是一次 SELECT,瞬间尴尬了

@Query("SELECT DISTINCT d.type FROM ReportEntity d WHERE d.tenantId = :tenantId")
List<String> findTenantReportTypes(@Param("tenantId") UUID tenantId);
  • WEB API,大概就是把 device 都替换成了 report,其他逻辑上没有做更改,为了配合前端的调用,这些都是必不可少的内容。因为用 device 衍生过来,把 cridentail 相关内容删除了。
    tb3.png

数据表

CREATE TABLE public.report
(
    id uuid NOT NULL,
    created_time bigint NOT NULL,
    additional_info character varying COLLATE pg_catalog."default",
    customer_id uuid,
    type character varying(255) COLLATE pg_catalog."default",
    name character varying(255) COLLATE pg_catalog."default",
    label character varying(255) COLLATE pg_catalog."default",
    search_text character varying(255) COLLATE pg_catalog."default",
    tenant_id uuid,
    CONSTRAINT report_pkey PRIMARY KEY (id),
    CONSTRAINT report_name_unq_key UNIQUE (tenant_id, name)
)
  • 如果你观察仔细,发现跟 device 表几乎一样,那是因为我们以 device 为原型做的二次开发。开发自己功能时,表的定义请随意啊。不过,name, type, search_text, id, customer_id, tenant_id, create_time 都是不能少的,因为框架业务逻辑有代码实现一些功能;
  • 建议增加几个不影响 ThingsBoard 框架工作的“扩展字段”,也可以直接使用 additional_info 来存储自己的数据,这个字段最长可以有 1G,足够放你需要的结构化或非结构化数据;类似:
{
    "hello": "world",
    "thingsboard": "iot",
    "company": "yiqisoft",
    "location": "shanghai",
    "protocol": ["coap", "mqtt", "http"]
}
  • 自由业务逻辑,针对“扩展字段”的操作,请随意,数据频繁修改的还是用自己的业务表,不要混为一谈。

业务逻辑实现

tb2.png
对于自己的业务逻辑开发方面,建议不要改动 ThingsBoard 的代码结构,可以使用外挂一个 jar,来独立实现业务功能,这个具体要看业务复杂度,是不是需要与 ThingsBoard 耦合,可以查看前期我们发布的文章:如何无缝扩展 ThingsBoard 功能?原来二次开发如此简单!如何无缝扩展 ThingsBoard 功能?原来二次开发如此简单!【续】

前端

代码结构

modified:   ui-ngx/src/app/core/http/entity.service.ts
new file:   ui-ngx/src/app/core/http/report.service.ts
modified:   ui-ngx/src/app/core/services/menu.service.ts
modified:   ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts
modified:   ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts
modified:   ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts
modified:   ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts
modified:   ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts
modified:   ui-ngx/src/app/modules/home/pages/customer/customer.component.html
modified:   ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts
modified:   ui-ngx/src/app/modules/home/pages/home-pages.module.ts
new file:   ui-ngx/src/app/modules/home/pages/report/report-routing.module.ts
new file:   ui-ngx/src/app/modules/home/pages/report/report-table-header.component.html
new file:   ui-ngx/src/app/modules/home/pages/report/report-table-header.component.scss
new file:   ui-ngx/src/app/modules/home/pages/report/report-table-header.component.ts
new file:   ui-ngx/src/app/modules/home/pages/report/report-tabs.component.html
new file:   ui-ngx/src/app/modules/home/pages/report/report-tabs.component.ts
new file:   ui-ngx/src/app/modules/home/pages/report/report.component.html
new file:   ui-ngx/src/app/modules/home/pages/report/report.component.scss
new file:   ui-ngx/src/app/modules/home/pages/report/report.component.ts
new file:   ui-ngx/src/app/modules/home/pages/report/report.module.ts
new file:   ui-ngx/src/app/modules/home/pages/report/reports-table-config.resolver.ts
modified:   ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
modified:   ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts
modified:   ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
modified:   ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts
modified:   ui-ngx/src/app/shared/components/footer.component.html
modified:   ui-ngx/src/app/shared/models/alias.models.ts
modified:   ui-ngx/src/app/shared/models/entity-type.models.ts
new file:   ui-ngx/src/app/shared/models/id/report-id.ts
new file:   ui-ngx/src/app/shared/models/report.models.ts
modified:   ui-ngx/src/assets/locale/locale.constant-en_US.json
modified:   ui-ngx/src/assets/locale/locale.constant-zh_CN.json
modified:   ui-ngx/src/assets/logo_title_white.svg
modified:   ui-ngx/src/environments/environment.prod.ts
modified:   ui-ngx/src/environments/environment.ts
modified:   ui-ngx/src/index.html
new file:   ui-ngx/src/yiqisoft.ico
  • 先复制一份 ui-ngx/src/app/modules/home/pages/device 到 ui-ngx/src/app/modules/home/pages/report,就是组件要完整,文件名都改掉,内部都是替换操作: Device -> Report, device -> report, DEVICE -> REPORT,这样我们的 ReportModule 就做好了;
  • 在 home-pages.module.ts 载入 ReportModule ;
  • 在 ui-ngx/src/app/core/services/menu.service.ts 增加需要的菜单;
  • 根据 report 目录里面的内容,需要从 device 功能模块复制一些组件和 service,挺多的,自己慢慢找,也算是熟悉一下前端代码;
  • 汉化,就是修改 ui-ngx/src/assets/locale/ 目录下相应的语言包,很容易;

功能扩展过程

前期,我们发布了文章,如何进行 ThingsBoard 前端二次开发?超强 Angular 框架帮你忙。,介绍如果用 Angular 二次开发的要点,具体看开发者对于 Angular 框架掌握程度。

功能扩展建议

  • 像上面的实体类型获取,增加功能特别不容易,因为代码实现是通过枚举值来 switch/case 来判断,那么新增功能模块时,需要改动太大了,如果能够独立到每个功能模块的组件中,那就方便多了。但是,前端代码就会有很多重复。为了扩展 REPORT,必须要找到所有的 case,上代码:
    switch (entity.id.entityType as EntityType | string) {
      case 'function':
        materialIcon = 'functions';
        break;
      case EntityType.DEVICE:
        materialIcon = 'devices_other';
        break;
      case EntityType.REPORT:
        materialIcon = 'equalizer';
        break;
      case EntityType.ASSET:
        materialIcon = 'domain';
        break;
    }
  • 更改 LOGO,需要使用工具做一个 svg 矢量图,替换 ui-ngx/src/assets/logo_title_white.svg 即可;
  • 更改 Swagger 显示内容需要改 thingsboard.yml;

欢迎大家一起来加入 ThingsBoard 讨论,可以在下方留言给作者。其实,我是最不建议在 ThingsBoard 上改动,我崇尚无缝扩展,但是太难了,所以才有了本文的发表,把自己的业务融入进去是一次尝试。当然,有能力的朋友可以自己独立做业务系统,对于简单物联网应用,何必重复制造轮子呢?

众所周知,ThingsBoard 核心采用 Spring Boot 开发,当然,很多人已经在 Spring Boot 之上平步青云,那如何无缝扩展 ThingsBoard 功能,而不改变原有代码结构,且将来无缝升级?

创建自定义 Spring Boot Starter,实现功能模块

新建 Spring Boot Starter 项目

  • 名称定义为了方便识别: xxx-xxx-spring-boot-starter
    project.png
  • 增加依赖,看需求,自己的项目需要,spring-boot-autoconfigure,必不可少!
    depends.png
  • 完整pom.xml,自己细品

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.3.4.RELEASE</version>
            <relativePath /> <!-- lookup parent from repository -->
        </parent>
        <groupId>cn.yiqisoft</groupId>
        <artifactId>report-spring-boot-starter</artifactId>
        <version>0.0.1</version>
        <name>EcolabReport</name>
        <description>Ecolab Report</description>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web-services</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-autoconfigure</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            
            
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>

新建controller,为了方便测试,简单即可

  • 新建 controller 文件

    package cn.yiqisoft.ecolabanalysis.controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    @RestController
    public class ReportController {

    @RequestMapping(value = "/api/report", method = RequestMethod.GET)
    public String reportList() {
        return "Report List";
    }

    }

自动装载工程

  • 新建 src/main/resources/META-INF/spring.factories 文件

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    cn.yiqisoft.ecolabanalysis.controller.ReportController
    
  • 通知 Spring Boot 启动时自动装载我们的 Controller

打包,jar 包输出

命令行编译,并安装到本地 maven 库

mvn clean install

生成 ~/.m2/repository/cn/yiqisoft/report-spring-boot-starter/0.0.1/ecolab-report-spring-boot-starter-0.0.1.jar,这样本地其他项目依赖此 jar 包即可。

【可选】本地运行项目,独立测试都没问题

命令行启动项目

mvn spring-boot:run

效果

jiekechoo@jiekechoo ~ % curl http://localhost:8080/api/report
Report List
jiekechoo@jiekechoo ~ % 

融入 ThingsBoard ,实现无缝二次开发

自定义 starter 分析

  • 我们的 maven 库

    <groupId>cn.yiqisoft</groupId>
    <artifactId>report-spring-boot-starter</artifactId>
    <version>0.0.1</version>
    
  • 加入 ThingsBoard application 工程依赖,在 application/pom.xml 加入我们的 maven 库即可
    pom.png

运行 ThingsBoard application 工程

  • 打开 swagger ,我们二次开发的模块已经被成功载入
    swagger.png

扩展

  • 即使 ThingsBoard 升级,我们单独 jar 包也相应升级即可;
  • 独立使用自己的二次开发数据库表,不影响 TB 本身;
  • 如果有能力,将自定义 jar 做成动态加载,实现更高级别。