第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阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容