4

Spring Boot 应用 Docker 化

《Spring Boot 2.0极简教程》(陈光剑)
—— 基于 Gradle + Kotlin的企业级应用开发最佳实践

前面的章节中,我们都是在IDE环境中开发运行测试 Spring Boot 应用程序。在开发测试发布整个软件生命周期的过程中,我们通常需要完成打包部署发布到日常、预发、线上机器运行等运维相关工作。
本章前半部分介绍 Spring Boot 应用的打包和部署,后半部分重点介绍如何使用 Docker 来构建部署运行 Spring Boot 应用。

1.1 准备工作

首先,使用http://start.spring.io/ 创建一个打包方式为 war 的 Spring Boot Kotlin 应用,采用 Gradle 构建。点击 Generate Project 等待创建完毕,下载 zip 包,导入 IDEA 中。可以看到,相比于项目打成jar 包方式,打成 war 包的项目中多了一个用于初始化Servlet的ServletInitializer类。代码如下

class ServletInitializer : SpringBootServletInitializer() {

    override fun configure(application: SpringApplicationBuilder) : SpringApplicationBuilder {
        return application.sources(DemoPackageAndDeployApplication::class.java)
    }

}

我们知道Spring Boot 默认集成了内嵌web容器(例如 Tomcat、Jetty 等),这个时候,Spring Boot 应用支持“一键启动”,像一个普通Java程序一样,从main函数入口开始启动。现在,我们是将项目打包成war包,放到独立的web容器中。
而如果我们这个 war 包中没有配置Spring MVC 的 DispatcherServlet 的 web.xml 文件或者初始化 Servlet的类,那么这个 war 包就不会被 Tomcat识别启动 。这个时候,我们需要告诉 Tomcat 这个 war 包的启动入口。而SpringBootServletInitializer就是来完成这件事情的。
通过重写configure (SpringApplicationBuilder) 方法,使用SpringApplicationBuilder 来配置应用程序的sources类。为了测试应用运行的效果,我们在DemoPackageAndDeployApplication.kt 中添加HelloWorld REST接口方便测试

@SpringBootApplication
open class DemoPackageAndDeployApplication

fun main(args: Array<String>) {
    runApplication<DemoPackageAndDeployApplication>(*args)
}

@RestController
class HelloWorld {
    @GetMapping(value = ["", "/"])
    fun hello(): Map<String, Any> {
        val result = mutableMapOf<String, Any>()
        result["msg"] = "Hello,World"
        result["time"] = Date()
        return result
    }
}

1.2 项目打包成可执行 jar

在 IDEA 的右边的 Gradle 工具栏中列出了 Gradle 构建项目的命令,如下图

图16-1 Gradle 构建项目的命令
我们可以直接点击 bootJar 把项目打成 jar 包。当然,在运维部署脚本中通常使用命令行: gradle bootJar 。执行日志如下

17:44:21: Executing task 'bootJar'...

:compileKotlin UP-TO-DATE
:compileJava NO-SOURCE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:bootJar UP-TO-DATE

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 up-to-date
17:44:22: Task execution finished 'bootJar'.

执行完毕,我们可以在项目的build/libs 目录下看到打好的 jar 包,如下图所示

图16-2 项目的build/libs 目录下打好的 jar 包
然后,我们就可以直接使用 java –jar 命令执行该 jar 包了
$ java -jar build/libs/demo_package_and_deploy-0.0.1-SNAPSHOT.jar
此时,我们浏览器访问 http://127.0.0.1:8080/ , 可以看到输出

{
  "msg": "Hello,World",
  "time": "2018-02-09T09:38:31.933+0000"
}

不过,使用java –jar 命令行来启动系统的这种方式
java -jar build/libs/demo_package_and_deploy-0.0.1-SNAPSHOT.jar
只要控制台关闭,服务就不能访问了。我们可以使用nohup 与 & 命令让进程在后台运行:
nohup java -jar build/libs/demo_package_and_deploy-0.0.1-SNAPSHOT.jar &

1.3 定制配置文件启动应用

我们也可以在启动的时候选择读取不同的配置文件。例如,在项目src/main/resources 目录下面有不同环境下的配置文件。如下图所示:

图16-3 不同环境的属性配置文件
其中,application-dev.properties中配置服务器端口号为9000:

server.port=9000

执行 bootJar重新打jar 包,执行下面的命令:

java -jar 
build/libs/demo_package_and_deploy-0.0.1-SNAPSHOT.jar       
--spring.profiles.active=dev

可以看到应用成功启动,并监听9000端口:

…
o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 9000 (http) with context path ''
2018-02-09 18:18:47.336  INFO 69156 --- [           main] .e.s.d.DemoPackageAndDeployApplicationKt : Started DemoPackageAndDeployApplicationKt in 6.493 seconds (JVM running for 7.589)

1.4 项目打包成 war 包

在上面创建的项目中,Gradle 构建配置文件 build.gradle 内容如下:

buildscript {
    …
}
…
apply plugin: 'war'
…
configurations {
    providedRuntime
}
dependencies {
    …
    providedRuntime('org.springframework.boot:spring-boot-starter-tomcat')
}

其中,apply plugin: 'war' 是使用 war 插件来完成项目的打包工作。
直接使用 gradle bootWar,即可把项目打成 war包。然后,就可以像普通J2EE项目一样部署到web容器。同样的,war 包的路径默认也是放在 build/libs 下面。
另外,如果下面这行代码还在:

@SpringBootApplication
open class DemoPackageAndDeployApplication

fun main(args: Array<String>) {
    runApplication<DemoPackageAndDeployApplication>(*args)
}

项目打成的war包,依然支持java –jar 运行:

$ java -jar build/libs/demo_package_and_deploy-0.0.1-SNAPSHOT.war 

这个 war 包很不错,既可以直接扔到 Tomcat 容器中执行,也可以直接命令行启动运行。
提示:项目打 war包的示例项目源代码:https://github.com/EasySpring...

1.5 Spring Boot应用运维

本节简单介绍一些 Spring Boot 应用的生产运维的一些内容。

1.5.1 查看JVM参数的值

使用命令:

ps -ef|grep java 

拿到对于Java程序的pid (第2列):

  501 69156 68678   0  6:18PM ttys002    0:21.59 /usr/bin/java -jar build/libs/demo_package_and_deploy-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev

可以根据java自带的jinfo命令:

jinfo -flags 69156

来查看jar 启动后使用的是什么gc、新生代、老年代,分批的内存都是多少,示例如下:

$ jinfo -flags 69156
Attaching to process ID 69156, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.40-b25
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=715653120 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=44564480 -XX:OldSize=89653248 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC 

其中的参数简单说明如表16-1所示。

表16-1 JVM参数
参数说明
-XX:CICompilerCount
最大的并行编译数
-XX:InitialHeapSize 和 -XX:MaxHeapSize
指定JVM的初始堆内存和最大堆内存大小
-XX:MaxNewSize
JVM堆区域新生代内存的最大可分配大小
-XX:+UseParallelGC
垃圾回收使用Parallel收集器
我们可以在 Java 命令行中配置我们需要的JVM参数指标。

提示:更多关于 JVM 选项参数配置参考:http://www.oracle.com/technet...

1.5.2 应用重启

要想重启应用,要首先找到该应用的 java进程,然后kill掉 java 进程。完成这个逻辑的shell 脚本如下:

kill -9 $(ps -ef|grep java|awk '{print $2}')

然后,再使用命令行重新启动应用即可。

1.6 使用 Docker 构建部署运行Spring Boot应用

本节介绍如何使用 Docker 来构建部署 Spring Boot 应用。

1.6.1 Docker 简介

Docker 是一个Go语言开发的开源的轻量级应用容器引擎,诞生与2013年。Docker的核心概念是:镜像、容器、仓库。关键字是: 分布式应用(distributed applications), 微服务( microservices), 容器( containers ), 虚拟化(docker virtualization)。
Docker容器“轻量级”的含义主要是跟传统的虚拟机方式的对比而言。如下图所示:

图16-4 Docker “轻量级”容器VS.传统的虚拟机方式
传统的虚拟机技术是在硬件层面实现虚拟化,需要额外的虚拟机管理软件跟虚拟机操作系统这层。而 Docker 是在操作系统层面上的虚拟化,直接使用的是本地操作系统资源,因此更加轻量级。
Docker 的主要目标是通过对应用组件的封装、分发、部署、运行等生命周期的管理,做到“一次封装,到处运行”。
Docker 是实现微服务( microservices )应用程序开发的理想选择。开发、部署和回滚都将变成“一键操作”。传统的在服务器上进行各种软件包的安装、环境配置、应用程序的打包部署、启动进程等零散的运维操作——被更高层次的“抽象”,放到了一个“集装箱”中,我们只是“开箱即用”。Docker把交付运行环境比作“海运”:OS如同一个货轮,每一个在OS上运行的软件都如同一个集装箱,用户可以通过标准化手段自由组装运行环境,同时集装箱的内容可以由用户自定义,也可以由专业人员制造——这样交付一个软件,就是一系列标准化组件集的交付,如同乐高积木,用户只需要选择合适的积木组合,最后个标准化组件就是给用户的应用程序。这就是基于docker的PaaS()产品的原型。
一个完整的Docker有以下几个部分组成:

 DockerClient客户端
 Docker Daemon守护进程
 Docker Image镜像
 DockerContainer容器
 在docker的网站上介绍了使用docker的典型场景:
 Automating the packaging and deployment of applications(应用打包部署自动化)
 Creation of lightweight, private PAAS environments(创建轻量、私有的PaaS环境)
 Automated testing and continuous integration/deployment(实现自动化测试和持续的集成/部署)
 Deploying and scaling web apps, databases and backend services(部署与扩展web app、数据库和后端服务)

由于Docker 基于LXC的轻量级虚拟化的特点,相比 KVM 之类虚拟机而言,最明显的特点就是启动快,资源占用小(轻量级)——这正是构建隔离的标准化的运行环境,轻量级的PaaS,构建自动化测试和持续集成环境,以及一切可以横向扩展的应用等场景的最佳选择。
提示:更多关于 Docker 的介绍参考: https://docs.docker.com 。Dockers Github 项目空间是:https://github.com/docker

1.6.2 环境搭建

本小节介绍如何搭建 Docker 环境。
安装 Docker
去 docker 官网 https://docs.docker.com/install/ 下载对应的操作系统上的安装包。安装完毕,打开Docker运行,可以看到Mac 系统菜单栏上的显示的 Docker 应用信息如下

图16-5 Mac 系统菜单栏上的 Docker 图标
想知道 docker 提供了哪些命令行操作吗?执行docker help即可看到一个详细的命令说明。例如,在命令行查看 Docker 版本信息:

$ docker version
Client:
 Version: 17.12.0-ce
 API version: 1.35
 Go version:  go1.9.2
 Git commit:  c97c6d6
 Built: Wed Dec 27 20:03:51 2017
 OS/Arch: darwin/amd64

Server:
 Engine:
  Version:  17.12.0-ce
  API version:  1.35 (minimum version 1.12)
  Go version: go1.9.2
  Git commit: c97c6d6
  Built:  Wed Dec 27 20:12:29 2017
  OS/Arch:  linux/amd64
  Experimental: false

查看详细的 docker 信息

$ docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 1
Server Version: 17.12.0-ce
…

从仓库 pull Java 环境镜像
使用sudo docker pull java命令从 Docker 官方仓库获取 Java 运行环境镜像:

$ sudo docker pull java
Password:
Using default tag: latest
latest: Pulling from library/java
...
bb9cdec9c7f3: Pull complete 
Digest: sha256:c1ff613e8ba25833d2e1940da0940c3824f03f802c449f3d1815a66b7f8c0e9d
Status: Downloaded newer image for java:latest

下载完毕之后,可以通过docker images命令查看镜像列表:

$ docker images
REPOSITORY TAG     IMAGE ID            CREATED         SIZE
Java       latest  d23bdf5b1b1b        12 months ago   643MB

可以看到,本地镜像中已经有了 java 运行环境。

1.7 Spring Boot 项目 Docker化实战

本节介绍如何把上面的 Spring Boot 项目 Docker 容器化。过程主要分为如下3步:
1)添加 docker构建插件。
2)配置Dockerfile文件创建自定义的镜像。
3)构建Docker镜像。
下面我们就来分别详细介绍。

1.7.1 添加 docker 构建插件

在 Gradle 项目构建配置文件build.gradle 中添加com.palantir.docker插件:

buildscript {
    ext {
        kotlinVersion = '1.2.20'
        springBootVersion = '2.0.0.RC1'
    }
    repositories {
        // gradle-docker plugin repo
        maven { url "https://plugins.gradle.org/m2/" }
        ...
    }
    dependencies {
        ...
        classpath('gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.17.2')
    }
}


apply plugin: 'com.palantir.docker'

...

