ThingsBoard单体架构的项目构建逻辑

ThingsBoard的项目用一个工程实现了单体和微服务两种架构,能做到重用大量的代码同时又具有横向扩展能力。

而本文研究的重点是:ThingsBoard单体架构的应用是怎么构建打包出来的?

1. 工程构建的交付物

首先了解一下工程的结构,通过tree命令可以查看项目结构:

tree -I "node_modules|target|src|pom.xml" -P "pom.xml" ./thingsboard/ > tree.txt

对一些与分析无关的子目录再手工删除一下,大致看了一下每个模块里面代码,得出结果:

./thingsboard/
├── application # 项目主程序模块,单体架构时包含所有功能模块于一体
├── common      # 公共模块
│   ├── actor   # 自己实现的actor系统
│   ├── dao-api # 数据库查询接口
│   ├── data    # 域模型(数据库表对应的Java类)
│   ├── message # 实现系统的消息机制
│   ├── queue   # 消息队列
│   ├── stats   # 统计
│   ├── transport # 接收设备消息的服务端
│   │   ├── coap  
│   │   ├── http  
│   │   ├── mqtt  
│   │   └── transport-api
│   └── util      # 工具
├── dao           # 数据库查询接口的实现类
├── docker        # 用于实现微服务架构的部署,docker部署相关的配置
│   ├── haproxy   
│   ├── tb-node
│   └── tb-transports
│       ├── coap
│       ├── http
│       └── mqtt
├── img           # 放logo的
├── k8s           # k8s部署相关配置
│   ├── basic
│   ├── common
│   └── high-availability
├── msa             # 实现微服务架构的模块
│   ├── black-box-tests
│   ├── js-executor # 用Node.js实现的能解析执行js脚本的执行器
│   ├── tb          # 用docker跑起来一个ThingsBoard实例
│   ├── tb-node     # 用docker实现横向扩展ThingBoard节点
│   ├── transport   # 用docker跑三种协议的服务端
│   │   ├── coap
│   │   ├── http
│   │   └── mqtt
│   └── web-ui       # 用Node.js的Express.js实现对外返回ui-ngx模块打包的网页,docker部署
├── netty-mqtt       # netty实现的mqtt客户端,被rule-engine模块引用
├── packaging     目主程序模块,单体架构时包含所有功能模块于一体
├── common      # 公共模块
│   ├── actor   # 自己实现的actor系统
│   ├── dao-api # 数据库查询接口
│   ├── data    # 域模型(数据库表对应的Java类)
│   ├── message # 实现系统的消息机制
│   ├── queue   # 消息队列
│   ├── stats   # 统计
│   ├── transport # 接收设备消息的服务端
│   │   ├── coap  
│   │   ├── http  
│   │   ├── mqtt  
│   │   └── transport-api
│   └── util      # 工具
├── packaging         # 打包相关
│   ├── java         # 后端模块打包
│   │   ├── assembly     # 构建打包成zip,给windows平台的distribution
│   │   ├── filters      # maven资源过滤用到的属性配置
│   │   └── scripts      # 一些安装脚本的模板
│   └── js           # 前端模块打包
│       ├── assembly
│       ├── filters
│       └── scripts
├── rest-client      # Java版的api客户端,可以调用页面上同样的接口(登录、查询设备...)
├── rule-engine      # 规则引擎
│   ├── rule-engine-api
│   └── rule-engine-components
├── tools            # 工具 
├── transport        # 三种协议的服务端做成独立的Java进程,实现代码都是引用common/transport
│   ├── coap
│   ├── http
│   └── mqtt
└── ui-ngx           # Angular.js 实现的前端页面

上面内容有点多,但只需要知道一点:单体架构的ThingsBoard应用就是 application 模块,打包的东西主要有 debrpmzip等交付物。

ThingsBoard单体架构的项目构建逻辑_第1张图片

 ThingsBoard项目采用Maven来构建,官网文档中Installation的部分有 Linux、Windows、Docker 等多种方式的安装说明,安装说明中的安装包用到的分法包的就是Maven构建出来的,比如:

  • Ubutu系统安装的是 thingsboard-3.2.2.deb

  • CentOS系统安装的是 thingsboard-3.2.2.rpm

  • Windows系统安装的是 thingsboard-windows-3.2.2.zip

那这个工程是怎么用Maven配置打包出这些分发包的?这就需要了解工程具体的Maven配置了。

2. deb包的结构

在深入了解maven的构建逻辑前还需要了解一些前置知识,比如deb包的结构以及如何打一个deb包,因为在后面ThingsBoard会使用maven调用gradle来打出一个deb包。

参考:

  • Basics of the Debian package management system

  • Debian New Maintainers' Guide

  • 用dpkg命令制作deb包方法总结

下面以一个简单的例子说明:

 $ tree ./sayhi
./sayhi
├── DEBIAN
│   ├── control
│   ├── postinst
│   ├── postrm
│   ├── preinst
│   └── prerm
└── usr
    └── bin
        └── sayhi.sh

按上面结构建立一个目录,根目录叫sayhi,也就是这个deb软件包的名称了,然后下面必须要有DEBIAN目录,用于存放软件包相关信息。

然后control文件也是必须要有的:

sayhi/DEBIAN/control

Package: hello
Version: 1.0
Section: utils
Architecture: i386
Maintainer: caibh [email protected]
Description: just say hi

postinstpostrmpreinstprerm等文件则是非必须的,这些文件通常是用来定义一些在安装前后、删除前后的处理逻辑的。我在这里面只是简单地打印了一下而已:

preinst

#!/usr/bin/env bash
echo preinst ...............

sayhi.sh则是程序包的程序,这个程序在deb包安装后会被复制到linux系统中同样的/usr/bin目录下:

sayhi.sh

#/usr/bin/env bash
echo "hi !"

打包deb包的方式或工具有很多,这里我用的是dpkg-deb,来打包试试:

$ ls
sayhi

$ dpkg-deb -b sayhi
dpkg-deb: 正在 'sayhi.deb' 中构建软件包 'hello'。

$ ls
sayhi  sayhi.deb

安装试试,可以在输出中看到打包前的 sayhi/DEBIAN/preinstsayhi/DEBIAN/postinst 脚本是被执行了的:

$ sudo dpkg -i sayhi.deb 
正在选中未选择的软件包 hello:i386。
(正在读取数据库 ... 系统当前共安装有 258960 个文件和目录。)
准备解压 sayhi.deb  ...
preinst ...............
正在解压 hello:i386 (1.0) ...
正在设置 hello:i386 (1.0) ...
postinst ...............

运行试试:

$ ls /usr/bin/say*
/usr/bin/sayhi.sh

$ sayhi.sh
hi !

3. 项目的Maven构建思路

如果不熟悉Maven,建议看看许晓斌的《Maven实战》以下章节:

  • 第5章 坐标和依赖

  • 第7章 生命周期和插件

  • 第8章 聚合与继承

  • 第14章 灵活的构建

从Maven工程的角度分析整个ThingsBoard工程的结构。版本是v3.2.2

$ git clone https://github.com/thingsboard/thingsboard.git
$ cd thingsboard
$ git tag -ln
$ git chekcout v3.2.2

3.1 配置模板

项目根pom.xml文件大致是这样的结构:

thingsboard/pom.xml


    
    
    
    
    
    
        ${basedir}
        true
        
    

    
    
        netty-mqtt                
        
    

    
    
        
        
        
            default
            
                true
            
                        
        
        
        
            download-dependencies
            
        
        
        
        
            packaging
            
                true
            
            
                
                
                    
                        
                        
                    
                
            
        
    

    
    
        
        
        
            
                kr.motd.maven
                os-maven-plugin
                1.5.0.Final
            
        
        
        
        
            
                
                
            
        
        
        
        
            
                com.mycila
                license-maven-plugin
            
        
    

    
    
        
            org.projectlombok
            lombok
            provided
        
    

    
    
        
            
        
    

    
    
        
    

    
    
        
    

属性

thingsboard/pom.xml中有很多属性,主要是一些库的版本号,还有一些特殊的值得注意:


    
 
        ${basedir}
        true
        none
        none
        thingsboard
        ${project.name}
        /var/log/${pkg.name}
        /usr/share/${pkg.name}
        1.3.2
        2.3.2
        2.3.2
        2.3.9.RELEASE
        5.2.10.RELEASE
        
    
    
    

