第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