Apache Spark原始碼解析之Maven構建

語言: CN / TW / HK

使用Spark作為生產時,我們常常需要指定Spark的Profile、以及它所依賴的版本進行編譯。這個過程其實並不簡單,裡面涉及到了很多的Maven打包、構建內容可以值得研究。看看Apache的頂級專案是如何構建的,對我們自己開發應用也是非常有幫助的。

  • Apache pom

    • 應用釋出的位置

    • 定義Maven外掛版本

    • apache-release

  • modules

  • 依賴控制

    • Profile依賴範圍

    • 測試依賴

  • 構建的核心:外掛

    • enforcer外掛

    • build-heldper外掛

    • scala-maven外掛

    • maven編譯外掛

    • antlr4外掛

    • surefire外掛

    • scalatest外掛

    • maven-source外掛

    • clean外掛

    • exec外掛

    • maven-assembly外掛

    • shade外掛

    • install外掛

    • maven-dependency外掛

    • scalastyle外掛

  • Profile

    • 版本切換

    • 新增模組

    • 依賴與外掛切換

    • 空Profile

    • 切換repository

  • 附錄

    • scalatest

    • 不同風格的測試

    • 使用scalatest Maven外掛

    • 斷言

    • Matcher語法

Apache pom

只要是Apache的專案,一般都會從一個名為apache的專案中繼承。

<parent>
<groupId>org.apache</groupId>
<artifactId>apache</artifactId>
<version>18</version>
</parent>

這個pom中定義了以下內容:

  1. 應用釋出的位置。

  2. 定義了一系列Maven外掛的版本。

  3. 以及一個名稱為Apache的profile。

應用釋出的位置

通過配置distributionManagement可以將打包後的JAR釋出到Maven的Nexus伺服器中。

  <distributionManagement>
<repository>
<id>apache.releases.https</id>
<name>Apache Release Distribution Repository</name>
<url>https://repository.apache.org/service/local/staging/deploy/maven2</url>
</repository>
<snapshotRepository>
<id>apache.snapshots.https</id>
<name>Apache Development Snapshot Repository</name>
<url>https://repository.apache.org/content/repositories/snapshots</url>
</snapshotRepository>
</distributionManagement>

一般我們自己編譯的時候,是不會執行這一部操作的。同時,Apache也設定了預設的Maven倉庫地址。

  <repositories>
<repository>
<id>apache.snapshots</id>
<name>Apache Snapshot Repository</name>
<url>https://repository.apache.org/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>

定義Maven外掛版本

在Apache中定義了非常多的Maven外掛版本,這些外掛會直接影響整個Spark的構建,Spark的整個編譯打包過程,都是由這些外掛完成的,它們也是Maven的核心。下面做一個簡單介紹。

maven-antrun-plugin

可以在Maven構建過程中執行一系列Ant命令,簡單來說,就是將Ant的功能直接嵌入到Maven中。大家可以在Ant的官網中看到可以使用的外掛。https://ant.apache.org/manual/tasksoverview.html,這裡面大家可以看到非常豐富的Ant任務。

maven-assembly-plugin

打包外掛。Apache的專案都是使用assembly打包的。所以,不要再問它和Shade外掛的區別。因為它是用來構建、釋出原始碼、程式集的。而不僅僅是打JAR包。後續大家可以注意到,每個Apache都有一個assembly專案用於打包。

maven-clean-plugin

清理外掛。

maven-compiler-plugin

編譯Java原始碼的外掛。

maven-dependency-plugin

執行依賴操作的外掛。我們在pom.xml中的依賴處理,就是由這個外掛完成。它可以分析專案的依賴關係。

maven-deploy-plugin

將構建的JAR包、或者是pom,推送到遠端倉庫。

maven-docck-plugin

檢查外掛文件的外掛。如果開發的是一個Maven常見,這個外掛會檢查當前開發的Maven外掛文件是否齊全。

maven-enforcer-plugin

這個外掛用來在執行編譯時,對一些環境依賴做檢查確認。這個是在編譯構建過程中非常容易出現問題的外掛。

maven-failsafe-plugin

在一個單獨的classloader中執行JUnit測試。

maven-gpg-plugin

為當前構建的JAR包、以及pom建立簽名。

maven-install-plugin

將打包後的JAR包、或者pom安裝到本地倉庫。

maven-invoker-plugin

這個外掛比較適合用於整合測試。它可以確定每個專案是否執行成功,驗證專案的輸出。

maven-jar-plugin

構建一個JAR包外掛。

maven-javadoc-plugin

為當前專案建立Java doc。

maven-plugin-plugin

這個外掛是用於開發Maven plugin用的,基於mojo建立一個plugin描述符。

maven-project-info

生成標準的專案資訊報告。

maven-release-plugin

釋出當前專案,在SCM中更新pom。

maven-remote-resources

將遠端的資源複製到輸出目錄中。

maven-resources-plugin

將資源複製到輸出目錄,以便打包到JAR中。

maven-scm-plugin

