相比起Maven的XML配置方式,Gradle提供了一套簡明的DSL用於構建Java項目,使我們就像編寫程序一樣編寫項目構建腳本。本文將從無到有創建一個用Gradle構建的Spring Boot項目,並在此過程中講到Gradle的一些典型用法。
本文Github代碼:https://github.com/davenkin/gradle-spring-boot.git
創建Gradle工程
Gradle采用了與Maven相同的目錄組織結構,你可以通過Spring Initializr網站創建Spring Boot工程。但是在本文中,我們將全部通過命令行操作創建Spring Boot工程。首先在命令行中創建如下目錄結構:
└── src
├── main
│ └── java
└── test
└── java
然后在src同級目錄中添加一個build.gradle文件,內容如下:
apply plugin: 'java'
大功告成,一個用Gradle構建的Java項目創建好了,盡情用以下命令編譯並打包咱們的Java項目吧:
gradle build
只是現在咱們的Java項目還是一個空架子,不用急,在下文中我們將一步一步在這個空架子中搭建一個有血有肉的Spring Boot項目。
值得一提的是,雖然此時的build.gradle文件中只有一行配置(apply plugin: 'java',作用是引入java插件),但是其背后已經幫我們做了很多事情,比如它使得我們能夠運行gradle build命令。這里的build即為Gradle中的一個任務(Task),我們還可以運行以下命令查看到更多的Task。
gradle tasks
此時輸出:
...
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.
...
這里的assemble、build和jar等Task都是java插件引入的。build.gradle是Gradle的配置文件,更多關於Gradle的知識請參考筆者的Gradle學習系列文章。
使用Gradle Wrapper
對於所有的Gradle項目來說,筆者都推薦使用Gradle Wrapper,甚至應該將其當做創建代碼庫之后的第一件事來做。使用Gradle Wrapper有以下好處:
- 不用安裝gradle也能運行gradle
- 所有人使用相同的gradle版本
在build.gradle中加入以下配置:
task wrapper(type: Wrapper) {
gradleVersion = '3.0'
}
然后在命令行運行:
gradle wrapper
此時會生成以下三個文件(夾):gradlew、gradlew.bat和gradle目錄。
這里的gradlew和gradlew.bat其實只是腳本文件(前者用於Unix/Linux/Mac,后者用於Windows),在使用gradle命令的地方替換為gradlew或gradlew.bat,他們將自動下載指定的gradle版本,然后用該版本進行項目構建。如上文所示,本文中我們配置gradle版本為3.0。
請注意,這三個文件(夾)都需要提交到代碼庫中。當項目其他人拿到代碼之后,由於gradlew和gradlew.bat文件均在源代碼中,他們本地即便沒有gradle,依然可以通過以下命令進行項目構建:
./gradlew build
如果你的項目有持續集成(CI)服務器(你也應該有),那么你的CI機器也沒有必要安裝Gradle了。另外,此時所有人都是使用的相同版本的gradle,進而避免了由於版本不同所帶來的問題。
添加Spring Boot依賴
在本文中,我們的業務非常簡單———輸出“Hello World!”。但是麻雀雖小,五臟俱全,首先需要在build.gradle中配置spring-boot插件,並引入Spring的Web組件,整個build.gradle如下:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE")
}
}
repositories {
jcenter()
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
sourceCompatibility = 1.8
targetCompatibility = 1.8
task wrapper(type: Wrapper) {
gradleVersion = '3.0'
}
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
testCompile("org.springframework.boot:spring-boot-starter-test")
}
然后創建Application類:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
依然很簡單,是吧?!這個Application類便是Spring Boot程序的入口。另外我們還需要一個Controller和一個業務類HelloWorld:
HelloWorldController:
@RestController("/helloworld")
public class HelloController {
private HelloWorld helloWorld;
public HelloController(HelloWorld helloWorld) {
this.helloWorld = helloWorld;
}
@GetMapping
public String hello() {
return helloWorld.hello();
}
}
HelloWorld:
@Component
public class HelloWorld {
public String hello() {
return "Hello World!";
}
}
此時工程的目錄結構為:
├── README.md
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── src
├── main
│ └── java
│ └── davenkin
│ ├── Application.java
│ ├── HelloController.java
│ └── HelloWorld.java
└── test
└── java
然后運行:
./gradlew bootRun
在瀏覽器或者Postman中打開http://localhost:8080/gradle-spring-boot/helloworld,便可以看到久違的"Hello World!"了。
生成IDE工程文件
我曾經看到不少人在Eclipse或者IntelliJ IDEA中導入Maven/Gradle工程,甚至在IDE中使用嵌入Tomcat容器。我並不推薦這么做,這些嚴重依賴於GUI操作的功能其實是很笨拙、很脆弱的。以嵌入Tomcat容器為例,它要求項目中所有人都在自己的IDE中手動地對Tomcat進行配置,而手動的過程總是容易出錯的。在持續交付中有個原則是“凡是能夠自動化的,都應該自動化”,這里的自動化說白了其實就是代碼化。
因此,在使用Gradle時,筆者更推崇的一種方式是通過Gradle的IDE插件一鍵式地生成IDE工程文件,然后在IDE中直接打開這樣的工程文件。這樣的好處一是非常簡單,二是所有人都使用了相同的IDE配置。
在Gradle中配置IntelliJ IDEA插件,只需在build.gradle中配置:
apply plugin: 'idea'
然后運行:
./gradlew idea
此時將生成后綴為ipr的IntelliJ IDEA工程文件,在IntelliJ IDEA中直接打開(Open)該文件即可。
對於Eclipse,在build.gradle中增加以下配置:
apply plugin: 'eclipse'
然后運行:
./gradlew eclipse
此時將生成Eclipse的.project工程文件。
請注意,所有IDE工程文件都不應該提交到代碼庫,對於Git來說應該將這些文件注冊到.gitignore文件中。各個開發者拿到代碼后需要各自運行./graldlw idea或./gradlew eclipse命令以生成本地工程文件。
調試
至少有兩種方式可以對Spring Boot項目進行調試。一種是直接運行命令:
./gradlew bootRun --debug-jvm
此時程序將默認監聽5005端口,並暫停以等待調試客戶端的連接,然后啟動Spring Boot。
另一種方式是使用Gradle的Application插件,在build.gradle中添加:
apply plugin: 'application'
applicationDefaultJvmArgs = [ "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" ]
此時運行:
./gradlew bootRun
程序將啟動並監聽5005調試端口,但是與第一種方法不同的是,程序不會暫停,而是將直接啟動整個Spring Boot程序。如果你想調試Spring Boot在啟動過程中的某些代碼,比如Spring框架啟動代碼,那么請選擇第一種方式;否則,第二種是更合適的選擇,因為我們並不是每次啟動程序都一定會調試的,對吧?!
自動化測試
軟件項目可以包含多種自動化測試,比如單元測試、集成測試、功能測試等。對於Spring Boot項目來說,筆者推薦將自動化測試划分為單元測試和API測試,其中單元測試即是傳統的單元測試,而API測試包含了集成測試、功能測試和端到端測試的功能,它的測試對象是程序向外暴露的REST API接口,在測試時我們需要啟動整個Spring Boot程序,然后模擬客戶端調用這些API接口來完成業務測試用例。
單元測試相對比較簡單,Spring Boot也提供了一些有助於單元測試的設施,但是我並不推薦大家使用,因為單元測試應該是非常純粹、粒度非常小的測試,不應該有框架摻和。
通常來說,單元測試和API測試應該是分離的,也即他們的代碼應該是分開的,運行測試的命令也應該是不同的。但是這給Gradle帶來了難題,因為默認情況下Gradle只提供一個./gradlew test命令用於測試,並且默認要求測試代碼位於src/test/java目錄下。為此,我們需要對Gradle進行改造。
我們的目的是:
- 默認的src/test/java目錄用於單元測試代碼,通過./gradlew test運行
- 新建src/apiTest/java目錄用於API測試代碼,通過./gradlew apiTest運行
可以看到,我么將Gradle默認的測試設施用於了單元測試,也即對於單元測試我們不需要做任何改變。對於API測試而言,首先我們需要添加名為apiTest的源代碼集合(SrouceSet),該SourceSet即對應了src/apiTest/java目錄,在build.gradle文件中增加如下配置:
sourceSets {
apiTest {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
}
}
configurations {
apiTestCompile.extendsFrom testCompile
apiTestRuntime.extendsFrom testRuntime
}
然后,添加一個Test類型的Task用於運行src/apiTest/java目錄下的API測試代碼:
task apiTest(type: Test) {
testClassesDir = sourceSets.apiTest.output.classesDir
classpath = sourceSets.apiTest.runtimeClasspath
}
為了使Intelli IDEA能夠感知到這些新添加的測試代碼,我們需要對Gradle的idea插件進行額外配置:
idea {
module {
testSourceDirs += file('src/apiTest/java')
testSourceDirs += file('src/apiTest/resources')
scopes.TEST.plus += [configurations.apiTestCompile]
scopes.TEST.plus += [configurations.apiTestRuntime]
}
}
另外,為了使本地構建(./gradlew biuld)過程能夠先運行單元測試,再運行API測試,我們還需要做以下配置:
apiTest.mustRunAfter test
build.dependsOn apiTest
第一行的意思是API測試必須運行在單元測試之后,第二行的意思是將API測試包含在build任務中。
使用JaCoCo
JaCoCo是一款代碼測試覆蓋率統計工具,我們主要將其用於統計單元測試的覆蓋率。在build.gradle中增加配置:
apply plugin: "jacoco"
此時運行./gradlew build之后,JaCoCo將在build/jacoco目錄下為單元測試和API測試分別生成原始數據文件(test.exec和apiTest.exec),但是此時並沒有測試報告生成,為此,我們還需要單獨運行:
./gradlew jacocoTestReport
在瀏覽器中打開build/report/jacoco/test/index.html,你將看到單元測試覆蓋率報告:
但是,此時的覆蓋率報告只是針對單元測試的,為了得到API測試的覆蓋率,我們需要添加一個新的Task:
task jacocoApiTestReport(type: JacocoReport){
sourceSets sourceSets.main
executionData apiTest
}
然后運行:
./gradlew jacocoApiTestReport
在瀏覽器中打開build/report/jacoco/jacocoApiTestReport/index.html,你將看到單元測試覆蓋率報告。
有時,我們希望看到單元測試和API測試的整體覆蓋率,此時我們需要再添加一個Task:
//Unit Test and API Test Code coverage all together
task jacocoAllTestReport(type: JacocoReport){
sourceSets sourceSets.main
executionData test, apiTest
}
然后運行:
./gradlew jacocoAllTestReport
在瀏覽器中打開build/report/jacoco/jacocoAllTestReport/index.html,你將看到所有測試整合后的覆蓋率報告。
作為演示,我們在HelloWorld中添加一個新的anotherHello()方法,此時HelloWorld為:
@Component
public class HelloWorld {
public String hello() {
return "Hello World!";
}
public String anotherHello() {
return "Another Hello World!";
}
}
對應的HelloWorldController也變為:
@RestController
public class HelloController {
private HelloWorld helloWorld;
public HelloController(HelloWorld helloWorld) {
this.helloWorld = helloWorld;
}
@GetMapping("/helloworld")
public String hello() {
return helloWorld.hello();
}
@GetMapping("/anotherHelloworld")
public String anotherHello() {
return helloWorld.anotherHello();
}
}
然后,我們讓HelloWorld的單元測試只測試hello()方法,讓API測試只測試anotherHello()方法(也即只調用“anotherHelloworld”的URL接口)。
此時單元測試覆蓋率為:
可以看到,anohterHello()方法沒有被單元測試覆蓋到。而集成測試雖然覆蓋到了anotherHello()方法,卻沒有覆蓋到hello()方法:
總體測試覆蓋率為:
此時,總體測試覆蓋率同時統計了單元測試和集成測試的覆蓋率。
使用Checkstyle
CheckStyle是一種靜態代碼檢查工具,主要用於檢查代碼風格或格式是否滿足要求。首先,我們需要一份配置文件來配置這樣的要求,這里我們采用Google的Checkstyle配置文件。
在biuld.gradle中增加checkstyle插件:
apply plugin: 'checkstyle'
下載Google的checkstyle文件並將其拷貝為config/checkstyle/checkstyle.xml,Gradle的checkstyle插件默認將讀取該配置文件。CheckStyle檢查將包含在./gradlew build中。注:在筆者電腦上,使用Google原始Checkstyle配置文件總是報錯,對Checkstyle進行了一些精簡之后運行成功。
總結
在本文中,我們從無到有創建了一個使用Gradle構建的Spring Boot項目,包括了對項目的編譯打包、運行單元測試和API測試,並且獲得測試覆蓋率報告。另外,我們提倡使用Gradle的idea/eclipse插件生成IDE工程文件,最后我們使用Checkstyle插件對代碼風格/格式做了靜態檢查。