Maven的属性有6类:

  • 内置属性。比如${basedir}表示项目根目录,即包含pom.xml文件的目录;${version}表示项目版本

  • POM属性。引用POM文件中对应元素的值,常用的有:

    • ${project.build.sourceDirectory},项目的源码目录,默认src/main/java/

    • ${project.build.testSourceDirectory},项目的测试源码目录,默认src/test/java/

    • ${project.build.directory},项目构建输出目录,默认target/

    • ${project.outputDirectory},项目源码编译输出目录,默认target/classes

    • ${project.testOutputDirectory},项目测试源码编译输出目录,默认target/test-classes

    • ${project.groupId},项目的groupId

    • ${project.artifactId},项目的artifactId

    • ${project.version},项目的version

    • ${project.build.finalName},项目打包输出文件的名称,默认${project.artifactId}-${project.version}

  • 自定义属性。标签下每一个标签都是自定义属性。

  • Settings属性。以settings.开头,用来引用就是settings.xml文件中XML元素的值,如 ${settings.localRepository}

  • Java系统属性。引用Java系统属性,如${user.home}

  • 环境变量属性。以env.开头,用来引用环境变量,如 ${env.JAVA_HOME}

了解了maven属性相关的知识后,回来再看看,可以知道:

  1. ${basedir}表示的是:项目自定义了一个main.dir属性,而这个属性的值就是内置属性basedir,即项目的根目录。

  2. 开头的都是打包相关的属性,见名知义

  3. ${project.name}引用的${project.name}就是:


    ......
    Thingsboard
    ......

小技巧:打印属性的实际值

如果觉得自己到子模块去找这个自定义属性,或者有些属性一下子忘了具体是什么值。可以使用antrun插件来打印属性。

thingsboard/pom.xml中的  - -  下加入antrun 插件配置,并在  - 下引入antrun插件:

thingsboard/pom.xml


    
 
        
        
            
                               
                
                
                    org.apache.maven.plugins
                    maven-antrun-plugin
                    1.7
                    
                        
                            validate
                            
                                run
                            
                            
                                
                                    
                                
                            
                        
                    
                
                
            
        
        
            
            
            
                org.apache.maven.plugins
                maven-antrun-plugin
            
        
    

在根目录下执行mvn validate,然后查看属性的值:

# 把输出结果重定向到一个文件
$ mvn validate > result.txt
# 通过grep命令筛选结果
$ cat result.txt | grep pkg.unixLogFolder
[echoproperties] pkg.unixLogFolder=/var/log/thingsboard
......

继承

ThingsBoard工程利用Maven的继承机制,把大部分的构建逻辑都做成一个“模板”,供子模块去继承,达到复用构建逻辑的目的。在上面的配置文件中带Management后缀的,都是“模板”:

  • 4.3.1 位置的 ,是打包相关的插件配置的模板

  • 5.2 位置的 ,是除了打包以外的其它插件的配置模板

  • 7 位置的

  • 8 位置的

这些元素,都有一个特点,这些元素下面声明的配置,只有在子模块引入的时候,才会子模块产生影响。

举个例子,在根pom.xml的5.2位置下有这么一个插件配置:

thingsboard/pom.xml

    
    
       
        
        
            
                                
                
                
    
                    org.apache.maven.plugins
                    maven-compiler-plugin
                    3.8.1
                    
                        11
                        
                            -Xlint:deprecation
                            -Xlint:removal
                            -Xlint:unchecked
                        
                        
                            
                                org.projectlombok
                                lombok
                                ${lombok.version}
                            
                        
                    
                
                
            
                

    

maven-compiler-plugin 声明了一些项目编译时的配置,大概意思是开启编译的一些警告,然后编译时处理代码中的lombok注解等,而项目根目录下是没有代码的,所以这段配置实际是给子模块继承的。那么看看thingsboard模块的子模块application中是怎么引入的:

thingsboard/application/pom.xml


    4.0.0
    
    
        org.thingsboard
        3.2.2
        thingsboard
    
    application
    jar
    
    
        
                    
            
            
            
            
                org.apache.maven.plugins
                maven-compiler-plugin
            
            
            
        
    
    

可以看到子模块application由于继承了thingsboard模块(项目根pom.xml),引入compiler插件时只需声明,其它都省去了。

而如果application模块没有在/元素下引入compiler插件(注意不是元素下了),父模块thingsboard中的那段 compiler 插件配置是不是对 application 模块产生影响的。

模板

**那为什么说根 pom.xml 中  元素下的是模板呢?**上面的例子也就仅仅是继承了而已。

这需要再看一个例子,在 thingsboard 模块的  下的packaging下,有这么一段配置:

thingsboard/pom.xml

    
  
  
     
  
    
      packaging
      
        true
      
      
        
        
          
          
                                  
              
                org.apache.maven.plugins
                maven-resources-plugin
                
                    
                  
                      copy-service-conf
                      ${pkg.process-resources.phase}
                      
                          copy-resources
                      
                      
                          ${project.build.directory}/conf
                          
                              
                                  src/main/conf
                                  true
                              
                          
                          
                              
                              ${main.dir}/packaging/${pkg.type}/filters/unix.properties
                          
                      
                  
                  
                  
              
                                    
          
        
      
    
  

这段配置是打包相关的resources插件的配置。如果是在IDEA中去看代码,上面的${pkg.type}属性,无论如何都是会报红的,因为在整个thingsboard/pom.xml文件中,是找不到这个属性的定义的。

那么去看看application子模块中的配置,是引入了resources插件的,并且它定义了属性:

thingsboard/application/pom.xml


    4.0.0
    
        org.thingsboard
        3.2.2
        thingsboard
    
    application
    jar

    
        
        java        
        process-resources        
    

    
        
        
            
            
            
                org.apache.maven.plugins
                maven-resources-plugin
            
            
        
    
    

在父模块 thingsboard/pom.xml 中定义了 resources 插件的配置,但这配置中引用的属性 ${pkg.type} 只有在子模块引入该resources插件时中才会填充真正的值(java)。

那么是不是还有其它子模块也引入了resources插件,而且pkg.type有不同的值?答案是肯定的,可以看看js-executor模块的pom.xml:

thingsboard/msa/js-executor/pom.xml


    4.0.0
    
        org.thingsboard
        3.2.2
        msa
    
    org.thingsboard.msa
    js-executor
    pom

            
        
        js
        process-resources        
    
    
    
    
                    
            
            
                org.apache.maven.plugins
                maven-resources-plugin
            
            
        
    
    

所以说,根 pom.xml 中  元素下的是模板,整个项目的构建配置都是按这种“模板”的思路写出来的。

这样设计的好处很明显:

  • 根pom.xml中约定了整个项目用到的所有插件的构建行为

  • 根pom.xml中约定了整个项目用到的所有依赖的版本

  • 简化子模块引入插件、依赖的代码量

  • 子模块可以按需修改插件的构建行为配置

  • 子模块可以按需修改依赖的配置

3.2 根pom.xml中一些细节

applicaiton模块从根pom.xml文件继承了大量插件的配置,由于有些插件在application中没有引入,所以这里作为补充的内容来了解。

项目子模块

然后定义了一些直接的子模块,看看就行,不多说:

    
    
 
        netty-mqtt
        common
        rule-engine
        dao
        transport
        ui-ngx
        tools
        application
        msa
        rest-client
    
    

项目profiles

接着是比较多的内容,标签下定义了不同环境的配置,先粗略地看看有那些,不急着去了解每个下具体的内容:

    
    
   
       
        
            default
            
            
                true
            
        
        
        
        
        
        
            download-dependencies
            
                true
                true
            
        
       
        
        
            packaging
            
            
                true
            
            
                
                    
                        
                    
                
            
       
    

可以看到,根pom.xml中定义了三种profile:

  • default,就是默认的profile,什么设置都没有,不用管

  • download-dependencies,从它上面的注释可以看出,就是用来下载依赖的源码包用的,那条命令(mvn package -Pdownload-dependencies -Dclassifier=sources dependency:copy-dependencies,注意,运行最好加上-DskipTests,不然要运行测试用例又要很久,还不一定成功)的意思是:

    • 执行default生命周期的package阶段(默认绑定maven-jar-plugin的jar目标,参考jar:jar )

    • 执行dependency插件的copy-dependencies目标(默认绑定生命周期阶段process-sources,参考dependency:copy-dependencies)

    • process-sources阶段会比package阶段先执行(参考introduction-to-the-lifecycle)

    • 通过传入的-Pdownload-dependencies选项激活两个属性(-PProfile的缩写)

    • 通过传入的-Dclassifier=sources选项告诉maven下载回来的源码jar包命名都带个source标识符

    • -Dclassifier=sourcesdependency:copy-dependencies的参数(参考:classifier)

  • packaging,明显就是打包时的配置