執行scm相關命令。例如:checkout、checkin之類的。

maven-scm-publish

將Maven的網頁釋出到scm的某個位置。

maven-site-plugin

為當前專案生成一個站點。

maven-source-plugin

為當前專案構建一個source jar包。

maven-surefire-plugin

在一個隔離的classloader中執行Junit單元測試。

maven-surefire-report

生成單元測試報告。

maven-war-plugin

構建一個WAR包。

apache-rat-plugin

稽核工具。

doxia-core

用於生成靜態、和動態內容。可以生成很多種類的檔案格式。

xercesImpl

高效能XML解析器。

clirr-maven-plugin

比較二進位制檔案或者源的相容性。

詳細內容大家可以自己去Maven官方網站瀏覽。

apache-release

這個Profile中定義了apached釋出需要使用的外掛。我們自己編譯是不需要選擇該Profile的。。

  • maven-assembly-plugin

  • maven-deploy-plugin

  • maven-source-plugin

  • maven-javadoc-plugin

  • maven-gpg-plugin

裡面也是一系列的約定,它定義了打包檔案的位置、部署、打包原始碼等。

modules

Spark專案中有很多模組,以下是截取出來固定的一部分。

  <modules>
<module>common/sketch</module>
<module>common/kvstore</module>
<module>common/network-common</module>
<module>common/network-shuffle</module>
<module>common/unsafe</module>
<module>common/tags</module>
<module>core</module>
<module>graphx</module>
<module>mllib</module>
<module>mllib-local</module>
<module>tools</module>
<module>streaming</module>
<module>sql/catalyst</module>
<module>sql/core</module>
<module>sql/hive</module>
<module>assembly</module>
<module>examples</module>
<module>repl</module>
<module>launcher</module>
<module>external/kafka-0-10-token-provider</module>
<module>external/kafka-0-10</module>
<module>external/kafka-0-10-assembly</module>
<module>external/kafka-0-10-sql</module>
<module>external/avro</module>
<!-- See additional modules enabled by profiles below -->
</modules>

在一些profile中,還會額外的定義module。例如:

    <profile>
<id>yarn</id>
<modules>
<module>resource-managers/yarn</module>
<module>common/network-yarn</module>
</modules>
</profile>

module標籤中對應的就是目錄結構。例如: <module>sql/hive</module> 對應的就是:sql目錄下的hive目錄的pom.xml檔案所對應的模組。我們嘗試來編譯一個模組:

mvn -Dmaven.test.skip=true -pl core compile

依賴控制

Profile依賴範圍

在Spark的pom.xml中property標籤中,定義了一系列與依賴的scope相關的屬性。這就直接決定是否應該將一些依賴打包到最終的發行包中。

    <hadoop.deps.scope>compile</hadoop.deps.scope>
<hive.deps.scope>compile</hive.deps.scope>
<hive.storage.version>2.7.2</hive.storage.version>
<hive.storage.scope>compile</hive.storage.scope>
<hive.common.scope>compile</hive.common.scope>
<hive.llap.scope>compile</hive.llap.scope>
<hive.serde.scope>compile</hive.serde.scope>
<hive.shims.scope>compile</hive.shims.scope>
<orc.deps.scope>compile</orc.deps.scope>
<parquet.deps.scope>compile</parquet.deps.scope>
<parquet.test.deps.scope>test</parquet.test.deps.scope>

Spark的pom.xml中,所有的依賴都是在parent的pom.xml中定義。例如:

<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop.version}</version>
<scope>${hadoop.deps.scope}</scope>
</dependency>

Spark專案中,這些依賴的控制是放在assembly子專案的pom的 中。

<profile>
<id>hadoop-provided</id>
<properties>
<hadoop.deps.scope>provided</hadoop.deps.scope>
</properties>
</profile>

該profile直接決定了,是否需要將hadoop依賴打包到Spark中。而在Spark core這些子模組中,無需處理版本、以及

<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
</dependency>

所有的依賴,以及依賴之間衝突的處理,都是在parent pom中解決。

測試依賴

在Spark parent pom.xml中,我們看到了一些單元測試的依賴。這些測試的依賴是新增到每個子專案中的。

    <dependencies>
<!-- 由scalatest外掛需要使用,所有子模組都需要依賴。-->
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalatestplus</groupId>
<artifactId>scalatestplus-scalacheck_${scala.binary.version}</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalatestplus</groupId>
<artifactId>scalatestplus-mockito_${scala.binary.version}</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalatestplus</groupId>
<artifactId>scalatestplus-selenium_${scala.binary.version}</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.novocode</groupId>
<artifactId>junit-interface</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

關於scalatest大家可以參考附錄。

構建的核心:外掛

spark的parent pom中定義了一系列的外掛,它通過這些Maven的外掛實現了豐富的構建功能。花一些時間來研究這些外掛,對我們構建Spark來說是非常有意義的。將來,我們自己開發專案也可以參考Spark中的一些使用方法。

