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 模块,打包的东西主要有 deb、rpm、zip等交付物。

ThingsBoard项目采用Maven来构建,官网文档中Installation的部分有 Linux、Windows、Docker 等多种方式的安装说明,安装说明中的安装包用到的分法包的就是Maven构建出来的,比如:
Ubutu系统安装的是
thingsboard-3.2.2.debCentOS系统安装的是
thingsboard-3.2.2.rpmWindows系统安装的是
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 caibaohong@outlook.com
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 !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.23.1 配置模板
项目根pom.xml文件大致是这样的结构:
thingsboard/pom.xml
<project>
<!-- 1.项目的基本新信息,GAV、版本、项目名等 -->
<!-- ...... -->
<!-- 2.自定义属性 -->
<properties>
<main.dir>${basedir}</main.dir>
<pkg.disabled>true</pkg.disabled>
<!-- ...... -->
</properties>
<!-- 3.包含的子模块 -->
<modules>
<module>netty-mqtt</module>
<!-- ...... -->
</modules>
<!-- 4.不同环境的配置,有启用配置才生效 -->
<profiles>
<!--- 4.1 default,默认启用,没任何配置 -->
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!--- 4.2 下载依赖时用的配置,默认不启用 -->
<profile>
<id>download-dependencies</id>
<!-- ...... -->
</profile>
<!--- 4.3 打包用的插件配置,默认启用 -->
<profile>
<id>packaging</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<!-- 4.3.1 当 packaging profile 生效时的插件配置“模板” -->
<pluginManagement>
<plugins>
<!--
这里有大量复杂的打包配置,
就是这里的配置实现打包出deb、rpm、zip
-->
<!-- ...... -->
</plugins>
</pluginManagement>
</build>
</profile>
</profiles>
<!-- 5.构建的配置 -->
<build>
<!-- 5.1
扩展,提供一些检测系统环境相关的属性,
如 ${os.detected.classifier}
-->
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
<!-- 5.2 插件配置“模板” -->
<pluginManagement>
<plugins>
<!--
主要声明一些插件的版本,
有部分插件有额外的配置,但都不是打包强相关,
因为打包的配置都集中在上面id为packaging的profile中了
-->
<!-- ...... -->
</plugins>
</pluginManagement>
<!-- 5.3 真正全局引入的插件,只有这个检查license的 -->
<plugins>
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<!-- 6.真正全局引入的依赖,只有lombok -->
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- 7.依赖的“模板”,主要声明依赖库的版本,子模块引入时就不用声明version -->
<dependencyManagement>
<dependencies>
<!-- ...... -->
</dependencies>
</dependencyManagement>
<!-- 8.发布上传的配置,这里不是重点 -->
<distributionManagement>
<!-- ...... -->
</distributionManagement>
<!-- 9.仓库配置,不是重点 -->
<repositories>
<!-- ...... -->
</repositories>
</project>属性
thingsboard/pom.xml中有很多属性,主要是一些库的版本号,还有一些特殊的值得注意:
<project>
<!-- ...... -->
<properties>
<main.dir>${basedir}</main.dir>
<pkg.disabled>true</pkg.disabled>
<pkg.process-resources.phase>none</pkg.process-resources.phase>
<pkg.package.phase>none</pkg.package.phase>
<pkg.user>thingsboard</pkg.user>
<pkg.implementationTitle>${project.name}</pkg.implementationTitle>
<pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<javax-annotation.version>1.3.2</javax-annotation.version>
<jakarta.xml.bind-api.version>2.3.2</jakarta.xml.bind-api.version>
<jaxb-runtime.version>2.3.2</jaxb-runtime.version>
<spring-boot.version>2.3.9.RELEASE</spring-boot.version>
<spring.version>5.2.10.RELEASE</spring.version>
<!-- ...... -->
</properties>
<!-- ...... -->
</project> 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}
自定义属性。
<properties>标签下每一个标签都是自定义属性。Settings属性。以
settings.开头,用来引用就是settings.xml文件中XML元素的值,如${settings.localRepository}Java系统属性。引用Java系统属性,如
${user.home}环境变量属性。以
env.开头,用来引用环境变量,如${env.JAVA_HOME}
了解了maven属性相关的知识后,回来再看看,可以知道:
<main.dir>${basedir}</main.dir>表示的是:项目自定义了一个main.dir属性,而这个属性的值就是内置属性basedir,即项目的根目录。<pkg.开头的都是打包相关的属性,见名知义<pkg.implementationTitle>${project.name}</pkg.implementationTitle>引用的${project.name}就是:
<project>
......
<name>Thingsboard</name>
......
</project>小技巧:打印属性的实际值
如果觉得自己到子模块去找这个自定义属性,或者有些属性一下子忘了具体是什么值。可以使用antrun插件来打印属性。
在thingsboard/pom.xml中的 <build> - <pluginManagemenet> - <plugins> 下加入antrun 插件配置,并在 <build> - <plugins>下引入antrun插件:
thingsboard/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- ...... -->
<build>
<!-- ...... -->
<pluginManagement>
<plugins>
<!-- ...... -->
<!-- 这里只是对插件配置,不会实质性引入插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<echoproperties />
</tasks>
</configuration>
</execution>
</executions>
</plugin>
<!-- ...... -->
</plugins>
</pluginManagement>
<plugins>
<!-- ...... -->
<!-- 在根pom.xml配置了,所有子模块都会引入 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>在根目录下执行mvn validate,然后查看<pkg.unixLogFolder>属性的值:
# 把输出结果重定向到一个文件
$ mvn validate > result.txt
# 通过grep命令筛选结果
$ cat result.txt | grep pkg.unixLogFolder
[echoproperties] pkg.unixLogFolder=/var/log/thingsboard
......继承
ThingsBoard工程利用Maven的继承机制,把大部分的构建逻辑都做成一个“模板”,供子模块去继承,达到复用构建逻辑的目的。在上面的配置文件中带Management后缀的,都是“模板”:
4.3.1位置的<pluginManagement>,是打包相关的插件配置的模板5.2位置的<pluginManagement>,是除了打包以外的其它插件的配置模板7位置的<dependencyManagement>,8位置的<distributionManagement>
这些<xxxManagement>元素,都有一个特点,这些元素下面声明的配置,只有在子模块引入的时候,才会子模块产生影响。
举个例子,在根pom.xml的5.2位置下有这么一个插件配置:
thingsboard/pom.xml
<project>
<build>
<!-- 5.2 插件配置“模板” -->
<pluginManagement>
<plugins>
<!--
主要声明一些插件的版本,
有部分插件有额外的配置,但都不是打包强相关,
因为打包的配置都集中在上面id为packaging的profile中了
-->
<!-- ...... -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
<compilerArgs>
<arg>-Xlint:deprecation</arg>
<arg>-Xlint:removal</arg>
<arg>-Xlint:unchecked</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>maven-compiler-plugin 声明了一些项目编译时的配置,大概意思是开启编译的一些警告,然后编译时处理代码中的lombok注解等,而项目根目录下是没有代码的,所以这段配置实际是给子模块继承的。那么看看thingsboard模块的子模块application中是怎么引入的:
thingsboard/application/pom.xml
<project>
<modelVersion>4.0.0</modelVersion>
<!-- application模块继承了thingsboard模块 -->
<parent>
<groupId>org.thingsboard</groupId>
<version>3.2.2</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>
<packaging>jar</packaging>
<!-- ...... -->
<build>
<!-- ...... -->
<plugins>
<!-- ...... -->
<!-- 引入compiler插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<!-- ...... -->
</plugins>
</build>
</project>可以看到子模块application由于继承了thingsboard模块(项目根pom.xml),引入compiler插件时只需声明<groupId>、<artifactId>,其它都省去了。
而如果application模块没有在<build>/<plugins>元素下引入compiler插件(注意不是<pluginManagement>元素下了),父模块thingsboard中的那段 compiler 插件配置是不是对 application 模块产生影响的。
模板
**那为什么说根 pom.xml 中 <xxxManagement> 元素下的是模板呢?**上面的例子也就仅仅是继承了而已。
这需要再看一个例子,在 thingsboard 模块的 <profiles> 下的<id>为packaging的<profile>下,有这么一段配置:
thingsboard/pom.xml
<project>
<!-- 4.不同环境的配置,有启用配置才生效 -->
<profiles>
<!--- ...... -->
<!--- 4.3 打包用的插件配置,默认启用 -->
<profile>
<id>packaging</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<!-- 4.3.1 当 packaging profile 生效时的插件配置“模板” -->
<pluginManagement>
<plugins>
<!--
这里有大量复杂的打包配置,
就是这里的配置实现打包出deb、rpm、zip
-->
<!-- ...... -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-service-conf</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/conf</outputDirectory>
<resources>
<resource>
<directory>src/main/conf</directory>
<filtering>true</filtering>
</resource>
</resources>
<filters>
<!-- !!!!!!! 在根pom.xml中找不到pkg.type属性 !!!!!! -->
<filter>${main.dir}/packaging/${pkg.type}/filters/unix.properties</filter>
</filters>
</configuration>
</execution>
<!-- ...... -->
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
</profiles>
</project>这段配置是打包相关的resources插件的配置。如果是在IDEA中去看代码,上面的${pkg.type}属性,无论如何都是会报红的,因为在整个thingsboard/pom.xml文件中,是找不到这个属性的定义的。
那么去看看application子模块中的配置,是引入了resources插件的,并且它定义了<pkg.type>属性:
thingsboard/application/pom.xml
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.2.2</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>
<packaging>jar</packaging>
<properties>
<!-- 定义了pkg.type属性在本模块中实际的值 -->
<pkg.type>java</pkg.type>
<pkg.process-resources.phase>process-resources</pkg.process-resources.phase>
</properties>
<build>
<!-- ...... -->
<plugins>
<!-- ...... -->
<!-- 引入了resources插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
</plugin>
<!-- ...... -->
</plugins>
</build>
<!-- ...... -->
</project>在父模块 thingsboard/pom.xml 中定义了 resources 插件的配置,但这配置中引用的属性 ${pkg.type} 只有在子模块引入该resources插件时中才会填充真正的值(<pkg.type>java</pkg.type>)。
那么是不是还有其它子模块也引入了resources插件,而且pkg.type有不同的值?答案是肯定的,可以看看js-executor模块的pom.xml:
thingsboard/msa/js-executor/pom.xml
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.2.2</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>
<artifactId>js-executor</artifactId>
<packaging>pom</packaging>
<properties>
<!-- pkg.type 的值变成 js 了 -->
<pkg.type>js</pkg.type>
<pkg.process-resources.phase>process-resources</pkg.process-resources.phase>
</properties>
<!-- ...... -->
<build>
<plugins>
<!-- ...... -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
</plugin>
<!-- ...... -->
</plugins>
</build>
<!-- ...... -->
</project>所以说,根 pom.xml 中 <xxxManagement> 元素下的是模板,整个项目的构建配置都是按这种“模板”的思路写出来的。
这样设计的好处很明显:
根pom.xml中约定了整个项目用到的所有插件的构建行为
根pom.xml中约定了整个项目用到的所有依赖的版本
简化子模块引入插件、依赖的代码量
子模块可以按需修改插件的构建行为配置
子模块可以按需修改依赖的配置
3.2 根pom.xml中一些细节
applicaiton模块从根pom.xml文件继承了大量插件的配置,由于有些插件在application中没有引入,所以这里作为补充的内容来了解。
项目子模块
然后定义了一些直接的子模块,看看就行,不多说:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- ...... -->
<modules>
<module>netty-mqtt</module>
<module>common</module>
<module>rule-engine</module>
<module>dao</module>
<module>transport</module>
<module>ui-ngx</module>
<module>tools</module>
<module>application</module>
<module>msa</module>
<module>rest-client</module>
</modules>
<!-- ...... -->
</project>项目profiles
接着是比较多的内容,<profiles>标签下定义了不同环境的配置,先粗略地看看有那些<profile>,不急着去了解每个<profile>下具体的内容:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- ...... -->
<profiles>
<!-- ######## default ######## -->
<profile>
<id>default</id>
<!-- 默认激活名为 default 的 profile -->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!-- ######## download-dependencies ######## -->
<!-- download sources under target/dependencies -->
<!-- mvn package -Pdownload-dependencies -Dclassifier=sources dependency:copy-dependencies -->
<profile>
<id>download-dependencies</id>
<properties>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</properties>
</profile>
<!-- ######## packaging ######## -->
<profile>
<id>packaging</id>
<!-- 默认激活名为 packaging 的 profile -->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<pluginManagement>
<plugins>
<!-- ...... -->
</plugins>
</pluginManagement>
</build>
</profile>
<!-- ...... -->
</project>可以看到,根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选项激活<downloadSource>和<downloadJavadocs>两个属性(-P是Profile的缩写)通过传入的
-Dclassifier=sources选项告诉maven下载回来的源码jar包命名都带个source标识符-Dclassifier=sources是dependency:copy-dependencies的参数(参考:classifier)
packaging,明显就是打包时的配置
检测系统信息的扩展插件
看完上面profiles中一大堆的打包配置后,接下来是构建的配置,主要是定义一些插件的行为,而且这些插件的定义是通用的,不是跟打包相关的。
下面首先看看这个扩展maven核心的配置:
<project>
......
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
</build>
</project>这个配置,就是给maven增加一些内置的属性,通过这些属性,可以在maven运行时动态访问到系统的平台等信息。比如:
${os.detected.classifier}:是${os.detected.name}-${os.detected.arch}的简写os.detected.name:系统类型,常见有:linux、windowsos.detected.arch:系统架构,常见有:x86_32、x86_64、、、、
build-helper-maven-plugin
<!-- project > build > pluginManagement > plugins > plugin -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.12</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${basedir}/target/generated-sources</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>build-helper-maven-plugin 是一个辅助性的插件,这个插件包括很多独立的goal用来帮助maven构建更方便。
比如上面的 add-source 目标,作用就是将指定目录追加为源码目录。
这段配置就是把 target/generated-sources目录追加为源码目录
lifecycle-mapping
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>
org.apache.maven.plugins
</groupId>
<artifactId>
maven-antrun-plugin
</artifactId>
<versionRange>
[1.3,)
</versionRange>
<goals>
<goal>run</goal>
</goals>
</pluginExecutionFilter>
<action>
<ignore></ignore>
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>这段配置,应该是当我们使用eclipse导入ThingsBoard时才有用。参考:Making Maven Plugins Compatible
license-maven-plugin
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>3.0</version>
<configuration>
<header>${main.dir}/license-header-template.txt</header>
<properties>
<owner>The Thingsboard Authors</owner>
</properties>
<excludes>
<exclude>**/.env</exclude>
<exclude>**/*.env</exclude>
<exclude>**/.eslintrc</exclude>
<exclude>**/.babelrc</exclude>
<exclude>**/.jshintrc</exclude>
<exclude>**/.gradle/**</exclude>
<exclude>**/nightwatch</exclude>
<exclude>**/README</exclude>
<exclude>**/LICENSE</exclude>
<exclude>**/banner.txt</exclude>
<exclude>node_modules/**</exclude>
<exclude>**/*.properties</exclude>
<exclude>src/test/resources/**</exclude>
<exclude>src/vendor/**</exclude>
<exclude>src/font/**</exclude>
<exclude>src/sh/**</exclude>
<exclude>packaging/*/scripts/control/**</exclude>
<exclude>packaging/*/scripts/windows/**</exclude>
<exclude>packaging/*/scripts/init/**</exclude>
<exclude>**/*.log</exclude>
<exclude>**/*.current</exclude>
<exclude>.instance_id</exclude>
<exclude>src/main/scripts/control/**</exclude>
<exclude>src/main/scripts/windows/**</exclude>
<exclude>src/main/resources/public/static/rulenode/**</exclude>
<exclude>**/*.proto.js</exclude>
<exclude>docker/haproxy/**</exclude>
<exclude>docker/tb-node/**</exclude>
<exclude>ui/**</exclude>
<exclude>src/.browserslistrc</exclude>
<exclude>**/yarn.lock</exclude>
<exclude>**/*.raw</exclude>
<exclude>**/apache/cassandra/io/**</exclude>
<exclude>.run/**</exclude>
</excludes>
<mapping>
<proto>JAVADOC_STYLE</proto>
<cql>DOUBLEDASHES_STYLE</cql>
<scss>JAVADOC_STYLE</scss>
<jsx>SLASHSTAR_STYLE</jsx>
<tsx>SLASHSTAR_STYLE</tsx>
<conf>SCRIPT_STYLE</conf>
<gradle>JAVADOC_STYLE</gradle>
</mapping>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>注意这个插件不是 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] [<goal(s)>] [<phase(s)>]
# 比如 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 packageMaven定义了三套生命周期,每套生命周期下又包含不同的phase(生命周期阶段):
clean
pre-clean
cleanpost-clean
default
process-sourcescompileprocess-test-sourcestest-compiletestpackageinstalldeploy
site
pre-site
sitepost-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的 <build>/<pluginManagement>中定义的) | copy (8) (复制winsw) | package |
| maven-dependency-plugin(b) (普通的 <build>/<pluginManagement>中定义的) | 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 下的 <build> / <buildPluginManagement> 和普通的<build> / <buildPluginManagement>块下定义的要执行的 copy 目标,只是这两个不同的目标在不同生命周期阶段执行,所以执行顺序上不会有冲突。为了区分,我在插件后加了a和b来区分。
我把每个插件的配置我研究了一遍,下面是具体每个插件配置的细节:
4.2 maven-dependency-plugin(b)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-protoc</id>
<phase>generate-sources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<!-- protoc 编译器的坐标 -->
<groupId>com.google.protobuf</groupId>
<artifactId>protoc</artifactId>
<version>${protobuf.version}</version>
<classifier>${os.detected.classifier}</classifier>
<type>exe</type>
<overWrite>true</overWrite>
<!-- thingsboard/application/target -->
<outputDirectory>${project.build.directory}</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>这段配置的作用是从maven本地仓库复制protoc到thingsboard/application/target目录,protoc protobuf 用到的编译器,protobuf是一种二进制rpc调用框架。下图是复制的图解:

4.3 protobuf-maven-plugin
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<!--
The version of protoc must match protobuf-java. If you don't depend on
protobuf-java directly, you will be transitively depending on the
protobuf-java version that grpc depends on.
-->
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.0.0:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>这段配置是参考了 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:
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.4.1.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<!--
The version of protoc must match protobuf-java. If you don't depend on
protobuf-java directly, you will be transitively depending on the
protobuf-java version that grpc depends on.
-->
<protocArtifact>com.google.protobuf:protoc:3.0.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.0.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>这段配置很明显就是用来编译项目中的 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生命周期阶段
<protocArtifact>文档说明:
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.
<pluginArtifact>文档说明:
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.
<protocArtifact>是 compile 和 compile-custom 的配置项,文档大概的意思是:<protocArtifact>是用来声明Protobuf编译器的maven坐标的,如果设置了这个参数,protobuf-maven-plugin 插件就会引用这个maven坐标所指向的文件作为protoc编译器的可执行程序;
<pluginArtifact>的文档我实在没明白是什么意思,但是按照注释内容来看,protobuf-maven-plugin 插件就是用<protocArtifact>指向的protoc编译程序来编译的 Protobuf 的 .proto 文件的:
<!--
The version of protoc must match protobuf-java. If you don't depend on
protobuf-java directly, you will be transitively depending on the
protobuf-java version that grpc depends on.
-->意思是:
protoc的版本必须与protobuf-java的版本一致。
如果项目中没有直接依赖 protobuf-java ,那么必须确保 grpc 依赖的protobuf-java版本与protoc的版本一致。
按这个注释的意思,验证了一下:
thingsboard/application/pom.xml中是声明了protobuf-java的依赖的,版本是3.11.4thingsboard/application/pom.xml中也引入了protobuf-maven-plugin插件,插件参数<protocArtifact>所声明的坐标位置能找到3.11.4版本的protoc编译器可执行文件:
# <protocArtifact>
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(如下图)。

4.4 maven-resources-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<!-- 可理解为执行的任务的id -->
<id>copy-conf</id>
<!-- 就是 process-resources -->
<phase>${pkg.process-resources.phase}</phase>
<!-- 绑定到该phase的goals -->
<goals>
<goal>copy-resources</goal>
</goals>
<!-- 这个goal执行的配置 -->
<configuration>
<!-- 复制到哪个目录,target/conf -->
<outputDirectory>${project.build.directory}/conf</outputDirectory>
<!-- 要复制的资源 -->
<resources>
<resource>
<directory>src/main/resources</directory>
<!-- 排除日志配置文件 -->
<excludes>
<exclude>logback.xml</exclude>
</excludes>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
......
</executions>
</plugin>resources插件下配置了很多execution,每一个execution就是一个复制资源的任务。这段配置中,是将maven-resources-plugin插件的copy-resources目标,绑定到process-resources阶段去执行。<executions>下是插件执行的配置,下面每一个<execution>子元素可以用来配置执行一个任务。有这些任务:
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
配置比较简单,直接看注释吧:
<execution>
<id>copy-conf</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- project.build.directory = target -->
<outputDirectory>${project.build.directory}/conf</outputDirectory>
<resources>
<resource>
<!-- 复制这个目录下的文件 -->
<directory>src/main/resources</directory>
<excludes>
<!-- 排除 logback.xml -->
<exclude>logback.xml</exclude>
</excludes>
<!-- 不做资源过滤 -->
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution><filtering>false</filtering>的意思就是复制的过程中,不会让maven把资源文件中出现的属性${xxx}替换成具体的值。(参考Maven的资源过滤)

这个任务就是复制src/main/resources目录下的文件,主要为配置文件,还有freemaker模板文件。
特别要注意的是,这里没有复制日志框架的 logback.xml 配置文件,而是在后面要介绍到的其它execution(copy-service-conf)中,从src/main/conf/logback.xml复制过去,而且在复制过程中做了资源过滤。这样做的目的,我理解是把开发阶段的日志配置和部署的日志配置分开来管理。
copy-service-conf
<execution>
<id>copy-service-conf</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- target/conf -->
<outputDirectory>${project.build.directory}/conf</outputDirectory>
<resources>
<resource>
<directory>src/main/conf</directory>
<filtering>true</filtering>
</resource>
</resources>
<filters>
<!-- thingsboard/packaging/java/filters/unix.properties -->
<filter>${main.dir}/packaging/${pkg.type}/filters/unix.properties</filter>
</filters>
</configuration>
</execution>这段配置可以解释为什么 copy-conf 中没有复制 logback.xml 配置文件,因为 copy-service-conf 做了资源过滤,区分开发环境和部署环境的配置。copy-conf、copy-service-conf 的处理逻辑可以画图理解:
copy-service-conf 把src/main/conf目录下所有资源文件复制到target/conf目录(包括两个文件,看下面截图)下,并且做资源过滤,资源过滤时替换的属性值来自于<filters>标签下指定的unix.properties属性文件。
这个unix.properties具体位置是什么?通过之前说过小技巧,使用antrun插件打印属性,可以知道:
[echoproperties] main.dir=/home/caibh/github/thingsboard/application/..
[echoproperties] pkg.type=java又或者直接看application模块的pom.xml:
application/pom.xml
<properties>
<main.dir>${basedir}/..</main.dir>
<pkg.type>java</pkg.type>
</properties>那么完整的路径就是:
<filter>${main.dir}/packaging/${pkg.type}/filters/unix.properties</filter>
完整路径是:
<filter>/home/caibh/github/thingsboard/packaging/java/filters/unix.properties</filter>再次提醒,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
<execution>
<id>copy-linux-conf</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- js-executor模块下:target/package/linux/conf -->
<outputDirectory>${pkg.linux.dist}/conf</outputDirectory>
<resources>
<resource>
<directory>config</directory>
<filtering>true</filtering>
</resource>
</resources>
<filters>
<filter>${main.dir}/packaging/${pkg.type}/filters/unix.properties</filter>
</filters>
</configuration>
</execution>通过搜索,发现只有msa/js-executor/pom.xml和msa/web-ui/pom.xml两个文件中出现<pkg.linux.dist>属性的定义(看下图),所以能确定 copy-linux-conf 这一项复制资源的配置,是为这两个子模块准备的。这段配置跟 application 模块的打包逻辑关系不大。

作者在这个问题的处理上有点 tricky:
copy-service-conf,作用于java写的模块(application),复制的源目录是
模块目录/src/main/conf,目标目录target/confcopy-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
<execution>
<id>copy-linux-init</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- thingsboard/msa/js-executor/target/package/linux/init -->
<outputDirectory>${pkg.linux.dist}/init</outputDirectory>
<resources>
<resource>
<!-- thingsboard/packaging/js/scripts/init -->
<directory>${main.dir}/packaging/${pkg.type}/scripts/init</directory>
<filtering>true</filtering>
</resource>
</resources>
<filters>
<filter>${main.dir}/packaging/${pkg.type}/filters/unix.properties</filter>
</filters>
</configuration>
</execution>这段配置跟 copy-linux-conf 情况一样,也是给msa/js-executor和msa/web-ui两个子模块准备的。跟 application 模块复制资源的逻辑没关系。
copy-win-conf
<!-- 下面注释以application模块中实际的值为例 -->
<execution>
<id>copy-win-conf</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- pkg.win.dist = target/windows -->
<outputDirectory>${pkg.win.dist}/conf</outputDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>logback.xml</exclude>
</excludes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/conf</directory>
<excludes>
<!-- pkg.name = thingsboard -->
<exclude>${pkg.name}.conf</exclude>
</excludes>
<filtering>true</filtering>
</resource>
</resources>
<filters>
<!-- main.dir = /home/caibh/github/thingsboard -->
<!-- pkg.type = java -->
<filter>${main.dir}/packaging/${pkg.type}/filters/windows.properties</filter>
</filters>
</configuration>
</execution>通过搜<pkg.win.dist>,有以下模块中出现:

先不管这段配置在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
<!-- 下面注释以application模块中实际的值为例 -->
<execution>
<id>copy-control</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- project.build.directory = target -->
<outputDirectory>${project.build.directory}/control</outputDirectory>
<resources>
<resource>
<!-- main.dir = /home/caibh/github/thingsboard (项目根目录) -->
<!-- pkg.type = java -->
<directory>${main.dir}/packaging/${pkg.type}/scripts/control</directory>
<filtering>true</filtering>
</resource>
</resources>
<filters>
<filter>${main.dir}/packaging/${pkg.type}/filters/unix.properties</filter>
</filters>
</configuration>
</execution>copy-control 复制的是打包 linux 系统的 deb、rpm包相关的安装前后、删除前后处理的脚本,还有注册安装成系统服务的配置模板:

copy-install
<!-- 下面注释以application模块中实际的值为例 -->
<execution>
<id>copy-install</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- project.build.directory = application/target -->
<outputDirectory>${project.build.directory}/bin/install</outputDirectory>
<resources>
<resource>
<!-- main.dir = /home/caibh/github/thingsboard (项目根目录) -->
<!-- pkg.type = java -->
<directory>${main.dir}/packaging/${pkg.type}/scripts/install</directory>
<includes>
<!-- ** 表示任何目录下 -->
<include>**/*.sh</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
<filters>
<filter>${main.dir}/packaging/${pkg.type}/filters/unix.properties</filter>
</filters>
</configuration>
</execution>copy-install 复制的是 linux deb、rpm包安装逻辑的相关的脚本,包括:安装ThingsBoard应用、安装数据库结构、日志文件、升级ThingsBoard应用、升级数据库结构等:

copy-windows-control
<!-- 下面注释以application模块中实际的值为例 -->
<execution>
<id>copy-windows-control</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- pkg.win.dist = application/target/windows -->
<outputDirectory>${pkg.win.dist}</outputDirectory>
<resources>
<resource>
<!-- main.dir = /home/caibh/github/thingsboard (项目根目录) -->
<!-- pkg.type = java -->
<directory>${main.dir}/packaging/${pkg.type}/scripts/windows</directory>
<filtering>true</filtering>
</resource>
</resources>
<filters>
<filter>${main.dir}/packaging/${pkg.type}/filters/windows.properties</filter>
</filters>
</configuration>
</execution> 这段配置就是跟 copy-control 对应的,copy-control 是针对linux系统的,而这里是复制windows系统下安装的脚本,注意有一个service.xml的配置文件,这个文件是留给winsw用的(就是下图右边没打绿色钩的那个service.exe,它是在其它任务中复制过去并重命名的),使用 winsw 能按照service.xml中的配置,把ThingsBoard的Java程序包装成Windows系统的服务来运行。

copy-windows-install
<!-- 下面注释以application模块中实际的值为例 -->
<execution>
<id>copy-windows-install</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- pkg.win.dist = application/target/windows -->
<outputDirectory>${pkg.win.dist}/install</outputDirectory>
<resources>
<resource>
<!-- main.dir = /home/caibh/github/thingsboard (项目根目录) -->
<!-- pkg.type = java -->
<directory>${main.dir}/packaging/${pkg.type}/scripts/install</directory>
<includes>
<!-- 仅复制logback.xml -->
<include>logback.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
<filters>
<filter>${main.dir}/packaging/${pkg.type}/filters/windows.properties</filter>
</filters>
</configuration>
</execution>这段配置就是跟 copy-install 对应的,这里的配置仅仅复制了日志配置文件 logback.xml ,由于跟copy-windows-control关系比较紧密,所以把两个复制的动作画在一起,方便理解:

copy-data
<!-- 下面注释以application模块中实际的值为例 -->
<execution>
<id>copy-data</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- project.build.directory = application/target -->
<outputDirectory>${project.build.directory}/data</outputDirectory>
<resources>
<resource>
<directory>src/main/data</directory>
</resource>
<resource>
<directory>../dao/src/main/resources</directory>
<includes>
<include>**/*.cql</include>
<include>**/*.sql</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>这个配置是复制数据库的初始化脚本和一些初始数据配置的:

copy-docker-config
<execution>
<id>copy-docker-config</id>
<phase>${pkg.process-resources.phase}</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resource>
<directory>docker</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
这段配置,是复制docker目录下的资源文件的,凡是模块下有docker目录的,都执行复制的操作:
复制本模块下
docker目录的所有资源文件到本模块的target目录下
这段配置主要是msa模块下的子模块中应用到,跟这里讨论的application模块关系不大。
resources插件小结
下图是整个工程中各个模块引入resources插件的情况:

其中js-executor、web-ui是前端相关的模块,用js写的,application模块则是一个java写的后端模块,${pkg.type}的值有两种:java或js,application模块的值明显就是java,所以在thingsboard/packaging目录下才有java和js两个目录用来存放后端和前端的打包相关的资源文件。
另外,经过上面这么多的<execution>执行的复制资源的动作后,最后复制到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的 <build>/<pluginManagement>中定义的) | copy (8) (复制winsw) | package |
| maven-dependency-plugin(b) (普通的 <build>/<pluginManagement>中定义的) | 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)执行了很多个<execution>定义的复制资源的任务后,已经把需要用到的资源文件都复制thingsboards/application/target目录,接下来还要经过编译主代码、编译protobuf测试代码、测试等插件的执行后,才会在maven-dependency-plugin(a)中把service.exe复制过去。
目前先了解到这一点,我们还是继续按顺序看下去(下面很快会讲到,这里先留一个坑,后面填上)。
protobuf-maven-plugin:test-compile(6)的配置在下面就跳过不说了,因为之前protobuf-maven-plugin的部分已经介绍过。
4.5 maven-compiler-plugin
<!-- project > build > pluginManagement > plugins > plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
<compilerArgs>
<arg>-Xlint:deprecation</arg>
<arg>-Xlint:removal</arg>
<arg>-Xlint:unchecked</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>compiler 插件 的配置项 <compilerArgs> 配置的是javac编译程序的参数,可以用javac -X了解选项的含义:
$ javac -X
-Xlint:<密钥>(,<密钥>)*
要启用或禁用的警告, 使用逗号分隔。
在关键字前面加上 - 可禁用指定的警告。
支持的关键字包括:
deprecation 有关使用了已过时项的警告。
removal 有关使用了标记为待删除的 API 的警告。
unchecked 有关未检查操作的警告。 也就是说,这些选项是用来在命令行打印出java代码的编译警告的。
而<annotationProcessorPaths>(参考:annotationProcessorPaths)就是配置编译处理java代码中的lombok注解(参考:Lombok的Maven配置)
4.6 maven-surefire-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M1</version>
<configuration>
<argLine>
--illegal-access=permit
</argLine>
</configuration>
</plugin>这个surefire插件是执行单元测试用的插件,<argLine>的意思是 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)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-winsw-service</id>
<!-- package -->
<phase>${pkg.package.phase}</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<!-- 复制文件的坐标 -->
<groupId>com.sun.winsw</groupId>
<artifactId>winsw</artifactId>
<classifier>bin</classifier>
<type>exe</type>
<!-- 重命名为service.exe -->
<destFileName>service.exe</destFileName>
</artifactItem>
</artifactItems>
<!-- target/windows -->
<outputDirectory>${pkg.win.dist}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>这段配置的作用是从maven本地仓库复制winsw到thingsboard/application/target目录,并重命名为service.exe,winsw 是一个可以在Windows系统下将Java程序包装成系统服务去运行的工具。下图是复制的图解:

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