检测系统信息的扩展插件

看完上面profiles中一大堆的打包配置后,接下来是构建的配置,主要是定义一些插件的行为,而且这些插件的定义是通用的,不是跟打包相关的。

下面首先看看这个扩展maven核心的配置:


    ......
    
        
            
                kr.motd.maven
                os-maven-plugin
                1.5.0.Final
            
        
    

这个配置,就是给maven增加一些内置的属性,通过这些属性,可以在maven运行时动态访问到系统的平台等信息。比如:

  • ${os.detected.classifier}:是${os.detected.name}-${os.detected.arch}的简写

  • os.detected.name:系统类型,常见有:linuxwindows

  • os.detected.arch:系统架构,常见有:x86_32x86_64、、、、

build-helper-maven-plugin



    org.codehaus.mojo
    build-helper-maven-plugin
    1.12
    
        
            add-source
            generate-sources
            
                add-source
            
            
                
                    ${basedir}/target/generated-sources
                
            
        
    

build-helper-maven-plugin 是一个辅助性的插件,这个插件包括很多独立的goal用来帮助maven构建更方便。

比如上面的 add-source 目标,作用就是将指定目录追加为源码目录。

这段配置就是把 target/generated-sources目录追加为源码目录

lifecycle-mapping


    org.eclipse.m2e
    lifecycle-mapping
    1.0.0
    
        
            
                
                    
                        
                            org.apache.maven.plugins
                        
                        
                            maven-antrun-plugin
                        
                        
                            [1.3,)
                        
                        
                            run
                        
                    
                    
                        
                    
                
            
        
    

这段配置,应该是当我们使用eclipse导入ThingsBoard时才有用。参考:Making Maven Plugins Compatible

license-maven-plugin


    com.mycila
    license-maven-plugin
    3.0
    
        