enforcer外掛

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M2</version>
<executions>
<execution>
<id>enforce-versions</id>
<goals>
<!-- goal:enforece對應的是validate階段,也就是在執行Maven的validate的時候就會執行該外掛 -->
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireMavenVersion>
<!-- 要求的Maven版本為:3.6.3 -->
<version>${maven.version}</version>
</requireMavenVersion>
<requireJavaVersion>
<!-- 要求的JDK版本為:1.8 -->
<version>${java.version}</version>
</requireJavaVersion>
<!-- 禁止依賴,只要找到了這些依賴Enforcer會直接報失敗 -->
<bannedDependencies>
<excludes>
<exclude>org.jboss.netty</exclude>
<exclude>org.codehaus.groovy</exclude>
<exclude>*:*_2.11</exclude>
<exclude>*:*_2.10</exclude>
</excludes>
<searchTransitive>true</searchTransitive>
</bannedDependencies>
</rules>
</configuration>
</execution>
<execution>
<id>enforce-no-duplicate-dependencies</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<!-- 檢查pom中沒有新增重複的依賴項 -->
<banDuplicatePomDependencyVersions/>
</rules>
</configuration>
</execution>
</executions>
</plugin>

上面,我做了一些基本的註釋。關於Enforcer外掛的內建規則可以在:https://maven.apache.org/enforcer/enforcer-rules/index.html裡面找到。大家可以去測試下,使用Enforcer外掛可以控制Maven編譯的版本。

[INFO] --- maven-enforcer-plugin:3.0.0-M2:enforce (enforce-no-duplicate-dependencies) @ scalatest-demo ---
[WARNING] Rule 0: org.apache.maven.plugins.enforcer.BanDuplicatePomDependencyVersions failed with message:
Rule 0: org.apache.maven.plugins.enforcer.BanDuplicatePomDependencyVersions failed with message:


Found 1 duplicate dependency declaration in this project:
- dependencies.dependency[org.scalactic:scalactic_2.12:jar] ( 2 times )

可以看到,出現上面的問題,表示在我們的pom.xml中有重複的scalactic_2.12依賴。

build-heldper外掛

這個外掛的名字可以看出來是與Maven build有關的外掛。裡面包含一系列的小助手來輔助Maven的生命週期。以下是該外掛的功能。

goal 說明
build-helper:add-source 向POM新增更多源目錄。
build-helper:add-test-source 將測試源目錄新增到 POM。
build-helper:add-resource 向POM新增更多資源目錄。
build-helper:add-test-resource 將測試資源目錄新增到 POM。
build-helper:attach-artifact 附加要安裝和部署的其他工件。
build-helper:maven-version 設定一個包含當前maven 版本的屬性。
build-helper:regex-property 通過將正則表示式替換規則應用於提供的值來設定屬性。
build-helper:regex-properties 通過將正則表示式替換規則應用於提供的值來設定屬性。
build-helper:released-version 解析本專案最新發布的版本。
build-helper:parse-version 將版本解析為不同的屬性。
build-helper:remove-project-artifact 從本地儲存庫中刪除專案的工件。
build-helper:reserve-network-port 保留隨機和未使用的網路埠列表。
build-helper:local-ip 檢索當前主機 IP 地址。
build-helper:hostname 檢索當前主機名。
build-helper:cpu-count 檢索可用 CPU 的數量。
build-helper:timestamp-property 根據當前日期和時間設定屬性。
build-helper:uptodate-property 根據檔案集的輸出相對於其輸入是否最新來設定屬性。
build-helper:uptodate-properties 根據多個檔案集的輸出相對於它們的輸入是否是最新的來設定多個屬性。
build-helper:rootlocation 設定一個屬性,該屬性定義多模組構建的根資料夾。
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<!-- 基於當前日期和時間設定一個屬性 -->
<id>module-timestamp-property</id>
<phase>validate</phase>
<goals>
<goal>timestamp-property</goal>
</goals>
<configuration>
<!-- 屬性名稱 -->
<name>module.build.timestamp</name>
<!-- yyyy-MM-dd HH:mm:ss z -->
<pattern>${maven.build.timestamp.format}</pattern>
<timeSource>current</timeSource>
<!-- 時區 -->
<timeZone>America/Los_Angeles</timeZone>
</configuration>
</execution>
<execution>
<id>local-timestamp-property</id>
<phase>validate</phase>
<goals>
<goal>timestamp-property</goal>
</goals>
<configuration>
<name>local.build.timestamp</name>
<pattern>${maven.build.timestamp.format}</pattern>
<timeSource>build</timeSource>
<timeZone>America/Los_Angeles</timeZone>
</configuration>
</execution>
</executions>
</plugin>

add-source、add-test-source、add-resource、add-test-resource這幾個goal是比較有用的。例如:我們想要在Maven中設定多個原始碼目錄,例如:包含java、scala。我們就可以使用這個元件了。

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>add-scala-source</id>
<goals>
<goal>add-source</goal>
</goals>
<phase>process-sources</phase>
<configuration>
<sources>
<source>src/main/scala</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-scala-test-source</id>
<goals>
<goal>add-test-source</goal>
</goals>
<phase>process-sources</phase>
<configuration>
<sources>src/test/scala</sources>
</configuration>
</execution>
</executions>
</plugin>

