DevOps入門(三)自動化構建工具Gradle
一、Gradle
Gradle是一個開源的專案自動化構建工具,建立在Apache Ant 和Apache Maven概念的基礎上,並引入了基於Groovy的特定鄰域語言(DSL),而不在使用XML形式管理構建指令碼。
Groovy是用於Java虛擬機器的一種敏捷的動態語言,他是一種成熟的面向物件的程式語言,既可以用於面向物件程式設計,也可以用作純粹的指令碼語言。使用該種語言不必編寫過多的程式碼,同時又具有閉包和動態語言的其他特性。
與Java相比較,Groovy完全相容Java語法,分號是可選的,類和方法預設都是public,編譯器給屬性自動新增getter/setter方法,屬性可以直接用點號獲取,不再需要通過方法來獲取,同時最後一個表示式的值
Groovy的高效特性中,自帶了assert語句,它是弱型別的程式語言,直接通過def來定義型別;同時它的括號是可選的,字串有多種寫法。
Gradle有以下作用:
1、 按照約定宣告構建和建設;
2、 強大的支援多工程的構建;
3、 強大的依賴管理(基於Apache ivy),提供強大的便利去構建工程;
4、 權利支援已有的maven或者ivy倉庫基礎建設;
5、 支援傳遞性依賴管理,在哪不需要遠端倉庫和pom.xml和ivy配置檔案的前提下;
6、 基於groovy指令碼構建,其build指令碼使用groovy語言編寫;
7、 具有廣泛的領域模型支援構建;
8、 深度API;
9、 易遷移;
Mac上的Gradle安裝如下:
$ brew update && brew install gradle
安裝後可以使用如下命令驗證安裝:
gradle -v
二、Gradle的目錄結構
Gradle相關的檔案和目錄結構如下:
Project-name
|--modules
|--build.gradle
|--build.gradle
|--gradlew
|--setting.gradle
./builld.gradle 和 ./app/build.grade
gradle專案自動編譯的時候要讀取的配置檔案。比如指定專案的依賴包等。
build.grade有兩個,一個是全域性的
buildscript {
repositories {
// 宣告倉庫源,比如我們構建了一個安卓的庫,現在想要把庫上傳到jcenter中供別人一起使用,則可以上傳到jcenter中
// 具體上傳步驟見:http://www.jcodecraeer.com/a/anzhuokaifa/Android_Studio/2015/0227/2502.html
jcenter()
}
dependencies {
// 說明gradle的版本號
classpath 'com.android.tools.build:gradle:1.3.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
// 所有專案都繼承這個配置
allprojects {
repositories {
mavenLocal()
jcenter()
}
}
在每個應用的根目錄下的build.gradle檔案中設定了模組的gradle構建配置,如下是一個工程專案的build.gradle檔案:
import org.ajoberstar.grgit.* //提供一個Grgit例項,允許與Gradle專案所在的Git倉庫互動
defineProperty("major", 2)
defineProperty("minor", 0)
defineProperty("buildNumber", "DEV")
defineProperty("type", "regression")
defineProperty("sourceMap", "true")
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath 'org.akhikhl.gretty:gretty:1.1.8',
'gradle.plugin.com.tools.security:dependency-check:0.0.4',
'org.ajoberstar:gradle-git:1.1.0',
'gradle.plugin.com.palantir:jacoco-coverage:0.4.0'
}
}
project(":") {
repositories {
mavenCentral()
maven {
url "http://[repalce with deliveryurl]"
}
}
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'rpm'
apply plugin: 'xwar'
apply plugin: 'checkstyle'
apply plugin: "jacoco"
apply plugin: 'com.palantir.jacoco-coverage'
apply plugin: 'idea'
apply plugin: 'dependency.check'
apply plugin: org.akhikhl.gretty.GrettyPlugin
sourceCompatibility = 1.8
targetCompatibility = 1.8
[compileJava, compileTestJava]*.options.collect { options ->
options.encoding = 'UTF-8'
}
dependencies {
compile("javax.servlet:javax.servlet-api:3.1.0",
"org.eclipse.jetty:jetty-webapp:9.2.3.v20140905",
"org.eclipse.jetty:jetty-servlets:9.2.3.v20140905",
'org.eclipse.jetty:jetty-util:9.2.3.v20140905',
'org.tuckey:urlrewritefilter:4.0.4',
"com.github.jknack:handlebars.java:4.0.4",
"com.github.jknack:handlebars-springmvc:4.0.4",
'com.github.jknack:handlebars-guava-cache:4.0.4',
"org.springframework:spring-web:4.2.4.RELEASE",
"org.springframework:spring-webmvc:4.2.4.RELEASE",
"org.springframework:spring-aop:4.2.4.RELEASE",
'org.springframework:spring-context:4.2.4.RELEASE',
'org.springframework:spring-context-support:4.2.4.RELEASE',
...)
testCompile('junit:junit:4.10',
'org.hamcrest:hamcrest-all:1.1',
'org.mockito:mockito-all:1.9.5',
"org.springframework:spring-test:4.1.2.RELEASE")
compile('org.owasp.esapi:esapi:2.1.0') {
force = true
exclude group: 'commons-beanutils'
}
}
jacoco {
toolVersion = "0.7.6.201602180812"
reportsDir = file("$buildDir/jacocoReportDir")
}
jacocoTestReport {
reports {
xml.enabled false
csv.enabled false
html.destination "${buildDir}/jacoco/coverage"
}
afterEvaluate {
classDirectories = files(classDirectories.files.collect {
fileTree(
dir: "${buildDir}/classes/main",
excludes: [
"com/[ProjectName]/web/HomeController.class",
"com/[ProjectName]/web/SearchController.class",
"com/[ProjectName]/web/MapViewController.class",
...
])
})
}
}
jacocoCoverage {
packageThreshold 0.41, LINE, "com/[ProjectName]/ancillary"
packageThreshold 0.42, METHOD, "com/[ProjectName]/ancillary"
packageThreshold 0.57, CLASS, "com/[ProjectName]/ancillary"
packageThreshold 0.36, LINE, "com/[ProjectName]/content"
packageThreshold 0.34, METHOD, "com/[ProjectName]/content"
packageThreshold 0.60, CLASS, "com/[ProjectName]/content"
...
}
test {
useJUnit {
includeCategories 'com.[ProjectName].junit.UnitTests'
}
}
idea {
module {
inheritOutputDirs = false
outputDir = file('out')
testOutputDir = file('out')
}
}
gretty {
servletContainer = "jetty9"
contextPath = ""
httpPort = 8080
host = "0.0.0.0"
scanInterval = 1
fastReload = true
integrationTestTask = "prePush"
onStart {
if (env == "ci" || env == "test" || env == "dev") {
systemProperty("spring.profiles.active", "internal")
}
systemProperty("app.env", System.getProperty('app.env', 'development'))
systemProperty("feature-toggle.conf", new File("src/main/conf/${type}.china-site.properties").absolutePath)
systemProperty("china-site.conf", project.getConfigFile())
systemProperty("log4j.configuration", "file://" + project.file("src/main/conf/log4j.properties").absolutePath)
}
afterEvaluate {
prepareInplaceWebAppClasses.dependsOn(['startAssetsPipelineServer', 'copyUrlRewriteConfig'])
jettyBeforeIntegrationTest.dependsOn(['check', 'checkJs', 'integrationTest', "jacocoTestReport", 'processAssets', ":acceptance-tests:startOmnitureServer", "acceptance-tests:clearListingsInSolr"])
jettyAfterIntegrationTest.dependsOn([":acceptance-tests:stopOmnitureServer", "stopAssetsPipelineServer"])
}
}
task integrationTest(type: Test, description: "Integration Tests.", group: "verification") {
doFirst {
systemProperty("china-site.conf", project.getConfigFile())
}
useJUnit {
includeCategories 'com.[ProjectName].junit.IntegrationTests'
}
}
task npmInstall(type: Exec, description: "Install Node Dependencies.", group: "verification") {
commandLine "npm"
args "install"
}
task stopAssetsPipelineServer(type: Exec, description: "stop assets pipeline server",
group: "asset-pipeline", dependsOn: ['npmInstall']) {
commandLine "node_modules/tiny-asset-pipeline/bin/main"
args "-stop"
ignoreExitValue true
}
task rpm(type: Rpm, dependsOn: 'executableWar') {
pkg = '[project-name]'
summary = '[Project Name]'
version = "${major}.${minor}.${buildNumber}"
release = "1"
user = 'user'
group = 'usgrp'
splunkIndex = '[project-name]'
service '/opt/[project-name]/service'
require 'java-1.8.0-openjdk'
from('src/main/conf') {
exclude '**/*.*.properties'
include '**/*.properties'
into "/opt/conf/[project-name]"
}
}
processResources.doFirst {
def versionFile = new File("src/main/resources/version.properties")
versionFile.text = "app.version = ${major}.${minor}.${buildNumber}"
versionFile.append(System.getProperty("line.separator"))
versionFile.append("commit=" + Grgit.open(project.file('.')).head().id)
}
task checkJs(type: Exec, description: "check JavaScript use jshint and do unit tests.", group: "verification", dependsOn: ['npmInstall']) {
commandLine "node_modules/grunt-cli/bin/grunt"
}
task compressImages(type: Exec, description: "compress images(*.png) use pngquant",
group: "asset-pipeline", dependsOn: ['npmInstall']) {
commandLine "node"
args "node_modules/tiny-asset-pipeline/lib/tools/compressImg.js", "src/main/webapp/assets/images/"
}
gradle.taskGraph.whenReady { taskGraph ->
if (taskGraph.hasTask(prePush) || taskGraph.hasTask(integrationTest)) {
defineProperty('env', 'test')
} else {
defineProperty('env', 'dev')
}
}
task wrapper(type: Wrapper) {
gradleVersion = "2.0"
}
}
String getConfigFile() {
def configFile = new File("src/main/conf/${env}.china-site.properties")
if (!configFile.exists()) {
configFile.createNewFile()
}
return configFile.absolutePath
}
void defineProperty(prop, defaultValue) {
if (!hasProperty(prop)) {
project.ext.set(prop, defaultValue)
}
}
配置分為很多部分,下面會詳細介紹。
./gradle.properties
grade的執行環境配置,比如使用多少記憶體之類的。
./gradlew 和 ./gradlew.bat
自動完成 gradle 環境的指令碼,在linux和mac下直接執行gradlew會自動完成gradle環境的搭建(這兩個檔案由gradle-wrapper建立)。
./local.properties
配置SDK或者NDK的環境路徑,各個機器上這個變數可能都是不一樣的,所以不應該進入版本庫
./setting.gradle
整個專案的管理,比如這個專案包含哪些模組等。
rootProject.name = 'china-site'
include(':acceptance-tests')
project(':acceptance-tests').projectDir = new File(settingsDir, 'acceptance-tests')
./[Project-name].imliml是Intellij Idea的入口檔案,可以使用Idea開啟其工程。
三、Groovy
Groovy是一門jvm語言,它最終是要編譯成class檔案然後在jvm上執行,所以Java語言的特性Groovy都支援,我們完全可以混寫Java和Groovy。既然如此,那Groovy的優勢是什麼呢?簡單來說,Groovy提供了更加靈活簡單的語法,大量的語法糖以及閉包特性可以讓你用更少的程式碼來實現和Java同樣的功能。比如解析xml檔案,Groovy就非常方便,只需要幾行程式碼就能搞定,而如果用Java則需要幾十行程式碼。
3.1 Groovy的變數和方法宣告
在Groovy中,通過 def 關鍵字來宣告變數和方法,比如:
def a = 1;
def b = "hello world";
def int c = 1;
def hello() {
println ("hello world");
return 1;
}
在Groovy中,很多東西都是可以省略的,比如- 語句後面的分號是可以省略的
- 變數的型別和方法的返回值也是可以省略的
- 方法呼叫時,括號也是可以省略的
- 甚至語句中的return都是可以省略的
def a = 1
def b = "hello world"
def int c = 1
def hello() {
println "hello world" // 方法呼叫省略括號
1; // 方法返回值省略return
}
def hello(String msg) {
println (msg)
}
// 方法省略引數型別
int hello(msg) {
println (msg)
return 1
}
3.2 Groovy的資料型別
Groovy中,資料型別有:
- Java中的基本資料型別
- Java中的物件
- Closure(閉包)
- 加強的List、Map等集合型別
- 加強的File、Stream等IO型別
1. String
String的特色在於字串的拼接,比如:
def a = 1
def b = "hello"
def c = "a=${a}, b=${b}"
println c
outputs:
a=1, b=hello
2. 閉包Groovy中有一種特殊的型別,叫做Closure,翻譯過來就是閉包,這是一種類似於C語言中函式指標的東西。閉包用起來非常方便,在Groovy中,閉包作為一種特殊的資料型別而存在,閉包可以作為方法的引數和返回值,也可以作為一個變數而存在。
{ parameters ->
code
}
閉包可以有返回值和引數,當然也可以沒有。下面是幾個具體的例子:
def closure = { int a, String b ->
println "a=${a}, b=${b}, I am a closure!"
}
// 這裡省略了閉包的引數型別
def test = { a, b ->
println "a=${a}, b=${b}, I am a closure!"
}
def ryg = { a, b ->
a + b
}
closure(100, "renyugang")
test.call(100, 200)
def c = ryg(100,200)
println c
閉包可以當做函式一樣使用,在上面的例子中,將會得到如下輸出:a=100, b=renyugang, I am a closure!
a=100, b=200, I am a closure!
300
3. List和Map
Groovy加強了Java中的集合類,比如List、Map、Set等。此處由於主要針對Gradle因此不多涉及。
4. IO
在Groovy中,檔案訪問要比Java簡單的多,不管是普通檔案還是xml檔案。Object下提供了eachLine方法:
public Object eachLine(int firstLine, Closure closure)
因此可以寫出程式碼如下
def file = new File("a.txt")
println "read file using two parameters"
file.eachLine { line, lineNo ->
println "${lineNo} ${line}"
}
四、Gradle的編譯和生命週期
在解析 Gradle 的編譯過程之前我們需要理解在 Gradle 中非常重要的兩個物件。Project和Task。每個專案的編譯至少有一個 Project,一個
build.gradle就代表一個project,每個project裡面包含了多個task,task 裡面又包含很多action,action是一個程式碼塊,裡面包含了需要被執行的程式碼。
在編譯過程中, Gradle 會根據 build 相關檔案(一般是build.gradle),聚合所有的project和task,執行task 中的 action。因為 build.gradle檔案中的task非常多,先執行哪個後執行那個需要一種邏輯來保證。這種邏輯就是依賴邏輯,幾乎所有的Task 都需要依賴其他 task 來執行,沒有被依賴的task 會首先被執行。所以到最後所有的
Task 會構成一個 有向無環圖(DAG Directed Acyclic Graph)的資料結構。
編譯過程分為三個階段:
- 初始化階段:建立 Project 物件,如果有多個build.gradle,也會建立多個project.
- 配置階段:在這個階段,會執行所有的編譯指令碼,同時還會建立project的所有的task,為後一個階段做準備。
- 執行階段:在這個階段,gradle 會根據傳入的引數決定如何執行這些task,真正action的執行程式碼就在這裡.
五、Gradle初始化和引數配置
Gradle在專案中一般使用build.gradle和settings.gradle檔案配置,建立build.gradle檔案的過程如下:
1. gradle init
gradle init命令用於建立初始的build.gradle和setting.gradle檔案,執行命令如下:
$ gradle-sample gradle init
Starting a Gradle Daemon (subsequent builds will be faster)
BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed
建立檔案目錄如下:
此時build.gradle和setting.gradle均為空,gradlew和gradlew.bat為Linux/Mac和Windows系統新增Gradle功能(使用方法為./gradlew [script])。
2. 配置外部引入庫
在build.gradle中使用import命令可以引入外部依賴庫,例項如下:
import org.gradle.api.tasks.Exec
import org.ajoberstar.grgit.* /*提供一個Grgit例項,允許與Gradle專案所在的Git倉庫互動*/
3. 配置依賴專案
在build.gradle中使用apply命令可以引入依賴項,示例如下:
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'rpm'
apply plugin: 'xwar'
apply plugin: 'checkstyle'
apply plugin: "jacoco"
apply plugin: 'com.palantir.jacoco-coverage'
apply plugin: 'idea'
apply plugin: 'dependency.check'
4. 設定maven倉庫和外部依賴
在buildscript中的repositories中配置maven專案
buildscript {
repositories {
jcenter()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath 'com.reagroup.china:gradle-rpm-plugin:1.18',
'com.reagroup.china:gradle-xwar-plugin:1.01',
'org.akhikhl.gretty:gretty:1.1.8',
'gradle.plugin.com.tools.security:dependency-check:0.0.4',
'org.ajoberstar:gradle-git:1.1.0',
'gradle.plugin.com.palantir:jacoco-coverage:0.4.0'
}
}
也可以使用類似如下dependencies配置:
dependencies {
compile("javax.servlet:javax.servlet-api:3.1.0",
"org.eclipse.jetty:jetty-webapp:9.2.3.v20140905",
"org.eclipse.jetty:jetty-servlets:9.2.3.v20140905",
'org.eclipse.jetty:jetty-util:9.2.3.v20140905',
'org.tuckey:urlrewritefilter:4.0.4',
"com.github.jknack:handlebars.java:4.0.4",
"com.github.jknack:handlebars-springmvc:4.0.4",
'com.github.jknack:handlebars-guava-cache:4.0.4',
"org.springframework:spring-web:4.2.4.RELEASE",
"org.springframework:spring-webmvc:4.2.4.RELEASE",
"org.springframework:spring-aop:4.2.4.RELEASE",
'org.springframework:spring-context:4.2.4.RELEASE',
'org.springframework:spring-context-support:4.2.4.RELEASE')
}
repositories中還可以配置本地倉庫和maven預設的中心倉庫:
mavenLocal
mavenCentral
此外如果url指定的倉庫需要配置密碼,也可使用 maven {
url "<you_company_resp_url>"
credentials {
username 'your_username'
password 'your_password'
}
}
Gradle還可以使用本地靜態倉庫,程式碼如下:
repositories{
flatDir{
dirs './static_repo'
}
}
此時在dependencies中的應用方式為:
dependencies{
compile(name:'libraryname', ext:'./static_repo')
}
5. 配置變數
Gradle使用defineProperty函式指定變數:
defineProperty("major", 2)
defineProperty("minor", 0)
defineProperty("buildNumber", "DEV")
defineProperty("s3StaticAssets", "myfun.com.assets")
defineProperty("awsRegion", "cn-north-1")
defineProperty("s3Bucket", "myfun.stg.com.dist")
defineProperty("type", "regression")
defineProperty("sourceMap", "true")
此處定義的變數可以在後續的task中使用。6. 配置JDK版本
sourceCompatibility = 1.8
targetCompatibility = 1.8
基本配置如上,除了如上的基本配置之外,gradle中還包括一些高階配置,以下將舉例說明。
五、Gradle配置Git
Grgit是Andrew Oberstar實現的JGit封裝器,為基於Groovy的工具與Git倉庫互動提供了更簡潔流暢的API。
- org.ajoberstar.grgit - 提供一個Grgit例項,允許與Gradle專案所在的Git倉庫互動
- org.ajoberstar.github-pages - 向Github倉庫的gh-pages分支釋出檔案
- org.ajoberstar.release-base - 提供用於從專案狀態和所在Git倉庫推斷當前專案版本和建立新版本的通用結構
- org.ajoberstar.release-opinion - 用於org.ajoberstar.release-base的預設選項,遵從語義版本控制(Semantic Versioning)
task cloneGitRepo(type: GitClone) {
def destination = file("destination_folder")
uri = "your_git_repo_uri"
destinationPath = destination
bare = false
enabled = !destination.exists() //to clone only once
}
六、Gradle配置test
JUnit和TestNG允許複雜的測試方法分組。對於分組,JUnit有測試類和方法。JUnit 4.8引入了類別的概念。測試任務允許指定要包括或排除的JUnit類別。可以使用build.gradle檔案中的以下程式碼段對測試方法進行分組。如下程式碼所示:
test {
useJUnit {
includeCategories 'org.gradle.junit.CategoryA'
excludeCategories 'org.gradle.junit.CategoryB'
}
}
Test類有一個include和exclude方法。這些方法可以用於指定哪些測試應該執行。只執行包含的測試 -
test {
include '**my.package.name/*'
}
以下程式碼中所示的build.gradle示例檔案顯示了不同的配置選項。
apply plugin: 'java' // adds 'test' task
test {
// enable TestNG support (default is JUnit)
useTestNG()
// set a system property for the test JVM(s)
systemProperty 'some.prop', 'value'
// explicitly include or exclude tests
include 'org/foo/**'
exclude 'org/boo/**'
// show standard out and standard error of the test JVM(s) on the console
testLogging.showStandardStreams = true
// set heap size for the test JVM(s)
minHeapSize = "64m"
maxHeapSize = "512m"
// set JVM arguments for the test JVM(s)
jvmArgs '-XX:MaxPermSize=256m'
// listen to events in the test execution lifecycle
beforeTest {
descriptor → logger.lifecycle("Running test: " + descriptor)
}
// listen to standard out and standard error of the test JVM(s)
onOutput {
descriptor, event → logger.lifecycle
("Test: " + descriptor + " produced standard out/err: "
+ event.message )
}
}
使用方法如下:
gradle <someTestTask> --debug-jvm
七、Gradle配置wrapper
想使用gradle wrapper,首先要在你的專案中建立。具體來說就是在build.gradle裡面加入類似於下面的task:
task wrapper(type: Wrapper) {
gradleVersion = "2.0"
}
然後使用
gradle createWrapper
即可生成如下目錄結構:Project-name/
gradlew
gradlew.bat
gradle/wrapper/
gradle-wrapper.jar
gradle-wrapper.properties
需要使用gradle wrapper的時候,我們就直接在專案根目錄下直接執行./gradlew(gradle wrapper的簡寫), 使用gradlew的方式和gradle一模一樣, 例如通過./gradlew tasks來檢視所有的任務。事實上,執行gradlew命令的時候,gradlew會委託gradle命令來做相應的事情,所以gradlew只是一個殼而已。
八、Gradle配置Task
任務(Task)是用於將任務定義到構建指令碼中的關鍵字。看看下面的例子,它是一個叫作 hello 的任務,將列印一個字串:hello world。將以下指令碼複製並儲存到 build.gradle 檔案中。 此構建指令碼定義一個名稱為 “hello” 的任務,用於列印hello world字串。
task hello {
doLast {
println 'hello world'
}
}
可以通過為 doLast 語句指定快捷方式(表示符號 <<)來簡化此 hello 任務。如果新增這個快捷方式到上面的 hello 任務中,參考如下指令碼。
task hello << {
println 'hello world'
}
任務依賴關係task hello << {
println 'Hello world!'
}
task intro(dependsOn: hello) << {
println "I'm Gradle"
}
九、Gradle配置Jetty
gretty支援熱部署、HTTPS、轉發、除錯、自動化執行環境等諸多特性,讓開發和部署變得更加簡單。
gretty {
servletContainer = "jetty9"
contextPath = ""
httpPort = 8080
host = "0.0.0.0"
scanInterval = 1
fastReload = true
integrationTestTask = "prePush"
onStart {
if (env == "ci" || env == "test" || env == "dev") {
systemProperty("spring.profiles.active", "internal")
}
systemProperty("app.env", System.getProperty('app.env', 'development'))
systemProperty("feature-toggle.conf", new File("src/main/conf/${type}.china-site.properties").absolutePath)
systemProperty("china-site.conf", project.getConfigFile())
systemProperty("log4j.configuration", "file://" + project.file("src/main/conf/log4j.properties").absolutePath)
}
afterEvaluate {
prepareInplaceWebAppClasses.dependsOn(['startAssetsPipelineServer', 'copyUrlRewriteConfig'])
jettyBeforeIntegrationTest.dependsOn(['check', 'checkJs', 'integrationTest', "jacocoTestReport", 'processAssets', ":acceptance-tests:startOmnitureServer", "acceptance-tests:clearListingsInSolr"])
jettyAfterIntegrationTest.dependsOn([":acceptance-tests:stopOmnitureServer", "stopAssetsPipelineServer"])
}
}
常用屬性- scanInterval:監視週期,單位為秒,設定為0等於完全關閉熱部署
- scanDir:需要監視的資料夾
- recompileOnSourceChange:監視原始碼變動,自動編譯
- reloadOnClassChange:編譯的類發生改變,自動載入
- reloadOnConfigChange:WEB-INF或META-INF發生改變
- reloadOnLibChange:依賴發生改變
- fastReload屬性,預設為true,監聽webapp/中的內容,檔案發生改變,無需重啟。