第9章 Spring Boot開發者工具

第9章 Spring Boot開發者工具

Spring Boot為Maven和Gradle提供構建工具插件。

9.1 Spring Boot maven plugin

Spring Boot Maven Plugin,提供了使用Maven構建Spring Boot 工程的支持。我們可以用這個插件完成打包功能。支持打可執行jar包, war包。該插件支持Maven 3.2 +版本。

使用方式如下:

<?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>
    <!-- ... -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>1.5.3.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

在我們的工程pom.xml文件里面配置上述代碼即可。

Spring Boot Maven Plugin提供的goals 如下:

  • repackage: 創建自動可執行的jar包或war包。
  • run: 運行你的Spring Boot 應用,可以配置一些options,參數parameters.
  • start: 管理Spring Boot應用的構建生命周期,默認綁定集成測試階段的pre-integration-test
  • stop : 管理Spring Boot應用的構建生命周期,默認綁定集成測試階段的post-integration-test
  • build-info: 生成Actuator的構建信息。
    對應的命令如下:
mvn spring-boot:repackage
mvn spring-boot:run
mvn spring-boot:start
mvn spring-boot:stop
mvn spring-boot:build-info

9.2 Spring Boot gradle plugin

Spring Boot Gradle Plugin 提供了使用Gradl構建Spring Boot 應用的支持。同樣支持打可執行 jar包或war包。運行 Spring Boot應用時,使用的是spring-boot-dependencies提供的依賴管理。

使用示例:

plugins {
    id 'org.springframework.boot' version '1.5.3.RELEASE'
}

在你的build.gradle配置文件添加上述配置即可。這個看起來,比使用maven的plugins要簡潔多了。這里也是groovy的DSL。

使用上面的配置,Spring Boot Gradle Plugin會完成使用spring-boot-starter-parent bom加載依賴的工作。

然后,我們在dependencies里面直接像下面這樣使用

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.thymeleaf:thymeleaf-spring4")
    compile("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect")
}

如果你想要打war,加上下面這句

apply plugin: 'war'

運行命令:

gradle bootRun

9.3 Spring Boot熱部署:spring-boot-devtools

spring-boot-devtools 是一個為開發者服務的一個模塊,其中最重要的功能就是熱部署。

當我們修改了classpath下的文件(包括類文件、屬性文件、頁面等)時,會重新啟動應用(由于其采用的雙類加載器機制,這個啟動會非常快,另外也可以選擇使用jrebel)。

spring-boot-devtools使用了兩個類加載器來實現重啟(restart)機制:

base類加載器(base ClassLoader), restart類加載器(restart ClassLoader)。

  • base ClassLoader:用于加載不會改變的jar(eg.第三方依賴的jar)
  • restart ClassLoader:用于加載我們正在開發的jar(eg.整個項目里我們自己編寫的類)。當應用重啟后,原先的restart ClassLoader被丟掉、重新new一個restart ClassLoader來加載這些修改過的東西,而base ClassLoader卻不需要動一下。這就是devtools重啟速度快的原因。

使用devtools ,只需要添加其依賴即可 :

Maven

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

Gradle.

dependencies {
    compile("org.springframework.boot:spring-boot-devtools")
}

devtools的功能在命令行運行jar包

 java -jar XXX.jar 

或者,當應用運行在指定的 classloader的時候, 自動失效(考慮到,或許是在生產環境)。

DevTools通過檢測classpath的資源文件 resources的變動來觸發應用的重啟。這個跟我們在 IntelliJ IDEA中, 使用Build -> Make Project,重新構建工程的效果是一樣的。

默認情況下,

/META-INF/maven,/META-INF/resources,/resources,/static,/templates,/public

這些文件夾下的文件修改不會使應用重啟,但是會重新加載(devtools內嵌了一個LiveReload server,當資源發生改變時,瀏覽器刷新)。

如果想改變默認的設置,可以自己設置不重啟的目錄:

spring.devtools.restart.exclude=static/**,public/**

這樣的話,就只有這兩個目錄下的文件修改不會導致restart操作了。
如果要在保留默認設置的基礎上還要添加其他的排除目錄:

spring.devtools.restart.additional-exclude

如果想要使得當非classpath下的文件發生變化時應用得以重啟,使用:

spring.devtools.restart.additional-paths

這樣devtools就會將該目錄列入了監聽范圍。

在application.properties文件中,關于DevTools的鍵值如下:

# ----------------------------------------
# DEVTOOLS PROPERTIES
# ----------------------------------------

# DEVTOOLS (DevToolsProperties)
spring.devtools.livereload.enabled=true # Enable a livereload.com compatible server.
spring.devtools.livereload.port=35729 # Server port.
spring.devtools.restart.additional-exclude= # Additional patterns that should be excluded from triggering a full restart.
spring.devtools.restart.additional-paths= # Additional paths to watch for changes.
spring.devtools.restart.enabled=true # Enable automatic restart.
spring.devtools.restart.exclude=META-INF/maven/**,META-INF/resources/**,resources/**,static/**,public/**,templates/**,**/*Test.class,**/*Tests.class,git.properties # Patterns that should be excluded from triggering a full restart.
spring.devtools.restart.poll-interval=1000 # Amount of time (in milliseconds) to wait between polling for classpath changes.
spring.devtools.restart.quiet-period=400 # Amount of quiet time (in milliseconds) required without any classpath changes before a restart is triggered.
spring.devtools.restart.trigger-file= # Name of a specific file that when changed will trigger the restart check. If not specified any classpath file change will trigger the restart.

# REMOTE DEVTOOLS (RemoteDevToolsProperties)
spring.devtools.remote.context-path=/.~~spring-boot!~ # Context path used to handle the remote connection.
spring.devtools.remote.debug.enabled=true # Enable remote debug support.
spring.devtools.remote.debug.local-port=8000 # Local remote debug server port.
spring.devtools.remote.proxy.host= # The host of the proxy to use to connect to the remote application.
spring.devtools.remote.proxy.port= # The port of the proxy to use to connect to the remote application.
spring.devtools.remote.restart.enabled=true # Enable remote restart.
spring.devtools.remote.secret= # A shared secret required to establish a connection (required to enable remote support).
spring.devtools.remote.secret-header-name=X-AUTH-TOKEN # HTTP header used to transfer the shared secret.

另外,使用Intellij的可能會遇到這個問題,即使項目使用了spring-boot-devtools,修改了類或者html、js等,idea還是不會自動重啟,非要手動去make一下或者重啟,就更沒有使用熱部署一樣。出現這種情況,并不是你的配置問題,其根本原因是因為Intellij IEDA和Eclipse不同,Eclipse設置了自動編譯之后,修改類它會自動編譯,而IDEA在非RUN或DEBUG情況下才會自動編譯(前提是你已經設置了Auto-Compile)。

首先,IDEA設置里面Build project automatically這里打勾

然后 Shift+Ctrl+Alt+/(Mac: Shift+Command+Alt+/),選擇Registry

進去之后,找到如下圖所示的選項,打勾

OK了,重啟一下項目,然后改一下類里面的內容,IDEA就會自動去make了。

筆者在使用maven-scala-plugin + spring-boot-devtools過程中,有個問題這里提一下。在spring-boot-devtools跟maven-scala-plugin一起使用,使用命令行

#!/usr/bin/env bash
mvn clean scala:compile scala:run -Dlauncher=app

啟動會報錯(直接在IDEA中啟動main入口類不報錯,這是scala-maven-plugin跟spring-boot-devtools集成兼容性bug)。報錯日志如下:

00:52:18.748 [restartedMain] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'delegatingApplicationListener' 
00:52:18.748 [restartedMain] DEBUG o.s.boot.devtools.restart.Restarter - Creating new Restarter for thread Thread[main,5,main] 
00:52:18.749 [restartedMain] DEBUG o.s.boot.devtools.restart.Restarter - Immediately restarting application 
00:52:18.749 [restartedMain] DEBUG o.s.boot.devtools.restart.Restarter - Created RestartClassLoader org.springframework.boot.devtools.restart.classloader.RestartClassLoader@1bfafd6 
00:52:18.749 [restartedMain] DEBUG o.s.boot.devtools.restart.Restarter - Starting application com.springboot.in.action.LightSwordApplication with URLs [file:/Users/jack/book/lightsword/target/test-classes/, file:/Users/jack/book/lightsword/target/classes/] 
00:52:18.751 [restartedMain] INFO  scala.App - Started App in 30.547 seconds (JVM running for 31.682) 
java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:497)
        at org_scala_tools_maven_executions.MainHelper.runMain(MainHelper.java:161)
        at org_scala_tools_maven_executions.MainWithArgsInFile.main(MainWithArgsInFile.java:26)
Caused by: org.springframework.boot.devtools.restart.SilentExitExceptionHandler$SilentExitException
        at org.springframework.boot.devtools.restart.SilentExitExceptionHandler.exitCurrentThread(SilentExitExceptionHandler.java:90)
        at org.springframework.boot.devtools.restart.Restarter.immediateRestart(Restarter.java:183)
        at org.springframework.boot.devtools.restart.Restarter.initialize(Restarter.java:162)
        at org.springframework.boot.devtools.restart.Restarter.initialize(Restarter.java:545)
        at org.springframework.boot.devtools.restart.RestartApplicationListener.onApplicationStartedEvent(RestartApplicationListener.java:68)
        at org.springframework.boot.devtools.restart.RestartApplicationListener.onApplicationEvent(RestartApplicationListener.java:45)
        at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:167)
        at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
        at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:122)
        at org.springframework.boot.context.event.EventPublishingRunListener.started(EventPublishingRunListener.java:67)
        at org.springframework.boot.SpringApplicationRunListeners.started(SpringApplicationRunListeners.java:48)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:305)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1187)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1176)
        at com.springboot.in.action.LightSwordApplication$.delayedEndpoint$com$springboot$in$action$LightSwordApplication$1(LightSwordApplication.scala:6)
        at com.springboot.in.action.LightSwordApplication$delayedInit$body.apply(LightSwordApplication.scala:5)
        at scala.Function0.apply$mcV$sp(Function0.scala:34)
        at scala.Function0.apply$mcV$sp$(Function0.scala:34)
        at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
        at scala.App.$anonfun$main$1$adapted(App.scala:76)
        at scala.App$$Lambda$5/1464642111.apply(Unknown Source)
        at scala.collection.immutable.List.foreach(List.scala:389)
        at scala.App.main(App.scala:76)
        at scala.App.main$(App.scala:74)
        at com.springboot.in.action.LightSwordApplication$.main(LightSwordApplication.scala:5)
        at com.springboot.in.action.LightSwordApplication.main(LightSwordApplication.scala)
        ... 6 more


從日志內容,我們可以看出,系統在spring-boot-devtools的Restarter初始化的時候報錯了。代碼如下

    private void onApplicationStartedEvent(ApplicationStartedEvent event) {
        // It's too early to use the Spring environment but we should still allow
        // users to disable restart using a System property.
        String enabled = System.getProperty(ENABLED_PROPERTY);
        if (enabled == null || Boolean.parseBoolean(enabled)) {
            String[] args = event.getArgs();
            DefaultRestartInitializer initializer = new DefaultRestartInitializer();
            boolean restartOnInitialize = !AgentReloader.isActive();
            Restarter.initialize(args, false, initializer, restartOnInitialize);
        }
        else {
            Restarter.disable();
        }
    }

報錯的代碼在if分支

Restarter.initialize(args, false, initializer, restartOnInitialize);

就是說,當我們沒有配置spring.devtools.restart.enabled的值,或者值是true的時候,會進來這行代碼。

繞過這個錯誤的解決辦法,是配置spring.devtools.restart.enabled的值是false。這樣就用不了自動重啟應用的功能。

package com.springboot.in.action

import org.springframework.boot.SpringApplication

object LightSwordApplication extends App {
  System.setProperty("spring.devtools.restart.enabled", "false")
  SpringApplication.run(classOf[AppConfig])
}


由于spring-boot-devtools的實現原理是,在發現代碼有更改之后,重新啟動應用。它使用了兩個ClassLoader,一個Classloader加載那些不會改變的類(第三方Jar包),另一個ClassLoader加載會更改的類,稱為 Restart ClassLoader
, 這樣在有代碼更改的時候,原來的restart ClassLoader 被丟棄,重新創建一個restart ClassLoader,由于需要加載的類相比較少,所以實現了較快的重啟時間。正是這樣的實現機制,導致我們使用scala語言集成SpringBoot開發的時候,一起使用scala-maven-plugin插件跟spring-boot-devtools的時候會報錯。

由于對應的各自的語言的maven插件實現原理,比如說scala-maven-plugin:

在應用啟動的時候,執行一次如下邏輯C:

先用其編譯api scalac, 把scala代碼編譯成.class文件,然后調用ClassLoader對.class文件進行讀寫操作, 尋找classpath下面的類, 加載到jvm中。最后在jvm中執行.class字節碼。

而后續的scala代碼的變動,便沒有實時調用到插件的邏輯C,動態編譯成.class文件。所以,spring-boot-devtools的在監測動態更新ClassLoader的時候,無法監測到scala代碼的更改,也就無法實現自動重啟熱部署了。要想實現對應的scala集成SpringBoot熱部署,需要特殊定制spring-boot-devtools-scala,監測scala代碼變更,動態編譯scala代碼到類路徑。這樣spring-boot-devtools就能監測到類的動態變更了。

9.4 Spring Boot遠程調試

有時會遇到一些問題:開發環境是正常的,而線上環境是有問題,而此時就需要遠程調試來定位問題。

使用Spring Boot開發應用程序,支持遠程調試。 啟動遠程調試,按照如下配置即可:

Maven

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                    <jvmArguments>
                        -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005
                    </jvmArguments>
                </configuration>
            </plugin>

Gradle

在build.gradle的bootRun任務里添加jvmArgs屬性,如下配置

bootRun {
    jvmArgs "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"
}

然后,在IDEA中開啟遠程debug即可。在IDEA中的示例如下圖

更多關于spring-boot-devtools的功能與特性,可以參考[4]。

參考資料:

1.http://docs.spring.io/spring-boot/docs/1.5.3.RELEASE/maven-plugin/
2.http://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html
3.https://segmentfault.com/a/1190000005369936
4.http://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html
5.http://www.cnblogs.com/java-zhao/p/5502398.html
6.http://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html#using-boot-devtools-restart-exclude

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容