scala-maven外掛

這個外掛只要是scala專案都必須新增的外掛。因為,我們可不想通過IDEA來控制整個專案的編譯。

<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>4.3.0</version>
<executions>
<execution>
<id>eclipse-add-source</id>
<goals>
<goal>add-source</goal>
</goals>
</execution>
<execution>
<id>scala-compile-first</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile-first</id>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
<execution>
<id>attach-scaladocs</id>
<phase>verify</phase>
<goals>
<goal>doc-jar</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 設定scala編譯版本 -->
<scalaVersion>${scala.version}</scalaVersion>
<!-- 檢查每個依賴是否都使用相同版本的scala -->
<checkMultipleScalaVersions>true</checkMultipleScalaVersions>
<failOnMultipleScalaVersions>true</failOnMultipleScalaVersions>
<!-- 重新編譯模式(預設為增量構建) -->
<recompileMode>incremental</recompileMode>
<useZincServer>true</useZincServer>
<!-- 配置scala編譯引數 -->
<args>
<arg>-unchecked</arg>
<arg>-deprecation</arg>
<arg>-feature</arg>
<arg>-explaintypes</arg>
<arg>-target:jvm-1.8</arg>
<arg>-Xfatal-warnings</arg>
<arg>-Ywarn-unused:imports</arg>
<arg>-P:silencer:globalFilters=.*deprecated.*</arg>
</args>
<!-- 配置scala編譯的JVM引數 -->
<jvmArgs>
<jvmArg>-Xms1024m</jvmArg>
<jvmArg>-Xmx1024m</jvmArg>
<jvmArg>-XX:ReservedCodeCacheSize=${CodeCacheSize}</jvmArg>
</jvmArgs>
<javacArgs>
<javacArg>-source</javacArg>
<javacArg>${java.version}</javacArg>
<javacArg>-target</javacArg>
<javacArg>${java.version}</javacArg>
<javacArg>-Xlint:all,-serial,-path,-try</javacArg>
</javacArgs>
<!-- 編譯時使用的編譯器外掛依賴 -->
<compilerPlugins>
<!-- https://github.com/ghik/silencer -->
<!-- 用於抑制警告的編譯外掛 -->
<compilerPlugin>
<groupId>com.github.ghik</groupId>
<artifactId>silencer-plugin_${scala.version}</artifactId>
<version>1.6.0</version>
</compilerPlugin>
</compilerPlugins>
</configuration>
</plugin>

更多內容大家可以參考:https://davidb.github.io/scala-maven-plugin/testCompile-mojo.html

maven編譯外掛

該外掛為定義Maven的預設編譯過程。預設它編譯的是java原始碼。

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<skipMain>true</skipMain> <!-- skip compile -->
<skip>true</skip> <!-- skip testCompile -->
</configuration>
</plugin>

antlr4外掛

Antlr是一種詞法分析器,可以讀取、處理、執行或者翻譯結構化的文字、或者是二進位制檔案。Spark SQL使用Antlr構建AST。

<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
<version>${antlr4.version}</version>
</plugin>

更詳細的說明請參考:https://www.antlr.org/api/maven-plugin/latest/。

surefire外掛