${main.dir}/license-header-template.txt
The Thingsboard Authors **/.env **/*.env **/.eslintrc **/.babelrc **/.jshintrc **/.gradle/** **/nightwatch **/README **/LICENSE **/banner.txt node_modules/** **/*.properties src/test/resources/** src/vendor/** src/font/** src/sh/** packaging/*/scripts/control/** packaging/*/scripts/windows/** packaging/*/scripts/init/** **/*.log **/*.current .instance_id src/main/scripts/control/** src/main/scripts/windows/** src/main/resources/public/static/rulenode/** **/*.proto.js docker/haproxy/** docker/tb-node/** ui/** src/.browserslistrc **/yarn.lock **/*.raw **/apache/cassandra/io/** .run/** JAVADOC_STYLE DOUBLEDASHES_STYLE JAVADOC_STYLE SLASHSTAR_STYLE SLASHSTAR_STYLE SCRIPT_STYLE JAVADOC_STYLE
check

注意这个插件不是 MojoHaus 的,而是 mycila 的。执行的目标 check 会检查源码中的 license header 是否合法。这个插件是唯一在在根 pom.xml 中默认已经引入的插件,也就是说整个项目所有代码都会检查 license header。

4. application模块构建逻辑

在了解完项目整体的构建思路后,就可以到子模块application中,了解构建的交付物的逻辑了。当看到thingsboard/application/pom.xml中引入了某个插件时,就知道应该去thingsboard/pom.xml看看这个插件构建的配置细节了。

说明:

我自己构建 ThingsBoard 的系统是 Deepin V20。

ThingsBoard支持几种数据库存储方式:

  • 默认,用 PostgreSQL

  • Hybrid,用 PostgreSQL + Cassandra

  • Hybrid,用 PostgreSQL + TimescaleDB

同样,队列的中间件也有多种:

  • 默认,JVM内存实现的队列

  • Kafka

  • RabbitMQ

  • ......

这里说的单体架构,数据库和队列都是用默认的。

thingsboard/application/pom.xml 引入以下插件:

  • maven-compiler-plugin

  • maven-surefire-plugin

  • maven-resources-plugin

  • maven-dependency-plugin

  • maven-jar-plugin

  • spring-boot-maven-plugin

  • gradle-maven-plugin

  • maven-assembly-plugin

  • maven-install-plugin

  • protobuf-maven-plugin

这么多插件,看着都头疼,要怎么看?我的方法是按Maven的生命周期执行顺序去看。

4.1 Maven生命周期和插件绑定

关于maven生命周期,有几点重点:

  • mvn命令执行的其实是生命周期的某个阶段,或者插件的某个目标

$ mvn --help
usage: mvn [options] [] []

# 比如 mvn clean install,其实是执行了 clean生命周期的 clean phase 和 default 生命周期的 install phase
  • 生命周期由phase(阶段)组成

  • 插件包含多个goal(目标)

  • 生命周期阶段和插件目标可以在同一条命令中执行(参考:A Build Phase is Made Up of Plugin Goals)

$ mvn clean dependency:copy-dependencies package

Maven定义了三套生命周期,每套生命周期下又包含不同的phase(生命周期阶段):

  • clean

    • pre-clean

    • clean

    • post-clean

  • default

    • process-sources

    • compile

    • process-test-sources

    • test-compile

    • test

    • package

    • install

    • deploy

  • site

    • pre-site

    • site

    • post-site

    • site-deploy

以上由于default生命周期有很多phase,完整的请参考:Lifecycle Reference

而Maven中的插件中则包含很多goal(执行目标),所以目标都会默认绑定到某个生命周期阶段的,具体绑定到哪个,就要看官网上该插件的文档了,例如:

ThingsBoard单体架构的项目构建逻辑_第2张图片

根据以下的判断逻辑,可以确定每个插件执行的goal,以及在哪个生命周期阶段执行goal:

  1. 配置中是否自定义了goal与phase的绑定关系

  2. 如果没有自定义绑定,那么去官网查这个插件这个goal默认绑定的phase

  3. 如果多个goal绑定到同一个phase,按声明顺序执行

  4. 按maven执行周期,下表相关的phase执行顺序是:

    1. generate-sources

    2. process-resources

    3. compile

    4. generate-test-sources

    5. test

    6. package

所以thingsboard/application.pom.xml中每个插件goal的执行顺序如下表:

插件 执行的goal (执行顺序) goal绑定的phase
maven-compiler-plugin compile(5) compile
maven-surefire-plugin test (7) test
maven-resources-plugin copy-resources (4) process-resources
maven-dependency-plugin(a)
(profile为packaging的/中定义的)
copy (8) (复制winsw) package
maven-dependency-plugin(b)
(普通的/中定义的)
copy (1) (复制protoc编译器) generate-sources
maven-jar-plugin jar (9) package
spring-boot-maven-plugin repackage (10) package
gradle-maven-plugin invoke (11) package
maven-assembly-plugin single (12) package
maven-install-plugin install-file (13) package
protobuf-maven-plugin compile (2)、
compile-custom (3)、
test-compile (6)
generate-sources、
generate-sources、
generate-test-sources

表中的maven-dependency-plugin需要注意一下,它分别在 profile 为 packaging 下的  /  和普通的 / 块下定义的要执行的 copy 目标,只是这两个不同的目标在不同生命周期阶段执行,所以执行顺序上不会有冲突。为了区分,我在插件后加了a和b来区分。

我把每个插件的配置我研究了一遍,下面是具体每个插件配置的细节:

4.2 maven-dependency-plugin(b)


    org.apache.maven.plugins
    maven-dependency-plugin
    
        
            copy-protoc
            generate-sources
            
                copy
            
            
                
                    
                        
                        com.google.protobuf
                        protoc
                        ${protobuf.version}
                        ${os.detected.classifier}
                        exe
                        true
                        
                        ${project.build.directory}
                    
                
            
        
    

这段配置的作用是从maven本地仓库复制protocthingsboard/application/target目录,protoc protobuf 用到的编译器,protobuf是一种二进制rpc调用框架。下图是复制的图解:

ThingsBoard单体架构的项目构建逻辑_第3张图片

4.3 protobuf-maven-plugin


    org.xolstice.maven.plugins
    protobuf-maven-plugin
    0.5.0
    
        
        com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
        
        grpc-java
        io.grpc:protoc-gen-grpc-java:1.0.0:exe:${os.detected.classifier}
        
    
    
        
            
                compile
                compile-custom
                test-compile
            
        
    

这段配置是参考了 grpc-java 1.0.0 版本的 README.md 中的说明的,相关内容如下:

For protobuf-based codegen, you can put your proto files in the src/main/proto and src/test/proto directories along with an appropriate plugin.

For protobuf-based codegen integrated with the Maven build system, you can use protobuf-maven-plugin:



  
      kr.motd.maven
      os-maven-plugin
      1.4.1.Final
  


  
      org.xolstice.maven.plugins
      protobuf-maven-plugin
      0.5.0
      
        
        com.google.protobuf:protoc:3.0.0:exe:${os.detected.classifier}
        grpc-java
        io.grpc:protoc-gen-grpc-java:1.0.0:exe:${os.detected.classifier}
      
      
        
          
            compile
            compile-custom
          
        
      
  

这段配置很明显就是用来编译项目中的 protobuf 定义的消息文件。按 protobuf-maven-plugin 插件的约定,这些文件都在项目 src/main/proto 目录下,编译后的java类文件则在target/generated-sources/protobuf/java目录下。

该配置(项目的)中,插件goal与phase的绑定关系是这样的:

  • compile 目标默认绑定 generate-sources 生命周期阶段

  • compile-custom 目标默认绑定 generate-sources 生命周期阶段

  • test-compile 目标默认绑定 generate-test-sources 生命周期阶段

文档说明:

Protobuf compiler artifact specification, in groupId:artifactId:version[:type[:classifier]] format. When this parameter is set, the plugin attempts to resolve the specified artifact as protoc executable.

文档说明:

Plugin artifact specification, in groupId:artifactId:version[:type[:classifier]] format. When this parameter is set, the specified artifact will be resolved as a plugin executable.

是 compile 和 compile-custom 的配置项,文档大概的意思是:是用来声明Protobuf编译器的maven坐标的,如果设置了这个参数,protobuf-maven-plugin 插件就会引用这个maven坐标所指向的文件作为protoc编译器的可执行程序;

的文档我实在没明白是什么意思,但是按照注释内容来看,protobuf-maven-plugin 插件就是用指向的protoc编译程序来编译的 Protobuf 的 .proto 文件的:

意思是:

  1. protoc的版本必须与protobuf-java的版本一致。

  2. 如果项目中没有直接依赖 protobuf-java ,那么必须确保 grpc 依赖的protobuf-java版本与protoc的版本一致。

按这个注释的意思,验证了一下:

  1. thingsboard/application/pom.xml中是声明了protobuf-java的依赖的,版本是3.11.4

  2. thingsboard/application/pom.xml中也引入了protobuf-maven-plugin插件,插件参数所声明的坐标位置能找到3.11.4版本的protoc编译器可执行文件:

# 
caibh@home:~/.m2/repository/com/google/protobuf/protoc/3.11.4$ ls
protoc-3.11.4-linux-x86_64.exe  protoc-3.11.4-linux-x86_64.exe.sha1  protoc-3.11.4.pom  protoc-3.11.4.pom.sha1  _remote.repositories

# chmod +x 添加执行权限后,能执行,确认这就是 protoc 程序
caibh@home:~/.m2/repository/com/google/protobuf/protoc/3.11.4$ ./protoc-3.11.4-linux-x86_64.exe 
Usage: ./protoc-3.11.4-linux-x86_64.exe [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:

对 protobuf-maven-plugin 的配置研究了这么多,最后发现 application 模块虽然声明了这些配置,但是该模块没有 src/main/proto 目录。也就是说application模块虽然加了protobuf编译的,但是项目构建够,是没有protobuf的东西编译出来的。

但是,研究是不会白费的,因为其它模块确实有 protobuf 需要编译的,比如 common/transport/transport-api(如下图)。

ThingsBoard单体架构的项目构建逻辑_第4张图片

4.4 maven-resources-plugin


    org.apache.maven.plugins
    maven-resources-plugin
    
        
            
            copy-conf
            
            ${pkg.process-resources.phase}
            
            
                copy-resources
            
            
            
                
                ${project.build.directory}/conf
                
                                    
                                            
                        src/main/resources
                        
                        
                            logback.xml
                        
                        false
                    
                
            
        
        ......
    

resources插件下配置了很多execution,每一个execution就是一个复制资源的任务。这段配置中,是将maven-resources-plugin插件的copy-resources目标,绑定到process-resources阶段去执行。下是插件执行的配置,下面每一个子元素可以用来配置执行一个任务。有这些任务:

  • copy-conf

  • copy-service-conf

  • copy-linux-conf

  • copy-linux-init

  • copy-win-conf

  • copy-control

  • copy-install

  • copy-windows-control

  • copy-windows-install

  • copy-data

  • copy-docker-config

下面逐个说明。

copy-conf

配置比较简单,直接看注释吧:


    copy-conf
    ${pkg.process-resources.phase}
    
        copy-resources
    
    
        
        ${project.build.directory}/conf
        
            
                
                src/main/resources
                
                    
                    logback.xml
                
                
                false
            
        
    

false的意思就是复制的过程中,不会让maven把资源文件中出现的属性${xxx}替换成具体的值。(参考Maven的资源过滤)

ThingsBoard单体架构的项目构建逻辑_第5张图片

这个任务就是复制src/main/resources目录下的文件,主要为配置文件,还有freemaker模板文件。

特别要注意的是,这里没有复制日志框架的 logback.xml 配置文件,而是在后面要介绍到的其它execution(copy-service-conf)中,从src/main/conf/logback.xml复制过去,而且在复制过程中做了资源过滤。这样做的目的,我理解是把开发阶段的日志配置和部署的日志配置分开来管理。

 copy-service-conf


    copy-service-conf
    ${pkg.process-resources.phase}
    
        copy-resources
    
    
        
        ${project.build.directory}/conf
        
            
                src/main/conf
                true
            
        
        
            
            ${main.dir}/packaging/${pkg.type}/filters/unix.properties
        
    

这段配置可以解释为什么 copy-conf 中没有复制 logback.xml 配置文件,因为 copy-service-conf 做了资源过滤,区分开发环境和部署环境的配置。copy-conf、copy-service-conf 的处理逻辑可以画图理解:

copy-service-conf 把src/main/conf目录下所有资源文件复制到target/conf目录(包括两个文件,看下面截图)下,并且做资源过滤,资源过滤时替换的属性值来自于标签下指定的unix.properties属性文件。

这个unix.properties具体位置是什么?通过之前说过小技巧,使用antrun插件打印属性,可以知道:

[echoproperties] main.dir=/home/caibh/github/thingsboard/application/..
[echoproperties] pkg.type=java

又或者直接看application模块的pom.xml:

application/pom.xml


    ${basedir}/..
    java    

那么完整的路径就是:

${main.dir}/packaging/${pkg.type}/filters/unix.properties
完整路径是:
/home/caibh/github/thingsboard/packaging/java/filters/unix.properties

再次提醒,logback.xml日志配置文件,在copy-conf中是排除了的,而在copy-service-conf中没有排除。换句话说,就是打包的时候,不是复制src/main/resources下的日志配置文件;而是复制src/main/conf下的日志配置文件,并替换该文件中的属性,替换成具体的值,比如说logback.xml中配置的日志文件位置。

看到这里,就能理解:

  • copy-conf 是复制能用于部署时的配置文件资源的,由于 logback.xml 中日志文件路径,在开发阶段和部署阶段不同,所以不能复制它。比如有这么一个场景:开发者电脑是windows系统的,部署的机器是linux系统的,两种路径完全不同。

  • copy-service-conf 利用 maven的资源过滤特性解决的这个问题,而且从名字上理解,部署时候ThingsBoard会安装成为随机器启动的系统服务(service),到时候就用到这里的文件。

copy-linux-conf


    copy-linux-conf
    ${pkg.process-resources.phase}
    
        copy-resources
    
    
        
        ${pkg.linux.dist}/conf
        
            
                config
                true
            
        
        
            ${main.dir}/packaging/${pkg.type}/filters/unix.properties
        
    

通过搜索,发现只有msa/js-executor/pom.xmlmsa/web-ui/pom.xml两个文件中出现属性的定义(看下图),所以能确定 copy-linux-conf 这一项复制资源的配置,是为这两个子模块准备的。这段配置跟 application 模块的打包逻辑关系不大。

ThingsBoard单体架构的项目构建逻辑_第6张图片

 

作者在这个问题的处理上有点 tricky:

  • copy-service-conf,作用于java写的模块(application),复制的源目录是模块目录/src/main/conf,目标目录target/conf

  • copy-linux-init,作用于js写的模块(js-executor),复制的源目录是模块目录/config,目标目录target/package/linux/conf

  • 由于application模块没有config目录(都没东西复制),所以 copy-linux-init 的操作不会影响到application模块的构建逻辑

  • copy-service-conf,源目录和目标目录命名都是conf,偏偏到了copy-linux-init,目标目录还是conf,源目录就命名成config,有点取巧的味道。

copy-linux-init


    copy-linux-init
    ${pkg.process-resources.phase}
    
        copy-resources
    
    
        
        ${pkg.linux.dist}/init
        
            
                
                ${main.dir}/packaging/${pkg.type}/scripts/init
                true
            
        
        
            ${main.dir}/packaging/${pkg.type}/filters/unix.properties
        
    

这段配置跟 copy-linux-conf 情况一样,也是给msa/js-executormsa/web-ui两个子模块准备的。跟 application 模块复制资源的逻辑没关系。

copy-win-conf



    copy-win-conf
    ${pkg.process-resources.phase}
    
        copy-resources
    
    
        
        ${pkg.win.dist}/conf
        
            
                src/main/resources
                
                    logback.xml
                
                false
            
            
                src/main/conf
                
                    
                    ${pkg.name}.conf
                
                true
            
        
        
            
            
            ${main.dir}/packaging/${pkg.type}/filters/windows.properties
        
    

通过搜,有以下模块中出现:

ThingsBoard单体架构的项目构建逻辑_第7张图片

 先不管这段配置在js-executorweb-ui那些前端相关的模块中这段配置会复制什么文件,先来看看application模块中它复制文件的逻辑:

这个 copy-win-conf 复制的资源文件,跟 copy-conf、copy-service-conf 的大同小异(复制的目标目录是target/conf),加上它复制的目标目录是 target/windows/conf,从命名上能确定它复制的就是部署在windows平台时的配置文件

那这段配置在js-executor模块中又会是怎样的呢?看看js-executor的目录结构:

ThingsBoard单体架构的项目构建逻辑_第8张图片

js-executorweb-ui等前端模块只有一个config目录,没有 copy-win-conf 中声明的src/main/resourcessrc/main/conf,所以 copy-win-conf 的配置不会对这两个模块产生影响。

copy-control



    copy-control
    ${pkg.process-resources.phase}
    
        copy-resources
    
    
        
        ${project.build.directory}/control
        
            
                
                
                ${main.dir}/packaging/${pkg.type}/scripts/control
                true
            
        
        
            ${main.dir}/packaging/${pkg.type}/filters/unix.properties
        
    

copy-control 复制的是打包 linux 系统的 deb、rpm包相关的安装前后、删除前后处理的脚本,还有注册安装成系统服务的配置模板:

ThingsBoard单体架构的项目构建逻辑_第9张图片

copy-install



    copy-install
    ${pkg.process-resources.phase}
    
        copy-resources
    
    
        
        ${project.build.directory}/bin/install
        
            
                
                
                ${main.dir}/packaging/${pkg.type}/scripts/install
                
                    
                    **/*.sh
                    **/*.xml
                
                true
            
        
        
            ${main.dir}/packaging/${pkg.type}/filters/unix.properties
        
    

copy-install 复制的是 linux deb、rpm包安装逻辑的相关的脚本,包括:安装ThingsBoard应用、安装数据库结构、日志文件、升级ThingsBoard应用、升级数据库结构等:

ThingsBoard单体架构的项目构建逻辑_第10张图片

copy-windows-control



    copy-windows-control
    ${pkg.process-resources.phase}
    
        copy-resources
    
    
        
        ${pkg.win.dist}
        
            
                
                
                ${main.dir}/packaging/${pkg.type}/scripts/windows
                true
            
        
                    
            ${main.dir}/packaging/${pkg.type}/filters/windows.properties
        
    

 这段配置就是跟 copy-control 对应的,copy-control 是针对linux系统的,而这里是复制windows系统下安装的脚本,注意有一个service.xml的配置文件,这个文件是留给winsw用的(就是下图右边没打绿色钩的那个service.exe,它是在其它任务中复制过去并重命名的),使用 winsw 能按照service.xml中的配置,把ThingsBoard的Java程序包装成Windows系统的服务来运行。

ThingsBoard单体架构的项目构建逻辑_第11张图片

copy-windows-install



    copy-windows-install
    ${pkg.process-resources.phase}
    
        copy-resources
    
    
        
        ${pkg.win.dist}/install
        
            
                
                
                ${main.dir}/packaging/${pkg.type}/scripts/install
                
                    
                    logback.xml
                
                true
            
        
        
            ${main.dir}/packaging/${pkg.type}/filters/windows.properties
        
    

这段配置就是跟 copy-install 对应的,这里的配置仅仅复制了日志配置文件 logback.xml ,由于跟copy-windows-control关系比较紧密,所以把两个复制的动作画在一起,方便理解:

ThingsBoard单体架构的项目构建逻辑_第12张图片

 

copy-data



    copy-data
    ${pkg.process-resources.phase}
    
        copy-resources
    
          
        
        ${project.build.directory}/data
        
            
                src/main/data
            
            
                ../dao/src/main/resources
                
                    **/*.cql
                    **/*.sql
                
                false
            
        
    

这个配置是复制数据库的初始化脚本和一些初始数据配置的:

ThingsBoard单体架构的项目构建逻辑_第13张图片

 

copy-docker-config


    copy-docker-config
    ${pkg.process-resources.phase}
    
        copy-resources
    
    
        ${project.build.directory}
        
            
                docker
                true
            
        
    

 

这段配置,是复制docker目录下的资源文件的,凡是模块下有docker目录的,都执行复制的操作:

  • 复制本模块下docker目录的所有资源文件到本模块的target目录下

这段配置主要是msa模块下的子模块中应用到,跟这里讨论的application模块关系不大

resources插件小结

下图是整个工程中各个模块引入resources插件的情况:

ThingsBoard单体架构的项目构建逻辑_第14张图片

其中js-executorweb-ui是前端相关的模块,用js写的,application模块则是一个java写的后端模块,${pkg.type}的值有两种:java或js,application模块的值明显就是java,所以在thingsboard/packaging目录下才有javajs两个目录用来存放后端和前端的打包相关的资源文件。

另外,经过上面这么多的执行的复制资源的动作后,最后复制到thingsboard/target目录下的有这些内容:

ThingsBoard单体架构的项目构建逻辑_第15张图片

看着上面这些打得密码密码的钩钩,就知道资源复制得“差不多”了,之所以说“差不多”,因为service.exe还没有复制过去呢!

这个service.exe是在dependency插件的copy目标执行时复制过去的。但是回顾一下之前整理的插件执行顺序(留意看goal的序号)会发现,在process-resources阶段执行copy-resources目标后,接下来执行的是:

  • maven-compiler-plugin:compile(5),编译主代码

  • protobuf-maven-plugin:test-compile(6),编译protobuf测试代码

  • maven-surefire-plugin:test(7),测试

  • maven-dependency-plugin:copy(8)

插件 执行的goal (执行顺序) goal绑定的phase
maven-compiler-plugin compile(5) compile
maven-surefire-plugin test (7) test
maven-resources-plugin copy-resources (4) process-resources
maven-dependency-plugin(a)
(profile为packaging的/中定义的)
copy (8) (复制winsw) package
maven-dependency-plugin(b)
(普通的/中定义的)
copy (1) (复制protoc编译器) generate-sources
maven-jar-plugin jar (9) package
spring-boot-maven-plugin repackage (10) package
gradle-maven-plugin invoke (11) package
maven-assembly-plugin single (12) package
maven-install-plugin install-file (13) package
protobuf-maven-plugin compile (2)、
compile-custom (3)、
test-compile (6)
generate-sources、
generate-sources、
generate-test-sources

也就是说,maven-dependency-plugin(b)执行了很多个定义的复制资源的任务后,已经把需要用到的资源文件都复制thingsboards/application/target目录,接下来还要经过编译主代码、编译protobuf测试代码、测试等插件的执行后,才会在maven-dependency-plugin(a)中把service.exe复制过去。

目前先了解到这一点,我们还是继续按顺序看下去(下面很快会讲到,这里先留一个坑,后面填上)。

protobuf-maven-plugin:test-compile(6)的配置在下面就跳过不说了,因为之前protobuf-maven-plugin的部分已经介绍过。

4.5 maven-compiler-plugin



    org.apache.maven.plugins
    maven-compiler-plugin
    3.8.1
    
        11
                    
            -Xlint:deprecation
            -Xlint:removal
            -Xlint:unchecked
        
        
            
                org.projectlombok
                lombok
                ${lombok.version}
            
        
    

compiler 插件 的配置项  配置的是javac编译程序的参数,可以用javac -X了解选项的含义:

$ javac -X
  -Xlint:<密钥>(,<密钥>)*
        要启用或禁用的警告, 使用逗号分隔。
                在关键字前面加上 - 可禁用指定的警告。
                支持的关键字包括:
          deprecation          有关使用了已过时项的警告。
          removal              有关使用了标记为待删除的 API 的警告。
          unchecked            有关未检查操作的警告。    

也就是说,这些选项是用来在命令行打印出java代码的编译警告的。

(参考:annotationProcessorPaths)就是配置编译处理java代码中的lombok注解(参考:Lombok的Maven配置)

4.6 maven-surefire-plugin


    org.apache.maven.plugins
    maven-surefire-plugin
    3.0.0-M1
    
        
            --illegal-access=permit
        
    

这个surefire插件是执行单元测试用的插件,的意思是 Arbitrary JVM options to set on the command line.

配置--illegal-access=permit这个参数是因为 ThingsBoard 现在的版本 3.2.2 需要用 JDK11。

(参考网上文章)JDK9以上模块不能使用反射去访问非公有的成员/成员方法以及构造方法,除非模块标识为opens去允许反射访问。旧JDK制作的库(JDK8及以下)运行在JDK9上会自动被标识为未命名模块,为了处理该警告,JDK9以上提出了一个新的JVM参数:--illegal-access

该参数有四个可选值:

  • permit:默认值,允许通过反射访问,因此会提示像上面一样的警告,这个是首次非法访问警告,后续不警告

  • warn:每次非法访问都会警告

  • debug:在warn的基础上加入了类似e.printStackTrace()的功能

  • deny:禁止所有的非法访问除了使用特别的命令行参数排除的模块,比如使用--add-opens排除某些模块使其能够通过非法反射访问

4.7 maven-dependency-plugin(a)


    org.apache.maven.plugins
    maven-dependency-plugin
    
        
            copy-winsw-service
            
            ${pkg.package.phase}
            
                copy
            
            
                
                    
                        
                        com.sun.winsw
                        winsw
                        bin
                        exe
                        
                        service.exe
                    
                
                
                ${pkg.win.dist}
            
        
    

这段配置的作用是从maven本地仓库复制winswthingsboard/application/target目录,并重命名为service.exe,winsw 是一个可以在Windows系统下将Java程序包装成系统服务去运行的工具。下图是复制的图解:

ThingsBoard单体架构的项目构建逻辑_第16张图片

 看到这里,终于把前面 maven-resources-plugin 留的小坑填上了,至此为止,thingsboard/application/target 目录下的相关小钩钩可以打满了:

ThingsBoard单体架构的项目构建逻辑_第17张图片

 还有一个细节:为什么能从仓库复制这个东西?因为在thingsboard/pom.xml中的依赖配置中配置了:


    ......
 
        2.0.1
    
    ......
    
        ......
        
            com.sun.winsw
            winsw
            ${winsw.version}
            bin
            exe
            provided
        
    

4.8 maven-jar-plugin


    org.apache.maven.plugins
    maven-jar-plugin
    
        
        
            **/logback.xml
        
        
            
            
                ${pkg.implementationTitle}
                ${project.version}
            
        
    

