第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这里打勾

《第9章 Spring Boot开发者工具》

然后 Shift+Ctrl+Alt+/(Mac: Shift+Command+Alt+/),选择Registry

《第9章 Spring Boot开发者工具》

进去之后,找到如下图所示的选项,打勾

《第9章 Spring Boot开发者工具》

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中的示例如下图

《第9章 Spring Boot开发者工具》

更多关于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

    原文作者:一个会写诗的程序员
    原文地址: https://www.jianshu.com/p/127dd30cf75f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