這個外掛是我們熟悉的,Maven測試外掛。但Spark在使用該外掛時,添加了不少配置。大家參考我的註釋。

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<!-- 執行所有以符合以下模式的Java測試類 -->
<includes>
<include>**/Test*.java</include>
<include>**/*Test.java</include>
<include>**/*TestCase.java</include>
<include>**/*Suite.java</include>
</includes>
<!-- 測試報告生成的目錄 -->
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
<!-- 命令列引數配置 -->
<argLine>-ea -Xmx4g -Xss4m -XX:ReservedCodeCacheSize=${CodeCacheSize} -Dio.netty.tryReflectionSetAccessible=true</argLine>
<!-- 環境變數 -->
<!-- 這些環境變數是啟動Spark測試所需要使用到的 -->
<environmentVariables>
<!--
Setting SPARK_DIST_CLASSPATH is a simple way to make sure any child processes
launched by the tests have access to the correct test-time classpath.
-->

<SPARK_DIST_CLASSPATH>${test_classpath}</SPARK_DIST_CLASSPATH>
<SPARK_PREPEND_CLASSES>1</SPARK_PREPEND_CLASSES>
<SPARK_SCALA_VERSION>${scala.binary.version}</SPARK_SCALA_VERSION>
<SPARK_TESTING>1</SPARK_TESTING>
<JAVA_HOME>${test.java.home}</JAVA_HOME>
</environmentVariables>
<systemProperties>
<log4j.configuration>file:src/test/resources/log4j.properties</log4j.configuration>
<derby.system.durability>test</derby.system.durability>
<java.awt.headless>true</java.awt.headless>
<java.io.tmpdir>${project.build.directory}/tmp</java.io.tmpdir>
<spark.test.home>${spark.test.home}</spark.test.home>
<spark.testing>1</spark.testing>
<spark.master.rest.enabled>false</spark.master.rest.enabled>
<spark.ui.enabled>false</spark.ui.enabled>
<spark.ui.showConsoleProgress>false</spark.ui.showConsoleProgress>
<spark.unsafe.exceptionOnMemoryLeak>true</spark.unsafe.exceptionOnMemoryLeak>
<spark.memory.debugFill>true</spark.memory.debugFill>
<!-- Needed by sql/hive tests. -->
<test.src.tables>src</test.src.tables>
</systemProperties>
<failIfNoTests>false</failIfNoTests>
<excludedGroups>${test.exclude.tags}</excludedGroups>
<groups>${test.include.tags}</groups>
</configuration>

scalatest外掛

<plugin>
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
<version>${scalatest-maven-plugin.version}</version>
</plugin>

maven-source外掛

source外掛為當前專案建立一個jar包。預設情況,會自動生成在target目錄中。

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<attach>true</attach>
</configuration>
<executions>
<execution>
<id>create-source-jar</id>
<goals>
<goal>jar-no-fork</goal>
<goal>test-jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>

clean外掛

除了目標目錄,還可以定義需要清理的目錄。

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<filesets>
<fileset>
<directory>work</directory>
</fileset>
<fileset>
<directory>checkpoint</directory>
</fileset>
<fileset>
<directory>lib_managed</directory>
</fileset>
<fileset>
<directory>metastore_db</directory>
</fileset>
<fileset>
<directory>spark-warehouse</directory>
</fileset>
</filesets>
</configuration>
</plugin>

exec外掛

可以用來執行某個程式或者Java程式。

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${exec-maven-plugin.version}</version>
</plugin>

它有兩個目標:一個是exec、一個是java。exec是在一個獨立的程序中執行應用程式,java是在同一個虛擬機器中執行Java程式。

maven-assembly外掛

assembly打包外掛,它可以讓開發人員將每個模組的輸出合併到一個單獨、可釋出的歸檔檔案中。包括依賴、模組、站點、以及其他檔案。

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<tarLongFileMode>posix</tarLongFileMode>
</configuration>
</plugin>

shade外掛

shade-plugin外掛可以將專案中所有依賴的artifact打包成一個Uber-jar。並且它還可以排除一些依賴、或者是重新命名一些依賴。

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<!-- 定義使用shaded外掛作為最終編譯出來的artifact。如果配置為false,表示使用shaded jar包作為artifact -->
<!-- 如果配置為true,shaded編譯出來的jar包會以xxx-shaded.jar結尾 -->
<shadedArtifactAttached>false</shadedArtifactAttached>
<artifactSet>
<!-- 控制哪些包需要包含到jar中。 -->
<includes>
<include>org.spark-project.spark:unused</include>
<include>org.eclipse.jetty:jetty-io</include>
<include>org.eclipse.jetty:jetty-http</include>
<include>org.eclipse.jetty:jetty-proxy</include>
<include>org.eclipse.jetty:jetty-client</include>
<include>org.eclipse.jetty:jetty-continuation</include>
<include>org.eclipse.jetty:jetty-servlet</include>
<include>org.eclipse.jetty:jetty-servlets</include>
<include>org.eclipse.jetty:jetty-plus</include>
<include>org.eclipse.jetty:jetty-security</include>
<include>org.eclipse.jetty:jetty-util</include>
<include>org.eclipse.jetty:jetty-server</include>
<include>com.google.guava:guava</include>
<include>org.jpmml:*</include>
</includes>
</artifactSet>
<relocations>
<!-- 可以將對應的包重新命名,這樣可以處理包衝突問題。 -->
<!-- 例如在spark專案中下面的這些依賴都變成了:org.sparkproject -->
<relocation>
<pattern>org.eclipse.jetty</pattern>
<shadedPattern>${spark.shade.packageName}.jetty</shadedPattern>
<includes>
<include>org.eclipse.jetty.**</include>
</includes>
</relocation>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>${spark.shade.packageName}.guava</shadedPattern>
</relocation>
<relocation>
<pattern>org.dmg.pmml</pattern>
<shadedPattern>${spark.shade.packageName}.dmg.pmml</shadedPattern>
</relocation>
<relocation>
<pattern>org.jpmml</pattern>
<shadedPattern>${spark.shade.packageName}.jpmml</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>

詳情請參考:https://maven.apache.org/plugins/maven-shade-plugin/shade-mojo.html。例如,我想要打一個shade包,並將 org.scalactic 改名為: cn.slash.scalatest

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
<relocations>
<relocation>
<pattern>org.scalactic</pattern>
<shadedPattern>cn.slash.scalatest</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>

install外掛

將編譯出來的artifact安裝到本地倉庫外掛。

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.0.0-M1</version>
</plugin>

maven-dependency外掛

專門用於管理Maven依賴的外掛。包含了pom依賴相關的很多功能。

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<!-- 將測試執行的classpath生成到一個檔案中 -->
<id>generate-test-classpath</id>
<phase>test-compile</phase>
<goals>
<goal>build-classpath</goal>
</goals>
<configuration>
<includeScope>test</includeScope>
<outputProperty>test_classpath</outputProperty>
</configuration>
</execution>
<execution>
<!-- 將模組所依賴的jar包拷貝到指定目錄 -->
<id>copy-module-dependencies</id>
<!-- 一般設定為package,打包的時候拷貝依賴 -->
<phase>${build.copyDependenciesPhase}</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<!--
runtime: 包含了runtime和compile依賴.
compile: 包含了compile, provided, 和 system 依賴.
test: 包含了所有的依賴 (equivalent to default).
provided: 包含了設定為 provided 依賴.
system: 包含了設定為 system 依賴.
-->

<includeScope>runtime</includeScope>
<outputDirectory>${jars.target.dir}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
image-20211205224207800

可以看到,當我們設定了copy-dependencies構建目標時,自動將依賴拷貝到了指定目錄。

scalastyle外掛

該外掛可以實現對scala語言的靜態分析。通過指定配置檔案,來檢查scala程式碼是否符合規範。如果開啟詳細資訊,可以看到哪些地方的程式碼有違規。

<plugin>
<groupId>org.scalastyle</groupId>
<artifactId>scalastyle-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<verbose>false</verbose>
<failOnViolation>true</failOnViolation>
<includeTestSourceDirectory>false</includeTestSourceDirectory>
<failOnWarning>false</failOnWarning>
<sourceDirectory>${basedir}/src/main/scala</sourceDirectory>
<testSourceDirectory>${basedir}/src/test/scala</testSourceDirectory>
<configLocation>scalastyle-config.xml</configLocation>
<outputFile>${basedir}/target/scalastyle-output.xml</outputFile>
<inputEncoding>${project.build.sourceEncoding}</inputEncoding>
<outputEncoding>${project.reporting.outputEncoding}</outputEncoding>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>

scalastyle-config.xml中定義了大量的靜態規則檢查。

  <!-- 定義了import的順序 -->
<check level="error" class="org.scalastyle.scalariform.ImportOrderChecker" enabled="true">
<parameters>
<parameter name="groups">java,scala,3rdParty,spark</parameter>
<parameter name="group.java">javax?\..*</parameter>
<parameter name="group.scala">scala\..*</parameter>
<parameter name="group.3rdParty">(?!org\.apache\.spark\.).*</parameter>
<parameter name="group.spark">org\.apache\.spark\..*</parameter>
</parameters>
</check>

<!-- 魔數檢查!哈哈哈!但Spark認為這沒啥大不了的,專案中有大量的魔數。 -->
<!-- Doesn't seem super big deal here, and we have a lot of magic numbers ... -->
<check level="error" class="org.scalastyle.scalariform.MagicNumberChecker" enabled="false">
<parameters><parameter name="ignore">-1,0,1,2,3</parameter></parameters>
</check>

Profile

Spark中大量地運用了Profile來定義不同的打包型別。例如:控制版本、編譯對某個功能的支援等。下面我們來找一些比較典型地用法。

版本切換

<profile>
<id>hadoop-2.7</id>
<properties>
<hadoop.version>2.7.4</hadoop.version>
<curator.version>2.7.1</curator.version>
<commons-io.version>2.4</commons-io.version>
<javax.servlet-api.name>servlet-api</javax.servlet-api.name>
</properties>
</profile>

可以看到,當我們選擇使用hadoop-2.7的profile時,Spark重新定義了hadoop以及相關的依賴的版本。預設為按照hadoop 2.7.4版本編譯的。

新增模組

<profile>
<id>yarn</id>
<modules>
<module>resource-managers/yarn</module>
<module>common/network-yarn</module>
</modules>
</profile>

直接新增到 中的都是必選的模組。但有些模式是可選的,就可以放入到 中。如果使用者沒有設定yarn profile,就不會編譯yarn相關的兩個模組。

依賴與外掛切換

<profile>
<id>scala-2.13</id>
<properties>
<scala.version>2.13.4</scala.version>
<scala.binary.version>2.13</scala.binary.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.scala-lang.modules</groupId>
<artifactId>scala-parallel-collections_${scala.binary.version}</artifactId>
<version>0.2.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<configuration>
<args>
<arg>-unchecked</arg>
<arg>-deprecation</arg>
<arg>-feature</arg>
<arg>-explaintypes</arg>
<arg>-target:jvm-1.8</arg>
<!--...省略了一些引數-->
</args>
<compilerPlugins combine.self="override">
</compilerPlugins>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

</profile>

上面的profile對應的是scala的2.13版本。該Profile定義了新的依賴版本,以及新的外掛版本。注意哦,這裡並沒有直接新增依賴,而是控制依賴的配置、以及版本。Spark的parent pom是不會直接管理依賴的。

空Profile

<profile>
<id>hadoop-provided</id>
</profile>

Spark的parent pom中包含了所有的Profile,雖然有些Profile其實是在子模組中定義的。這樣可以避免Maven在命令列編譯時出現警告。

切換repository

<profile>
<id>snapshots-and-staging</id>
<properties>
<!-- override point for ASF staging/snapshot repos -->
<asf.staging>https://repository.apache.org/content/groups/staging/</asf.staging>
<asf.snapshots>https://repository.apache.org/content/repositories/snapshots/</asf.snapshots>
</properties>

<pluginRepositories>
<pluginRepository>
<id>ASF Staging</id>
<url>${asf.staging}</url>
</pluginRepository>
<pluginRepository>
<id>ASF Snapshots</id>
<url>${asf.snapshots}</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>

</pluginRepositories>
<repositories>
<repository>
<id>ASF Staging</id>
<url>${asf.staging}</url>
</repository>
<repository>
<id>ASF Snapshots</id>
<url>${asf.snapshots}</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</profile>

大家可以看到,上面的profile,直接可以切換repositorty的地址。例如:我們要編譯出來一個專案支援CDH或者Apache。就可以使用Profile來切換連線到不同的Repository來編譯。

附錄

scalatest

JUnit框架在Java開發領域有廣泛使用,在編寫Scala過程中,我們也可以使用Junit。但其實,Scala也有自己的「Native」的單元測試框架,而且Spark框架裡面大量使用了scalatest。所以,今天我們來看看scala特色的單元測試是什麼樣的。

簡介

ScalaTest是Scala生態系統中最靈活、最受歡迎的測試工具。使它可以測試 Scala、Scala.js (JavaScript)、Scala Native、和 Java 程式碼。它可以與 JUnit、TestNG、Ant、Maven、sbt、ScalaCheck、JMock、EasyMock、Mockito、ScalaMock、Selenium、Eclipse、NetBeans 和 IntelliJ 等工具的深度整合。它很容易入手,容易討好各種團隊開發經驗。支援多種風格的測試。

image-20211205114733607

專案依然在維護著,只不過更新迭代沒有那麼快,但最新的scala 3,已經支援了。

ScalaTest核心概念

  1. ScalaTest的核心概念是Suite,它可以包含多個測試(Test)。

  2. 一個Test可以有任意的名字,可以是start、successed、fail、pending、canceled狀態。

  3. Suite這個Trait(特質)定義了run方法、以及其他的生命週期方法。這些生命週期的方法定義了編寫、執行測試的預設方法。我們可以在自己的Suite中覆蓋這些方法,編寫自定義的編寫和執行方式。

  4. ScalaTest可以支援不同的測試樣式。例如:類似JUnit方式的測試用例。

  5. ScalaTest提供了mix-in特質方式來重寫生週期方法以滿足特定的需求。

  6. 可以在類中組合Suite樣式、和min-in特質來定義測試類。

  7. 可以組合Suite示例來定義測試套件。

ScalaTest 支援不同風格的測試,我們可以根據自己的專案情況來選擇不同的風格。而測試風格,僅僅是決定測試用例的「樣式」,就是大概長什麼樣子。而ScalaTest中的所有其他內容,例如:斷言、匹配器、混合特質等都是同樣的工作方式。官方推薦的方式是:使用FlatSpec風格單元測試。FlatSpec方式和我們所熟悉的JUnit風格是類似的(非巢狀的)。我們這裡演示幾種,其他的大家自己去看官網。

引入Maven依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>org.slash</groupId>
<artifactId>scalatest-demo</artifactId>
<version>1.0-SNAPSHOT</version>

<repositories>
<repository>
<id>artima</id>
<name>Artima Maven Repository</name>
<url>https://repo.artima.com/releases</url>
</repository>
</repositories>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.scalactic</groupId>
<artifactId>scalactic_2.12</artifactId>
<version>3.2.3</version>
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_2.12</artifactId>
<version>3.2.3</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
  • 其中scalactic是ScalaTest的兄弟專案,不是必須的,它旨在通過型別資訊提高質量。

  • IDEA的scala外掛已經集成了scalatest,所以,可以直擊在IDEA中直接執行測試。

不同風格的測試

FunSuite風格測試

這種風格和JUnit非常類似,也是一種扁平化的風格測試。

實現方法:

  1. 從AnyFunSite繼承

  2. 呼叫test(xxx){}方法。

參考程式碼:

/**
* FunSuite風格單元測試
* 1. 從AnyFunSite繼承
* 2. 呼叫test() {} 函式
*/