maven-jar-plugin 插件的 jar 目标是默认绑定到 package 生命周期阶段的,所以这里没有显式指定(参考:Build-in Lifecycle Bindings)。

这里就是定义一些打jar包的配置(参考:Setting Package Version Infomation),需要注意的是这里没有把 logback.xml 打进jar包,是因为想将这个日志放在jar包外,方便部署时可以修改配置。

maven-jar-plugin 插件打出来的jar包是仅仅包含编译好的class文件的(这个jar包只有1M),打出来的jar包名在:thingsboard/application/target/thingsboard-3.2.2.jar,使用jar -xf thingsboard-3.2.2.jar解压后,是这样子的(已省略多余的信息):

thingsboard-3.2.2.jar
├── banner.txt
├── i18n
│   └── messages.properties
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── org.thingsboard
│           └── application
│               ├── pom.properties
│               └── pom.xml
├── org
│   └── thingsboard
│       └── server
│           └── utils
│               ├── EventDeduplicationExecutor.class
│               └── MiscUtils.class
├── templates
│   └── test.ftl
├── thingsboard.yml

在 META-INF/MANIFEST.MF中也没有jar包启动入口相关的信息,但可以看到上面配置中的Implementation-TitleImplementation-Version

META-INF/MANIFEST.MF

Manifest-Version: 1.0
Created-By: Apache Maven 3.6.3
Built-By: caibh
Build-Jdk: 11.0.11
Implementation-Title: ThingsBoard
Implementation-Version: 3.2.2

