ThingsBoard的项目用一个工程实现了单体和微服务两种架构,能做到重用大量的代码同时又具有横向扩展能力。
而本文研究的重点是:ThingsBoard单体架构的应用是怎么构建打包出来的?
首先了解一下工程的结构,通过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 模块,打包的东西主要有 deb
、rpm
、zip
等交付物。
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配置了。
在深入了解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
而postinst
、postrm
、preinst
、prerm
等文件则是非必须的,这些文件通常是用来定义一些在安装前后、删除前后的处理逻辑的。我在这里面只是简单地打印了一下而已:
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/preinst
、sayhi/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 !
如果不熟悉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
项目根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属性相关的知识后,回来再看看,可以知道:
表示的是:项目自定义了一个main.dir
属性,而这个属性的值就是内置属性basedir
,即项目的根目录。
引用的${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插件时中才会填充真正的值(
)。
那么是不是还有其它子模块也引入了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中约定了整个项目用到的所有依赖的版本
简化子模块引入插件、依赖的代码量
子模块可以按需修改插件的构建行为配置
子模块可以按需修改依赖的配置
applicaiton模块从根pom.xml文件继承了大量插件的配置,由于有些插件在application中没有引入,所以这里作为补充的内容来了解。
然后定义了一些直接的子模块,看看就行,不多说:
netty-mqtt
common
rule-engine
dao
transport
ui-ngx
tools
application
msa
rest-client
接着是比较多的内容,
标签下定义了不同环境的配置,先粗略地看看有那些
,不急着去了解每个
下具体的内容:
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
选项激活
和
两个属性(-P
是Profile
的缩写)
通过传入的-Dclassifier=sources
选项告诉maven下载回来的源码jar包命名都带个source
标识符
-Dclassifier=sources
是dependency: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
:系统类型,常见有:linux
、windows
os.detected.arch
:系统架构,常见有:x86_32
、x86_64
、、、、
org.codehaus.mojo
build-helper-maven-plugin
1.12
add-source
generate-sources
add-source
build-helper-maven-plugin 是一个辅助性的插件,这个插件包括很多独立的goal用来帮助maven构建更方便。
比如上面的 add-source
目标,作用就是将指定目录追加为源码目录。
这段配置就是把 target/generated-sources
目录追加为源码目录
org.eclipse.m2e
lifecycle-mapping
1.0.0
org.apache.maven.plugins
maven-antrun-plugin
[1.3,)
run
这段配置,应该是当我们使用eclipse导入ThingsBoard时才有用。参考:Making Maven Plugins Compatible
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。
在了解完项目整体的构建思路后,就可以到子模块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的生命周期执行顺序去看。
关于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(执行目标),所以目标都会默认绑定到某个生命周期阶段的,具体绑定到哪个,就要看官网上该插件的文档了,例如:
根据以下的判断逻辑,可以确定每个插件执行的goal,以及在哪个生命周期阶段执行goal:
配置中是否自定义了goal与phase的绑定关系
如果没有自定义绑定,那么去官网查这个插件这个goal默认绑定的phase
如果多个goal绑定到同一个phase,按声明顺序执行
按maven执行周期,下表相关的phase执行顺序是:
generate-sources
process-resources
compile
generate-test-sources
test
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来区分。
我把每个插件的配置我研究了一遍,下面是具体每个插件配置的细节:
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本地仓库复制protoc
到thingsboard/application/target
目录,protoc protobuf 用到的编译器,protobuf是一种二进制rpc调用框架。下图是复制的图解:
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
文件的:
意思是:
protoc的版本必须与protobuf-java的版本一致。
如果项目中没有直接依赖 protobuf-java ,那么必须确保 grpc 依赖的protobuf-java版本与protoc的版本一致。
按这个注释的意思,验证了一下:
thingsboard/application/pom.xml
中是声明了protobuf-java
的依赖的,版本是3.11.4
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
(如下图)。
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
${pkg.process-resources.phase}
copy-resources
${project.build.directory}/conf
src/main/resources
logback.xml
false
的意思就是复制的过程中,不会让maven把资源文件中出现的属性${xxx}
替换成具体的值。(参考Maven的资源过滤)
这个任务就是复制src/main/resources
目录下的文件,主要为配置文件,还有freemaker
模板文件。
特别要注意的是,这里没有复制日志框架的 logback.xml 配置文件,而是在后面要介绍到的其它execution(copy-service-conf)中,从src/main/conf/logback.xml
复制过去,而且在复制过程中做了资源过滤。这样做的目的,我理解是把开发阶段的日志配置和部署的日志配置分开来管理。
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
${pkg.process-resources.phase}
copy-resources
${pkg.linux.dist}/conf
config
true
${main.dir}/packaging/${pkg.type}/filters/unix.properties
通过搜索,发现只有msa/js-executor/pom.xml
和msa/web-ui/pom.xml
两个文件中出现
属性的定义(看下图),所以能确定 copy-linux-conf 这一项复制资源的配置,是为这两个子模块准备的。这段配置跟 application 模块的打包逻辑关系不大。
作者在这个问题的处理上有点 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
${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-executor
和msa/web-ui
两个子模块准备的。跟 application 模块复制资源的逻辑没关系。
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
通过搜
,有以下模块中出现:
先不管这段配置在js-executor
、web-ui
那些前端相关的模块中这段配置会复制什么文件,先来看看application
模块中它复制文件的逻辑:
这个 copy-win-conf 复制的资源文件,跟 copy-conf、copy-service-conf 的大同小异(复制的目标目录是target/conf
),加上它复制的目标目录是 target/windows/conf
,从命名上能确定它复制的就是部署在windows平台时的配置文件。
那这段配置在js-executor
模块中又会是怎样的呢?看看js-executor
的目录结构:
js-executor
、web-ui
等前端模块只有一个config
目录,没有 copy-win-conf 中声明的src/main/resources
、src/main/conf
,所以 copy-win-conf 的配置不会对这两个模块产生影响。
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包相关的安装前后、删除前后处理的脚本,还有注册安装成系统服务的配置模板:
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应用、升级数据库结构等:
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系统的服务来运行。
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关系比较紧密,所以把两个复制的动作画在一起,方便理解:
copy-data
${pkg.process-resources.phase}
copy-resources
${project.build.directory}/data
src/main/data
../dao/src/main/resources
**/*.cql
**/*.sql
false
这个配置是复制数据库的初始化脚本和一些初始数据配置的:
copy-docker-config
${pkg.process-resources.phase}
copy-resources
${project.build.directory}
docker
true
这段配置,是复制docker目录下的资源文件的,凡是模块下有docker
目录的,都执行复制的操作:
复制本模块下docker
目录的所有资源文件到本模块的target
目录下
这段配置主要是msa
模块下的子模块中应用到,跟这里讨论的application
模块关系不大。
下图是整个工程中各个模块引入resources插件的情况:
其中js-executor
、web-ui
是前端相关的模块,用js写的,application
模块则是一个java写的后端模块,${pkg.type}的值有两种:java或js,application
模块的值明显就是java,所以在thingsboard/packaging
目录下才有java
和js
两个目录用来存放后端和前端的打包相关的资源文件。
另外,经过上面这么多的
执行的复制资源的动作后,最后复制到thingsboard/target
目录下的有这些内容:
看着上面这些打得密码密码的钩钩,就知道资源复制得“差不多”了,之所以说“差不多”,因为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
的部分已经介绍过。
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配置)
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排除某些模块使其能够通过非法反射访问
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本地仓库复制winsw
到thingsboard/application/target
目录,并重命名为service.exe
,winsw 是一个可以在Windows系统下将Java程序包装成系统服务去运行的工具。下图是复制的图解:
看到这里,终于把前面 maven-resources-plugin 留的小坑填上了,至此为止,thingsboard/application/target
目录下的相关小钩钩可以打满了:
还有一个细节:为什么能从仓库复制这个东西?因为在thingsboard/pom.xml
中的依赖配置中配置了:
......
2.0.1
......
......
com.sun.winsw
winsw
${winsw.version}
bin
exe
provided
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-Title
、Implementation-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
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
. Withlayout=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
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”:
CONF_FOLDER
:
LOG_FOLDER
:
LOG_FILENAME
:
由于配置中配置了 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-Class
、Start-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/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-Title
、Implementation-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..........
小结一下,这个插件这段配置的作用就是:
打出一个既可以通过jar -jar
执行又可以直接作为二进制文件执行的 jar 包
配置了在linux系统下运行该jar包时配置文件的目录、日志的目录、日志的文件名
注意,这里的内嵌脚本的配置(
),是不考虑Windows系统下的情况的。因为把jar包打成可执行文件这种特性,仅仅支持Linux等类unix系统(参考:executable 文档)
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 系统的 deb
、rpm
等安装包的。从
的配置可以看出,packaging/java
和packaging/js
是两个为打包而建立的gradle工程。
注意看上面的
,表明这个插件是 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来执行打包。在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的。
// 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包,对照这个的配置理解:
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")
}
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 版本还能运行,到了更高版本可能就运行报错了。
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
lib
${pkg.name}.jar
${pkg.name}.exe
${pkg.name}.xml
windows
windows
windows
windows
注意有个小细节,一段tricky的配置:
${pkg.win.dist}
logs
*/**
这是段配置复制 target/windows
目录,但不复制里面任何文件,目标目录重命名为logs,就是为了生成一个logs目录
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 模块的打包过程全部捋了一遍。
把 ThingsBoard application 模块整个构建逻辑看完一遍之后基本了解了项目的整体规划是怎样的。
这个项目把前后端代码都放在一个工程构建中,这种做法叫monorepo
。如果从单个开发者的角度,那么每个开发这肯定喜欢自己管自己的,自己的项目有独立的git提交历史;但如果作为一个项目负责人的角度去看那就不一定了,monorepo由于项目所有相关的东西都放在一块了,所以容易对项目形成一个整体的思维。
当然放在一块也有缺点,其中明显的感觉就是构建变复杂了,需要把各个模块共性的逻辑抽取出来,同时又保留各模块构建逻辑的灵活性,搞不好就会写出一堆臃肿的构建配置。
在了解了ThingsBoard的构建逻辑后,其实可以“抄作业”了,这个项目在打包、配置文件、日志文件、启停控制、注册系统服务、跨平台的交付件等各个方便都考虑得比较全面,值得借鉴。