class SuiteTest extends AnyFunSuite {
test("這是一個FunSuite單元測試") {
assert(Set.empty.size == 0)
}
test("這是一個丟擲異常的單元測試") {
assertThrows[NoSuchElementException] {
Set.empty.head
}
}
}

這種方式和Junit非常類似,也使用Spark中使用的單元測試方式。它比JUnit要優秀的是,可以給每個測試用例指定名稱。

FlatSpec風格測試

這種也是扁平化風格的測試,它要求測試有更規範的約定。

實現方法:

  1. 從AnyFlatSpec繼承。

  2. 這種方式的測試用例,必須以 X should Y、或者 A must B風格命名。

參考程式碼:

class FlatSpecTest extends AnyFlatSpec{
"An empty Set" should "have size 0" in {
assert(Set.empty.size == 0)
}

it should "product NullPointerException when head is invoked" in {
assertThrows[NoSuchElementException] {
Set.empty.head
}
}
}

這種方式對測試用例的結構要求更為嚴格。形式為:

字串主語/it should/will/can/must "字串說明" in {
測試程式碼
}

FunSpecTest風格測試

這種風格是一種巢狀結構風格。

實現方法:

  1. 從AnyFunSpec繼承。

  2. 使用describe/it來定義層次結構。descripbe定義父節點,it定義葉子節點。