4.9 spring-boot-maven-plugin


    org.springframework.boot
    spring-boot-maven-plugin
    
        ${pkg.disabled}
        ${pkg.mainClass}
        boot
        ZIP
        
        true
        
        true
        
            
            ${pkg.installFolder}/conf
            
            ${pkg.unixLogFolder}
            
            ${pkg.name}.out
            
            ${pkg.name}
        
    
    
        
            
                repackage
            
        
    

这个是 springboot 的 maven 插件(版本 2.3.9.RELEASE),这段配置会在maven的 package 阶段执行 repackage 目标。这个目标的作用是:

Repackage existing JAR and WAR archives so that they can be executed from the command line using java -jar. With layout=NONE can also be used simply to package a JAR with nested dependencies (and no main class, so not executable).

翻译大概意思是:

对 JAR 和 WAR 类型的归档(archives)进行repackage,repackage之后这些归档就可以使用 java -jar 来执行。如果配置了 layout=NONE ,打包出来的jar包也是包含所有依赖的jar的,但是就不能执行,因为这种layout打出来没有配置程序入口(main class)

标签下的元素,都是执行 repackage 目标时的参数。比如:

  • 指定是否跳过执行

  • 指定程序入口

  • 让打出来的jar包带了一个boot标识符:thingsboard-3.2.2-boot.jar

  • 配置jar包内部归档的方式为ZIP

  • 配置为true可以让打出来的jar包本身就像一个二进制可执行文件那样子执行,比如 bash thingsboard-3.2.2-boot.jar

jar包也是可执行文件

executable为true时为什么打出来的jar包就能当做二进制可执行文件呢?

这是因为 spring-boot-maven-plugin 插件对打出来的 jar 包做了手脚,打出来的jar包本质也是一段二进制的数据,这个插件在jar包的数据之前加入了一段启停脚本,你解压了也找不到这段脚本,但是可以使用 vim -b 或 head 来查看到:

# 用head命令查看到第306行(下面仅粗略显示一下脚本的内容)
$ head -n306 thingsboard-3.2.2-boot.jar

#!/bin/bash
#
#    .   ____          _            __ _ _
#   /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
#  ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
#   \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
#    '  |____| .__|_| |_|_| |_\__, | / / / /
#   =========|_|==============|___/=/_/_/_/
#   :: Spring Boot Startup Script ::
#

# Action functions
start() {
......
}

所以,就是配置这个内嵌的脚本中一些属性(参考:Customizing the Startup Script):

配置的标签名 内嵌脚本中的属性 Maven default
initInfoProvides The Provides section of “INIT INFO” ${project.artifactId}
confFolder The default value for CONF_FOLDER Folder containing the jar
logFolder Default value for LOG_FOLDER. Only valid for an init.d service
logFilename Default value for LOG_FILENAME. Only valid for an init.d service

那么就来验证一下内嵌脚本中是否有这些变量:

The Provides section of “INIT INFO”:

ThingsBoard单体架构的项目构建逻辑_第18张图片

CONF_FOLDER

 

LOG_FOLDER

 ThingsBoard单体架构的项目构建逻辑_第19张图片

LOG_FILENAME

 

thingsboard-3.2.2-boot.jar 