还有一个细节:为什么能从仓库复制这个东西?因为在thingsboard/pom.xml中的依赖配置中配置了:
<project>
......
<properties>
<winsw.version>2.0.1</winsw.version>
</properties>
......
<dependencies>
......
<dependency>
<groupId>com.sun.winsw</groupId>
<artifactId>winsw</artifactId>
<version>${winsw.version}</version>
<classifier>bin</classifier>
<type>exe</type>
<scope>provided</scope>
</dependency>
</dependencies>
</project>4.8 maven-jar-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<!-- 不把任何目录下的logback.xml打包到jar包里面 -->
<excludes>
<exclude>**/logback.xml</exclude>
</excludes>
<archive>
<!-- 版本信息 -->
<manifestEntries>
<Implementation-Title>${pkg.implementationTitle}</Implementation-Title>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>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.24.9 spring-boot-maven-plugin
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>${pkg.disabled}</skip>
<mainClass>${pkg.mainClass}</mainClass>
<classifier>boot</classifier>
<layout>ZIP</layout>
<!-- 打出来jar包可以直接执行,如:./thingsboard-3.2.2-boot.jar 这样就能启动 -->
<executable>true</executable>
<!-- 排除devtools依赖,默认就是true -->
<excludeDevtools>true</excludeDevtools>
<embeddedLaunchScriptProperties>
<!-- /usr/share/thingboard/conf -->
<confFolder>${pkg.installFolder}/conf</confFolder>
<!-- /var/log/thingsboard -->
<logFolder>${pkg.unixLogFolder}</logFolder>
<!-- thingsboard.out -->
<logFilename>${pkg.name}.out</logFilename>
<!-- thingsboard -->
<initInfoProvides>${pkg.name}</initInfoProvides>
</embeddedLaunchScriptProperties>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>这个是 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=NONEcan 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)
<configuration>标签下的元素,都是执行 repackage 目标时的参数。比如:
<skip>指定是否跳过执行<execution><mainClass>指定程序入口<classifier>让打出来的jar包带了一个boot标识符:thingsboard-3.2.2-boot.jar<layout>配置jar包内部归档的方式为ZIP<executable>配置为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() {
......
}所以,<embeddedLaunchScriptProperties>就是配置这个内嵌的脚本中一些属性(参考: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:

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-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.idxthingsboard-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-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.2Manifest-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
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/logback.xml</exclude>
</excludes>
<archive>
<manifestEntries>
<Implementation-Title>${pkg.implementationTitle}</Implementation-Title>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>尝试执行,确实是不能运行的:
# 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包时配置文件的目录、日志的目录、日志的文件名
注意,这里的内嵌脚本的配置(
<embeddedLaunchScriptProperties>),是不考虑Windows系统下的情况的。因为把jar包打成可执行文件这种特性,仅仅支持Linux等类unix系统(参考:executable 文档)
4.10 gradle-maven-plugin
<plugin>
<groupId>org.thingsboard</groupId>
<artifactId>gradle-maven-plugin</artifactId>
<configuration>
<!-- thingsboard/packaging/java,如:application -->
<!-- 或:thingsboard/packaging/js,如:js-executor -->
<gradleProjectDirectory>${main.dir}/packaging/${pkg.type}</gradleProjectDirectory>
<!-- 执行的 gradle task, build 是 gradle 默认的task,类似 mvn install -->
<tasks>
<task>build</task>
<task>buildDeb</task>
<task>buildRpm</task>
<task>renameDeb</task>
<task>renameRpm</task>
</tasks>
<!-- 传给 gradle 的参数 -->
<args>
<arg>-PpackagingDir=${main.dir}/packaging</arg>
<arg>-PprojectBuildDir=${basedir}/target</arg>
<arg>-PprojectVersion=${project.version}</arg>
<arg>
-PmainJar=${project.build.directory}/${project.build.finalName}-boot.${project.packaging}
</arg>
<arg>-PpkgName=${pkg.name}</arg>
<arg>-PpkgUser=${pkg.user}</arg>
<arg>-PpkgInstallFolder=${pkg.installFolder}</arg>
<arg>-PpkgCopyInstallScripts=${pkg.copyInstallScripts}</arg>
<arg>-PpkgLogFolder=${pkg.unixLogFolder}</arg>
<arg>--warning-mode</arg>
<arg>all</arg>
</args>
</configuration>
<executions>
<execution>
<!-- 绑定到package生命周期阶段 -->
<phase>${pkg.package.phase}</phase>
<!-- 被绑定的插件的goal,一定要是这个invoke -->
<goals>
<goal>invoke</goal>
</goals>
</execution>
</executions>
</plugin>这段配置是利用 gradle-maven-plugin 调用 gradle 打包 linux 系统的 deb、rpm等安装包的。从<gradleProjectDirectory>的配置可以看出,packaging/java和packaging/js是两个为打包而建立的gradle工程。
注意看上面的<groupId>org.thingsboard</groupId>,表明这个插件是 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 allgradle打包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包,对照这个的配置理解:

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")
}
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
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<finalName>${pkg.name}</finalName>
<descriptors>
<descriptor>${main.dir}/packaging/${pkg.type}/assembly/windows.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>assembly</id>
<phase>${pkg.package.phase}</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
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">
<id>windows</id>
<formats>
<format>zip</format>
</formats>
<!-- pkg.win.dist = target/windows -->
<!-- Workaround to create logs directory -->
<fileSets>
<!-- 这是一个取巧的配置,复制target/windows 目录,
但不复制里面任何文件,目标目录重命名为logs,
就是为了生成一个logs目录
-->
<fileSet>
<directory>${pkg.win.dist}</directory>
<outputDirectory>logs</outputDirectory>
<excludes>
<exclude>*/**</exclude>
</excludes>
</fileSet>
<!-- 复制 install 目录 -->
<fileSet>
<directory>${pkg.win.dist}/install</directory>
<outputDirectory>install</outputDirectory>
<lineEnding>windows</lineEnding>
</fileSet>
<!-- 复制 conf 目录 -->
<fileSet>
<directory>${pkg.win.dist}/conf</directory>
<outputDirectory>conf</outputDirectory>
<lineEnding>windows</lineEnding>
</fileSet>
<!-- 复制 extensions 目录 -->
<fileSet>
<directory>${project.build.directory}/extensions</directory>
<outputDirectory>extensions</outputDirectory>
</fileSet>
<!-- 复制 data 目录 -->
<fileSet>
<directory>${project.build.directory}/data</directory>
<outputDirectory>data</outputDirectory>
</fileSet>
</fileSets>
<files>
<!-- 复制thingsboard-3.2.2-boot.jar -->
<file>
<source>${project.build.directory}/${project.build.finalName}-boot.${project.packaging}</source>
<!-- 复制到分发包的lib目录下 -->
<outputDirectory>lib</outputDirectory>
<destName>${pkg.name}.jar</destName>
</file>
<!-- 复制 winsw.exe,重命名为thingsboard.exe -->
<file>
<source>${pkg.win.dist}/service.exe</source>
<!-- 复制到分发包的根目录下 -->
<outputDirectory/>
<destName>${pkg.name}.exe</destName>
</file>
<!-- 复制 service.xml,重命名为thingsboard.xml,是winsw对应的启动的配置文件 -->
<file>
<source>${pkg.win.dist}/service.xml</source>
<outputDirectory/>
<destName>${pkg.name}.xml</destName>
<lineEnding>windows</lineEnding>
</file>
<!-- 复制 安装脚本 -->
<file>
<source>${pkg.win.dist}/install.bat</source>
<outputDirectory/>
<lineEnding>windows</lineEnding>
</file>
<!-- 复制 卸载脚本 -->
<file>
<source>${pkg.win.dist}/uninstall.bat</source>
<outputDirectory/>
<lineEnding>windows</lineEnding>
</file>
<!-- 复制 升级脚本 -->
<file>
<source>${pkg.win.dist}/upgrade.bat</source>
<outputDirectory/>
<lineEnding>windows</lineEnding>
</file>
</files>
</assembly>
注意有个小细节,一段tricky的配置:
<fileSet>
<!-- target/windows -->
<directory>${pkg.win.dist}</directory>
<outputDirectory>logs</outputDirectory>
<excludes>
<exclude>*/**</exclude>
</excludes>
</fileSet>这是段配置复制 target/windows 目录,但不复制里面任何文件,目标目录重命名为logs,就是为了生成一个logs目录
4.12 maven-install-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<configuration>
<file>${project.build.directory}/${pkg.name}.deb</file>
<artifactId>${project.artifactId}</artifactId>
<groupId>${project.groupId}</groupId>
<version>${project.version}</version>
<classifier>deb</classifier>
<packaging>deb</packaging>
</configuration>
<executions>
<execution>
<id>install-deb</id>
<phase>${pkg.package.phase}</phase>
<goals>
<goal>install-file</goal>
</goals>
</execution>
</executions>
</plugin>这段 install 插件的配置(参考:install:install-file),就是把打包出来的 target/application/thingsboard.deb复制一份到仓库,并且复制的时候把命名改一下,看下图:

至此,整个 application 模块的打包过程全部捋了一遍。
5. 总结
把 ThingsBoard application 模块整个构建逻辑看完一遍之后基本了解了项目的整体规划是怎样的。
这个项目把前后端代码都放在一个工程构建中,这种做法叫monorepo。如果从单个开发者的角度,那么每个开发这肯定喜欢自己管自己的,自己的项目有独立的git提交历史;但如果作为一个项目负责人的角度去看那就不一定了,monorepo由于项目所有相关的东西都放在一块了,所以容易对项目形成一个整体的思维。
当然放在一块也有缺点,其中明显的感觉就是构建变复杂了,需要把各个模块共性的逻辑抽取出来,同时又保留各模块构建逻辑的灵活性,搞不好就会写出一堆臃肿的构建配置。
在了解了ThingsBoard的构建逻辑后,其实可以“抄作业”了,这个项目在打包、配置文件、日志文件、启停控制、注册系统服务、跨平台的交付件等各个方便都考虑得比较全面,值得借鉴。