參考程式碼:

class FunSpecTest extends AnyFunSpec{
describe("這是一個測試集") {
describe("當Set為空時") {
it("大小為0") {
assert(Set.empty.size == 0)
}

it("執行head方法丟擲異常") {
assertThrows[NoSuchElementException]{
Set.empty.head
}
}
}
}
}

使用scalatest Maven外掛

    <build>
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/main/scala</testSourceDirectory>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<configuration>
<compilerPlugins>
<compilerPlugin>
<groupId>com.artima.supersafe</groupId>
<artifactId>supersafe_2.12.10</artifactId>
<version>1.1.12</version>
</compilerPlugin>
</compilerPlugins>
</configuration>
</plugin>

<!-- 關閉Maven自帶的surefire外掛 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>

<!-- 使用scalatest外掛 -->
<plugin>
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
<version>1.0</version>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
<junitxml>.</junitxml>
<filereports>WDF TestSuite.txt</filereports>
</configuration>
<executions>
<execution>
<id>test</id>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
  • ScalaTest官方推薦我們使用SuperSafe社群版本的scala編譯外掛。

  • 要讓Maven能夠自動執行scalatest,需要引入scalatest-maven-plugin外掛。

使用Maven外掛執行自動測試。

mvn test

FlatSpecTest:
An empty Set
- should have size 0
- should product NullPointerException when head is invoked
FunSpecTest:
這是一個測試集
當Set為空時
SuiteTest:
- 這是一個FunSuite單元測試
- 這是一個丟擲異常的單元測試
Run completed in 159 milliseconds.
Total number of tests run: 4
Suites: completed 4, aborted 0
Tests: succeeded 4, failed 0, canceled 0, ignored 0, pending 0
All tests passed.