由于配置中配置了 classifier 为 boot,所以在 thingsboard/application/target 目录下很容易找到对应的文件:thingsboard-3.2.2-boot.jar,这个文件解压后结构是下面这样的,明显是 spring-boot-maven-plugin 做过手脚的了,最明显就是把依赖的jar包都打到BOOT-INF/lib下,:

thingsboard-3.2.2-boot.jar
├── BOOT-INF
│   ├── classes
│   │   ├── banner.txt
│   │   ├── i18n
│   │   │   └── messages.properties
│   │   ├── org
│   │   │   └── thingsboard
│   │   │       └── server
│   │   │           └── utils
│   │   │               ├── EventDeduplicationExecutor.class
│   │   │               └── MiscUtils.class
│   │   ├── templates
│   │   │   └── test.ftl
│   │   └── thingsboard.yml
│   ├── classpath.idx
│   └── lib
│       └── zstd-jni-1.4.4-7.jar
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── org.thingsboard
│           └── application
│               ├── pom.properties
│               └── pom.xml
├── org
│   └── springframework
│       └── boot
│           └── loader
│               ├── PropertiesLauncher.class

还有在 META-INF/MANIFEST.MF中声明了jar包启动入口相关的信息(Main-ClassStart-Class):

Manifest-Version: 1.0
Created-By: Apache Maven 3.6.3
Built-By: caibh
Build-Jdk: 11.0.11
Implementation-Title: ThingsBoard
Implementation-Version: 3.2.2
Main-Class: org.springframework.boot.loader.PropertiesLauncher
Start-Class: org.thingsboard.server.ThingsboardServerApplication
Spring-Boot-Version: 2.3.9.RELEASE
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx

thingsboard-3.2.2.jar

同样在 thingsboard/application/target 目录下,能找到另一个相似命名的文件:thingsboard-3.2.2.jar

这个文件解压后是这样的,是不带依赖jar包的(这个jar包只有1M,而上面带boot的那个是141M):

thingsboard-3.2.2.jar
├── banner.txt
├── i18n
│   └── messages.properties
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── org.thingsboard
│           └── application
│               ├── pom.properties
│               └── pom.xml
├── org
│   └── thingsboard
│       └── server
│           └── utils
│               ├── EventDeduplicationExecutor.class
│               └── MiscUtils.class
├── templates
│   └── test.ftl
├── thingsboard.yml

在 META-INF/MANIFEST.MF中也没有jar包启动入口相关的信息,注意这里Implementation-TitleImplementation-Version是在maven-jar-plugin中配置的:

META-INF/MANIFEST.MF

Manifest-Version: 1.0
Created-By: Apache Maven 3.6.3
Built-By: caibh
Build-Jdk: 11.0.11
Implementation-Title: ThingsBoard
Implementation-Version: 3.2.2

Manifest-Version: 1.0
Created-By: Apache Maven 3.6.3
Built-By: caibh
Build-Jdk: 11.0.11
Implementation-Title: ThingsBoard
Implementation-Version: 3.2.2

thingsboard/pom.xml


    org.apache.maven.plugins
    maven-jar-plugin
    
        
            **/logback.xml
        
        
            
                ${pkg.implementationTitle}
                ${project.version}
            
        
    

尝试执行,确实是不能运行的:

# thingsboard-3.2.2.jar,不能运行
$ java -jar thingsboard-3.2.2.jar 
thingsboard-3.2.2.jar中没有主清单属性

# thingsboard-3.2.2-boot.jar,能运行
$ java -jar thingsboard-3.2.2-boot.jar 
 ===================================================
 :: ThingsBoard ::       (v3.2.2)
 ===================================================

2021-06-10 10:32:56.534  INFO 14474 --- [           main] o.t.server.ThingsboardServerApplication  : Starting ThingsboardServerApplication v3.2.2..........

springboot maven 插件小结

小结一下,这个插件这段配置的作用就是:

  • 打出一个既可以通过jar -jar执行又可以直接作为二进制文件执行的 jar 包

  • 配置了在linux系统下运行该jar包时配置文件的目录、日志的目录、日志的文件名

  • 注意,这里的内嵌脚本的配置),是不考虑Windows系统下的情况的。因为把jar包打成可执行文件这种特性,仅仅支持Linux等类unix系统(参考:executable 文档)

4.10 gradle-maven-plugin


    org.thingsboard
    gradle-maven-plugin
    
        
        
        ${main.dir}/packaging/${pkg.type}
        
        
            build
            buildDeb
            buildRpm
            renameDeb
            renameRpm
        
        
        
            -PpackagingDir=${main.dir}/packaging
            -PprojectBuildDir=${basedir}/target
            -PprojectVersion=${project.version}
            
                -PmainJar=${project.build.directory}/${project.build.finalName}-boot.${project.packaging}
            
            -PpkgName=${pkg.name}
            -PpkgUser=${pkg.user}
            -PpkgInstallFolder=${pkg.installFolder}
            -PpkgCopyInstallScripts=${pkg.copyInstallScripts}
            -PpkgLogFolder=${pkg.unixLogFolder}
            --warning-mode
            all
        
    
    
        
            
            ${pkg.package.phase}
            
            
                invoke
            
        
    

这段配置是利用 gradle-maven-plugin 调用 gradle 打包 linux 系统的 debrpm等安装包的。从的配置可以看出,packaging/javapackaging/js是两个为打包而建立的gradle工程。

注意看上面的org.thingsboard,表明这个插件是 thingsboard fork 了 LendingClub/gradle-maven-plugin 后自己维护的(参考:thingsboard/gradle-maven-plugin)。

对于 application 模块来说,上面的配置相当于在 application 模块下执行下面的命令:

# 使用gradle6.3版本执行 build buildDeb buildRpm renameDeb renameRpm 等gradle task
/home/caibh/app/gradle/gradle-6.3/bin/gradle build buildDeb buildRpm renameDeb renameRpm \
-PpackagingDir=/home/caibh/github/thingsboard/packaging \
# gradle构建输出的目录,默认是build目录
-PprojectBuildDir=/home/caibh/github/thingsboard/application/target \
-PprojectVersion=3.2.2 \
-PmainJar=/home/caibh/github/thingsboard/application/thingsboard-3.2.2-boot.jar \
-PpkgName=thingsboard \
-PpkgUser=thingsboard \
-PpkgInstallFolder=/usr/share/thingsboard \
-PpkgCopyInstallScripts=true \
-PpkgLogFolder=/var/log/thingsboard \
# 打印gradle的api的deprecated警告信息
--warning-mode all

gradle打包deb、rpm

接下来就由gradle来执行打包。在gradle中也有很多丰富的插件,ThingBoard用了Netflix出品的 gradle-ospackage-plugin,是 Netflix 出品,文档 写得简单易懂,配置基本上能见名知义。

thingsboard/packaging/java/build.gradle 所在的目录就是一个 gradle 工程,也就是说 thingsboard/packaging/java 是一个专门给后端模块打包成deb、rpm包的。build.gradle 相当于 maven中的 pom.xml,它的配置主要包括几个配置块:

ospackage:打包deb和rpm通用的配置,都是些复制文件的配置

buildRpm:rpm打包配置

buildDeb:deb打包配置

task renameDeb:重命名deb包

task renameRpm:重命名rpm包

// 引入ant的一个api,用来做maven里面资源过滤(就是替换字符串)同样的工作
import org.apache.tools.ant.filters.ReplaceTokens

// 声明引入 gradle-ospackage-plugin
buildscript {
    ext {
        osPackageVersion = "8.3.0"
    }
    repositories {
        jcenter()
    }
    dependencies {
        classpath("com.netflix.nebula:gradle-ospackage-plugin:${osPackageVersion}")
    }
}

// 使用 gradle-ospackage-plugin
apply plugin: "nebula.ospackage"

// 命令行中传入的参数转化为gradle内变量
// buildDir、version是gradle内置的属性,distsDirName则是普通变量
// bulildDir = /home/caibh/github/thingsboard/application/target
buildDir = projectBuildDir
version = projectVersion
distsDirName = "./"

ospackage {
    // ......
}
buildRpm {    
    // ......
}
buildDeb {
  // ......
}
task renameDeb(type: Copy) {
    // ......
}
task renameRpm(type: Copy) {
    // ......
}

下面逐块配置看看,由于deb、rpm打包配置都是大同小异,所以下面就只说deb的。

ospackage{...}