docker {
    name "${project.group}/${jar.baseName}"
    files jar.archivePath
    buildArgs(['JAR_FILE': "${jar.archiveName}"])
}

其中,buildArgs(['JAR_FILE': "${jar.archiveName}"]) 中配置的'JAR_FILE': "${jar.archiveName}" 是我们的 Spring Boot 项目打成 jar包的名称,会传递到Dockerfile文件中使用(下一步骤中将会看到)。
提示:关于Docker 插件com.palantir.docker的介绍参考文档: https://github.com/palantir/g...

这个插件发布在https://plugins.gradle.org/m2...,所以我们添加 maven 仓库的依赖

    repositories {
        // gradle-docker plugin repo
        maven { url "https://plugins.gradle.org/m2/" }
        ...
    }

gradle-docker提供的版本有:
https://plugins.gradle.org/m2...

1.7.2 配置 Dockerfile 文件创建自定义的镜像

Dockerfile文件放置在项目根目录:

图16-6 Dockerfile文件放置在项目根目录
Dockerfile文件内容如下:

FROM java:latest
VOLUME /tmp
ARG JAR_FILE
ADD ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

配置构建参数JAR_FILE,这里的JAR_FILE是在 build.gradle 中buildArgs中配置的

docker {
    name "${project.group}/${jar.baseName}"
    files jar.archivePath
    buildArgs(['JAR_FILE': "${jar.archiveName}"])
} 
ADD ${JAR_FILE} app.jar

将文件${JAR_FILE}拷贝到docker container的文件系统对应的路径app.jar

ENTRYPOINT ["java",
"-Djava.security.egd=
file:/dev/./urandom",
"-jar",
"/app.jar"]

Docker container启动时执行的命令。注意:一个Dockerfile中只能有一条ENTRYPOINT命令。如果多条,则只执行最后一条。

-Djava.security.egd=file:/dev/./urandom

配置 JRE 使用非阻塞的 Entropy Source。SecureRandom generateSeed 使用 /dev/random 生成种子。但是 /dev/random 是一个阻塞数字生成器,如果它没有足够的随机数据提供,它就一直等,这迫使 JVM 等待。通过在 JVM 启动参数中配置这么一行:-Djava.security.egd=file:/dev/./urandom 解决这个阻塞问题。

Dockerfile是一个文本格式的配置文件,我们可以使用Dockerfile文件快速创建自定义的镜像。Dockerfile支持的丰富的运维指令。这些指令分为4部分:

 基础镜像信息
 维护者信息
 镜像操作指令
 容器启动时的执行指令
...

1.7.5 启动 Docker 应用镜像运行

直接在命令行执行:

$ docker run -p 8080:9000 -t com.easy.springboot/demo_package_and_deploy

即可启动我们构建发布在 Docker 镜像仓库中的Spring Boot 应用镜像了。

1.7.6 端口映射

我们的 Spring Boot 应用镜像运行在 Docker容器沙箱环境中,端口号是9000,作为外部Host OS环境要访问这个服务, 需要添加TCP端口映射:把本机8080端口映射到 Docker 容器端口9000,如下图所示:

图16-7 把本机8080端口映射到 Docker 容器端口9000
其中:
 -p 是将容器的端口9000映射到 docker 所在操作系统的端口8080;
 -t 是打开一个伪终端,以便后续可以进入查看控制台 log。
使用 docker ps 命令查看运行中的容器:

$ docker ps
CONTAINER ID        IMAGE                                         COMMAND                  CREATED             STATUS              PORTS                    NAMES
36fbfaf05359        com.easy.springboot/demo_package_and_deploy   "java -Djava.securit…"   25 minutes ago      Up 25 minutes       0.0.0.0:8080->9000/tcp   infallible_kare

……
然后,执行 push 命令即可:

$ docker push com.easy.springboot/demo_package_and_deploy

提示:本节项目源代码:https://github.com/EasySpring...

1.8 本章小结

本章简单介绍了Spring Boot项目的打包、分环境运行、生产运维等操作。通常,在企业项目实践中,会实现一套 Spring Boot应用部署发布的自动化运维平台工具。本章还给出了一个完整的 Spring Boot项目 Docker 化的实战案例。
经过前面的学习,相信您已经对如何使用基于 Kotlin 编程语言的 Spring Boot项目开发有了一个比较好的掌握。


陈光剑
499 声望183 粉丝