可以看到,所有測試都執行通過了。

斷言

scala中一共有三種斷言,分別是:

  • assert:通用斷言。

  • assertResult:斷言預期值和實際值。

  • assertThrows:確保程式碼會丟擲指定的異常。

前面我們已經使用過了。

class FlatSpecTest extends AnyFlatSpec
"呼叫fail()函式" should "執行失敗!" in {
fail("強制執行失敗了!")
}

"呼叫cancel()函式" should "取消執行" in {
cancel("強制取消執行測試!")
}
}

看,可以在測試用強制錯誤關閉、或者取消掉測試執行。

Matcher語法

Matcher語法是scalatest提供的斷言DSL。下面簡單列舉了幾種方式:

class MatcherTest extends AnyFlatSpec with Matchers{
"測試用例1" should "執行成功" in {
// 使用匹配器檢查相等性
1 should equal(1)
Seq(1, 2, 3) should equal(Array(1, 2, 3))

// 檢查尺寸和長度
val a = (0 until 10)
a should have size 10
a should have length 10

// 檢查字串
val b = "usecast"
b should startWith("use")
b should endWith("cast")
b should include("seca")
b should include regex("^u.*t$")

// 大於和小於
val c = 10
c should be < 20
c should be > 5
}
}

還有很多的這種支援的語法,不過原本assert的一個簡單呼叫可以實現的事情,這種看著會讓人感覺很蒙圈。因為scala呼叫函式的語法非常豐富,所以,這可以實現出一些看起來讓人摸不著頭腦的寫法來。這種用法在一些很scala的專案中可以看到。我個人覺得assert可以滿足絕大多數需求,比如:Spark中就是用assert來編寫斷言的。但有些scala專案,就用的FlatSpec + Matcher風格的測試。例如:Livy專案。

  it should "execute `1 + 2` == 3" in withSession { session =>
val statement = execute(session)("1 + 2")
statement.id should equal(0)

val result = parse(statement.output)
val expectedResult = Extraction.decompose(Map(
"status" -> "ok",
"execution_count" -> 0,
"data" -> Map(
"text/plain" -> "[1] 3"
)
))

result should equal(expectedResult)
}

參考文獻:

[1] https://maven.apache.org/

[2 https://www.scalatest.org/

[3] https://github.com/scalatest/scalatest

[4] http://scalamock.org/