// buildDeb和buildRpm相同的配置,都是一些复制文件的操作
ospackage {
    packageName = pkgName
    version = "${project.version}"
    release = 1
    os = LINUX
    type = BINARY
    
    // 下面from中配置的,都复制到这目录下:/usr/share/thingsboard
    into pkgInstallFolder

    // 用户和用户组:thingsboard
    user pkgUser
    permissionGroup pkgUser
    
    // mainJar = /home/caibh/github/thingsboard/application/thingsboard-3.2.2-boot.jar
    from(mainJar) {
        // 重命名
        rename { String fileName ->
            // pkgName = thingsboard
            "${pkgName}.jar"
        }
        // 文件权限
        fileMode 0500
        // 复制到bin子目录,即 
        into "bin"
    }

    // pkgCopyInstallScripts = true
    if("${pkgCopyInstallScripts}".equalsIgnoreCase("true")) {
        from("${buildDir}/bin/install/install.sh") {
            fileMode 0775
            into "bin/install"
        }

        from("${buildDir}/bin/install/upgrade.sh") {
            fileMode 0775
            into "bin/install"
        }

        from("${buildDir}/bin/install/logback.xml") {
            into "bin/install"
        }
    }

    from("${buildDir}/conf") {
        // 排除 thingsboard.conf
        exclude "${pkgName}.conf"
        fileType CONFIG | NOREPLACE
        fileMode 0754
        into "conf"
    }    
    
    from("${buildDir}/data") {
        fileType CONFIG | NOREPLACE
        fileMode 0754
        into "data"
    }
    
    from("${buildDir}/extensions") {
        into "extensions"
    }
}

这段其实就是将之前在target目录集中好的资源文件,复制到打包deb时指定的目录,可以通过dpkg -x thingsboard.deb ./thingsboard 解压打包好的deb包,对照这个的配置理解:

ThingsBoard单体架构的项目构建逻辑_第20张图片

buildDeb{...} 

buildDeb {

    // 对应打出来的deb包为:thingsboard_3.2.2-1_all.deb
    arch = "all"

    archiveFileName = "${pkgName}.deb"

    // sudo dpkg -i thingsboard.deb 安装时,会检查系统是否有安装这些依赖
    requires("openjdk-11-jre").or("java11-runtime").or("oracle-java11-installer").or("openjdk-11-jre-headless")

    // target/conf/thingsboard.conf复制到deb包下/usr/share/thingsboard/conf/thingsboard.conf,做资源过滤
    from("${buildDir}/conf") {
        include "${pkgName}.conf"
        filter(ReplaceTokens, tokens: ['pkg.platform': 'deb'])
        fileType CONFIG | NOREPLACE
        fileMode 0754
        into "${pkgInstallFolder}/conf"
    }

    // 标记为配置文件
    // /usr/share/thingsboard/conf/...
    configurationFile("${pkgInstallFolder}/conf/${pkgName}.conf")
    configurationFile("${pkgInstallFolder}/conf/${pkgName}.yml")
    configurationFile("${pkgInstallFolder}/conf/logback.xml")
    configurationFile("${pkgInstallFolder}/conf/actor-system.conf")
 
    // 向deb包加入安装前后、删除前后的处理脚本
    preInstall file("${buildDir}/control/deb/preinst")
    postInstall file("${buildDir}/control/deb/postinst")
    preUninstall file("${buildDir}/control/deb/prerm")
    postUninstall file("${buildDir}/control/deb/postrm")

    user pkgUser
    permissionGroup pkgUser

    // 复制注册成系统服务的文件
    from("${buildDir}/control/template.service") {
        addParentDirs = false
        fileMode 0644
        into "/lib/systemd/system"
        rename { String filename ->
            "${pkgName}.service"
        }
    }
    // 创建软链接    
    // link(String symLinkPath, String targetPath, int permissions)
    link("${pkgInstallFolder}/bin/${pkgName}.yml", "${pkgInstallFolder}/conf/${pkgName}.yml")    
    link("/etc/${pkgName}/conf", "${pkgInstallFolder}/conf")
}

ThingsBoard单体架构的项目构建逻辑_第21张图片

task renameDeb

 

task renameDeb(type: Copy) {
    from("${buildDir}/") {
        include '*.deb'
        destinationDir file("${buildDir}/")
        rename { String filename ->
            "${pkgName}.deb"
        }
    }
}

这段配置就是将打包出来的deb包复制并重命名,将 thingsboard/application/target/ 目录下 *.deb 文件复制并重命名为 thingsboard.deb

注意这段配置是有风险的,因为目录下可能有多个*.deb文件,但是在 gradle-6.3 版本还能运行,到了更高版本可能就运行报错了。

4.11 maven-assembly-plugin


    org.apache.maven.plugins
    maven-assembly-plugin
    
        ${pkg.name}
        
            ${main.dir}/packaging/${pkg.type}/assembly/windows.xml
        
    
    
        
            assembly
            ${pkg.package.phase}
            
                single
            
        
    

 

gradle-maven-plugin完成了打包deb、rpm包的任务,而打包zip包,就是用maven-assembly-plugin插件来实现的。

上面的这段配置中,就是根据指定的windows.xml中的配置来将各种资源文件汇聚到一起,然后打成一个zip包。从文件命名也能看出,打出来的zip包,就是给Windows平台的分发包。

assembly预定义了四中打包的Descriptor:

  • bin

  • jar-with-dependencies

  • src

  • project

ThingsBoard也是用这些预定义的descriptor的语法,打出windows平台的zip分发包。

下面以application模块为例,看看后端模块打zip包的逻辑是怎样的:

packaging/java/assembly/windows.xml

assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
    windows

    
        zip
    
    
    

    
    
        
        
            ${pkg.win.dist}
            logs
            
                */**
            
        
        
        
            ${pkg.win.dist}/install
            install
            windows
        
        
        
            ${pkg.win.dist}/conf
            conf
            windows
        
        
        
            ${project.build.directory}/extensions
            extensions
        
  
        
            ${project.build.directory}/data
            data
        
    

    
        
        
            ${project.build.directory}/${project.build.finalName}-boot.${project.packaging}
            
            lib
            ${pkg.name}.jar
        
        
        
            ${pkg.win.dist}/service.exe
            
            
            ${pkg.name}.exe
        
        
        
            ${pkg.win.dist}/service.xml
            
            ${pkg.name}.xml
            windows
        
        
        
            ${pkg.win.dist}/install.bat
            
            windows
        
        
        
            ${pkg.win.dist}/uninstall.bat
            
            windows
        
        
        
            ${pkg.win.dist}/upgrade.bat
            
            windows
        
    

ThingsBoard单体架构的项目构建逻辑_第22张图片

 

注意有个小细节,一段tricky的配置:


    
    ${pkg.win.dist}
    logs
    
        */**
    

这是段配置复制 target/windows 目录,但不复制里面任何文件,目标目录重命名为logs,就是为了生成一个logs目录

4.12 maven-install-plugin


    org.apache.maven.plugins
    maven-install-plugin
    
        ${project.build.directory}/${pkg.name}.deb
        ${project.artifactId}
        ${project.groupId}
        ${project.version}
        deb
        deb
    
    
        
            install-deb
            ${pkg.package.phase}
            
                install-file
            
        
    

这段 install 插件的配置(参考:install:install-file),就是把打包出来的 target/application/thingsboard.deb复制一份到仓库,并且复制的时候把命名改一下,看下图:

至此,整个 application 模块的打包过程全部捋了一遍。

 5. 总结

把 ThingsBoard application 模块整个构建逻辑看完一遍之后基本了解了项目的整体规划是怎样的。

这个项目把前后端代码都放在一个工程构建中,这种做法叫monorepo。如果从单个开发者的角度,那么每个开发这肯定喜欢自己管自己的,自己的项目有独立的git提交历史;但如果作为一个项目负责人的角度去看那就不一定了,monorepo由于项目所有相关的东西都放在一块了,所以容易对项目形成一个整体的思维。

当然放在一块也有缺点,其中明显的感觉就是构建变复杂了,需要把各个模块共性的逻辑抽取出来,同时又保留各模块构建逻辑的灵活性,搞不好就会写出一堆臃肿的构建配置。

在了解了ThingsBoard的构建逻辑后,其实可以“抄作业”了,这个项目在打包、配置文件、日志文件、启停控制、注册系统服务、跨平台的交付件等各个方便都考虑得比较全面,值得借鉴。

你可能感兴趣的:(物联网,thingsboard,打包原理,物联网)