Grails 5
已经发布有一阵时间了,作为一个小众却不失大雅的全栈框架,在 springboot
和 groovy
的加持下,一直以卓越的效率而著称,是一款名副其实的 不打断你思路
的趁手框架。
Docker
应该不用介绍了。
本文产生于一次需求(临时要码个证书管理,请无视),以 Docker
方式部署的,在部署过程中,萌生出 Grails 怎么做 Docker 最帅气
的想法,因此,有了此篇备忘。
文章比较长,建议点击 在看
、收藏
和 分享
。
第一部分 准备工作
1. 确认环境
采用当前主流的 JDK 11 + Grails 5
[aaron@micrograils.com workspaces]$ grails -version
| Grails Version: 5.0.2
| JVM Version: 11.0.13
[aaron@micrograils.com workspaces]$
2. 创建项目
仅为了演示,所以没有任何逻辑(不过这点也正说明 Grails
的骚气:创建完项目就可以运行了)
[aaron@micrograils.com workspaces]$ grails create-app com.micrograils.helloworld
Resolving dependencies......
...
| Application created at /home/aaron/workspaces/helloworld
[aaron@micrograils.com workspaces]$
3. 编译打包
[aaron@micrograils.com workspaces]$ cd helloworld/
[aaron@micrograils.com helloworld]$ grails package
...
BUILD SUCCESSFUL in 18s
9 actionable tasks: 1 executed, 8 up-to-date
| Built application to build/libs using environment: production
[aaron@micrograils.com helloworld]$ ls build/libs/
helloworld-0.1-plain.war helloworld-0.1.war
[aaron@micrograils.com helloworld]$
3. 部署验证
无。
因为咱们刚刚做的是一个“空”项目,没啥好验证的,非要验证你是不相信 spring 咋滴?
第二部分 镜像制作
因 Grails
框架自身的三个特征,
是个 java
项目;是个 springboot
项目;是个 grails
项目;
(这不是废话么?)
因此,Grails
项目的 Docker
镜像也有三类制作方式:
以 java
的方式;以 spirngboot
的方式;以 grails
的方式;
(这不又是废话么?)(为什么要用“又”?)
1. Java 方式
所谓 java
方式,其实就是指的传统、通用的 Docker
镜像制作办法:写 Dockerfile
、build
、run
,不涉及项目具体框架与结构。
1.1 编写 Dockerfile
在 build/
下,新建一个 Dockerfile.j
文件(名字有个 .j
是为了方便后面跟其他方式做区分):
(正式项目建议放在 src/main/docker
下)
FROM openjdk:11
LABEL maintainer="aaron#micrograils.com"
EXPOSE 8080
WORKDIR app
ARG WAR_FILE=libs/*.war
COPY ${WAR_FILE} application.war
ENTRYPOINT ["java", "-jar", "/app/application.war"]
FROM
不用多说了;
新版 Docker
中,MAINTAINER
已经被标注 deprecated
了,官方推荐用 LABEL maintainer=""
来代替;
其他几个也很好理解,都是常用命令。
1.2 构建镜像
(Grails 打包会生成两个文件, *-plain.war
和 *.war
,这里为了方便,先删掉了 -plain.war
。当然也可以不删掉,然后在 docker build
时,以参数的形式将 helloworld-0.1.war
传进去,参见前面的 Dockerfile.j
定义)
rm build/libs/helloworld-0.1-plain.war
在 build/
路径下执行:
sudo docker build -t micrograils/hello:0.1 -f Dockerfile.j .
[aaron@micrograils.com build]$ sudo docker build -t micrograils/hello:0.1 -f Dockerfile.j .
Sending build context to Docker daemon 92.57MB
Step 1/7 : FROM openjdk:11
...
Successfully built f413d0195c9a
Successfully tagged micrograils/hello:0.1
[aaron@micrograils.com build]$
确认一下:
[aaron@micrograils.com build]$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
micrograils/hello 0.1 f413d0195c9a 37 seconds ago 743MB
...
1.3 部署验证
sudo docker run -d -p 28080:8080 micrograils/hello:0.1
[aaron@micrograils.com build]$ sudo docker run -d -p 28080:8080 micrograils/hello:0.1
22cece7c6df7757c16cf8021fbd4c33738d005041ce7c61d09e316b894502ebd
[aaron@micrograils.com build]$
[aaron@micrograils.com build]$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
22cece7c6df7 micrograils/hello:0.1 "java -jar /app/appl…" 34 seconds ago Up 33 seconds 0.0.0.0:28080->8080/tcp, :::28080->8080/tcp jovial_bartik
...
打开浏览器,验证是否可以正常访问。

OK,通常情况基于传统方式制作的 Docker 应该没啥特别注意的,可以理解为只是包了一个壳子而已。
2. springboot 方式
一个标准的 springboot
项目有多种 docker
镜像的做法:传统的 Dockerfile
方式(类似上面)、使用 spring boot 插件
、使用 Google
的 Jib
等等,本文着重提及一下从 spring boot 2.3.*
开始支持的 分层
构建方式(Grails 5
是基于 Spring Boot 2.5
)。
补充说明:Grails 5
的其他基础组件版本为 Apache Groovy 3
,Micronaut framework 3
,Gradle 7
,Spring framework 5.3
,Spock 2.0
。
正如前面看到的,Grails 5
在打包时默认生成两个文件,其中我们使用到的 helloworld-0.1.war
,如果手动解包,则能看到相对于 helloworld-0.1-plain.war
多了 WEB-INF/lib/spring-boot-jarmode-layertools-*.jar
和 WEB-INF/layers.idx
这样两个文件,这两个文件的作用,就是用来支撑 spring boot
项目的 分层
构建。
2.1 Q:为什么要 分层
构建?
A:最直观的好处就是 性能
:我们都知道一个 Docker
镜像是由许多层组成的,镜像本质上是增量构建,后一层是在前一层基础上做封装。拉取镜像时,这些层会在主机缓存,而动则几百兆大小的容器镜像,真正属于工程代码的部分很少,因此,分层最大的好处是解耦 变
与 不变
的层(类似动静分离),进而提升未来因代码调整而带来的镜像更新影响范围(已缓存的前提下,只有变化的层才重新拉取);其他还有安全等方面因素的考量,这里不一一扩展。
我们看下怎么分层。
2.2 关于 jarmode
当前还在 build/
下,那么执行:
java -Djarmode=layertools -jar libs/helloworld-0.1.war
[aaron@micrograils.com build]$ java -Djarmode=layertools -jar libs/helloworld-0.1.war
...
Usage:
java -Djarmode=layertools -jar helloworld-0.1.war
Available commands:
list List layers from the jar that can be extracted
extract Extracts layers from the jar for image creation
help Help about any command
[aaron@micrograils.com build]$
可以看到有三个命令可用:查看(list)
、提取(extract)
、帮助(help)
。
list
一下:
java -Djarmode=layertools -jar libs/helloworld-0.1.war list
[aaron@micrograils.com build]$ java -Djarmode=layertools -jar libs/helloworld-0.1.war list
dependencies
spring-boot-loader
snapshot-dependencies
application
[aaron@micrograils.com build]$
其实这里列出的,就是前面提到的 layers.idx
所定义的内容,layers.idx
的完整内容如下:
- "dependencies":
- "WEB-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "META-INF/"
- "WEB-INF/classes/"
- "WEB-INF/layers.idx"
- "assets/"
- "cafiles/"
OK,开始以 分层
的方式构建镜像。
2.3 编写 Dockerfile
基于 分层
方式构建镜像,官方的建议是分步构建,就是将原来 一次成型
的操作,分隔成 打基础
和 真正做
两个阶段,当然,配置文件还是一个。
在当面路径下 build/
新建一个 Dockerfile.s
文件:
FROM adoptopenjdk:11-jre-hotspot as builder
WORKDIR app
ARG JAR_FILE=libs/*.war
COPY ${JAR_FILE} application.war
RUN java -Djarmode=layertools -jar application.war extract
FROM adoptopenjdk:11-jre-hotspot
LABEL maintainer="aaron@micrograils.com"
EXPOSE 8080
WORKDIR app
COPY --from=builder app/dependencies/ ./
COPY --from=builder app/spring-boot-loader/ ./
COPY --from=builder app/snapshot-dependencies/ ./
COPY --from=builder app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.WarLauncher"]
第一个 FROM
(1~6),核心内容就是将 *.war
按 layers.idx
定义的层提取( extract
)数据,第二个 FROM
内容与传统 Dockerfile
做法一致,其中的四行 COPY
恰好对应着上面的 extract
,其他无需多讲。
2.4 构建镜像
(我已经删掉了 build/libs/helloworld-0.1-plain.war
了哈):
sudo docker build -t micrograils/hello:0.2 -f Dockerfile.s .
[aaron@micrograils.com build]$ sudo docker build -t micrograils/hello:0.2 -f Dockerfile.s .
Sending build context to Docker daemon 92.57MB
Step 1/14 : FROM adoptopenjdk:11-jre-hotspot as builder
...
Successfully built 38a1a2e4988d
Successfully tagged micrograils/hello:0.2
[aaron@micrograils.com build]$
2.5 部署验证
sudo docker run -d -p 28080:8080 micrograils/hello:0.2
倒数五个数,打开浏览器,咦?咋没动静?
好吧,看看日志:
sudo docker logs e7be8defdcbd
[aaron@micrograils.com build]$ sudo docker logs e7be8defdcbd
...
***************************
APPLICATION FAILED TO START
***************************
Description:
An attempt was made to call a method that does not exist. The attempt was made from the following location:
javax.el.ELManager.getExpressionFactory(ELManager.java:61)
The following method did not exist:
'javax.el.ExpressionFactory javax.el.ELUtil.getExpressionFactory()'
The method's class, javax.el.ELUtil, is available from the following locations:
jar:file:/app/WEB-INF/lib/el-api-2.2.1-b04.jar!/javax/el/ELUtil.class
jar:file:/app/WEB-INF/lib/javax.el-3.0.1-b12.jar!/javax/el/ELUtil.class
jar:file:/app/WEB-INF/lib/javax.el-api-3.0.1-b06.jar!/javax/el/ELUtil.class
The class hierarchy was loaded from the following locations:
javax.el.ELUtil: file:/app/WEB-INF/lib/el-api-2.2.1-b04.jar
Action:
Correct the classpath of your application so that it contains a single, compatible version of javax.el.ELUtil
搜嘎,典型的依赖冲突,经排查是 org.glassfish.web
依赖了 javax.el:el-api:2.2.1-b04
, 这个是用来将 Grails
部署到 GlassFish
的,明显用不上,干掉之~,重新打包后继续。
(编辑 build.gradle
,注释掉对应的依赖)
// runtimeOnly "org.glassfish.web:el-impl:2.2.1-b05"
重新 build
,run
,熟悉的大酒瓶子又出现在我们面前~
3. Grails 方式
终于到了最后一种了...一边敲一边验证结果真挺累的……
众所周知,Grails
自打有狗的那天起,就以提倡且践行 约定大于配置
而闻名,因此,符合 grails
风格的做法,通常是 你可以不知道在哪儿有,你猜他有他就有
。但是截至到我发稿,目前还没发现 Grails
官方的 Docker
指令,不过官方网站上基于 Grails 4
有一篇教程是关于 Docker
的,本文此部分也正是参考了官方的教程,结合新版本特性进行测验并形成备忘的。
核心思路是使用了一个 com.bmuschko:gradle-docker-plugin
的插件(github
上开源且 star
还不少,文档也很清晰简明),通过提供 gradle
的 task
来实现自动编译打包和创建镜像的。此插件包含三个插件:
DockerRemoteApiPlugin DockerJavaApplicationPlugin DockerSpringBootApplicationPlugin
Grails 4
的那篇教程是基于 DockerRemoteApiPlugin
的,本来我还想着是不是基于他的 DockerSpringBootApplicationPlugin
搞事情,但是看了下源码,关于 Dockerfile
的某些内容他已经固化到代码里了,于是最终也决定直接从 DockerRemoteApiPlugin
入手。
3.1 编写 docker.gradle
在 gradle/
下新建一个 docker.gradle
文件,用来提供命令行的 gradle buildImage
指令,其内容如下(约定大于配置
的前提下,可以认为是一劳永逸的配置):
[aaron@micrograils.com helloworld]$ cat gradle/docker.gradle
import com.bmuschko.gradle.docker.tasks.image.Dockerfile
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage
buildscript {
repositories {
gradlePluginPortal()
mavenCentral()
}
dependencies {
classpath 'com.bmuschko:gradle-docker-plugin:7.1.0'
}
}
apply plugin: com.bmuschko.gradle.docker.DockerRemoteApiPlugin
ext {
dockerTag = "${project.group}/${project.name}:${project.version}".toLowerCase()
dockerBuildDir = mkdir("${buildDir}/docker")
}
task prepareDocker(type: Copy, dependsOn: assemble) {
description = 'Copy files from src/main/docker and application jar to Docker temporal build directory'
group = 'Docker'
from project.bootJar
into dockerBuildDir
}
task createDockerfile(type: Dockerfile, dependsOn: prepareDocker) {
description = 'Create a Dockerfile file'
group = 'Docker'
destFile = project.file("${dockerBuildDir}/Dockerfile")
from 'adoptopenjdk:11-jre-hotspot'
exposePort 8080
workingDir '/app'
copyFile bootJar.archiveName, 'application.jar'
entryPoint 'java', '-jar', '/app/application.jar'
}
task buildImage(type: DockerBuildImage, dependsOn: createDockerfile) {
description = 'Create Docker image to run the Grails application'
group = 'Docker'
inputDir = file(dockerBuildDir)
images.add(dockerTag)
}
之后,在 build.gradle
中,添加对此文件的引用即可:
apply from: 'gradle/docker.gradle'
3.2 构建镜像
sudo -E $GRADLE_HOME/bin/gradle buildImage
注:这里我用了 sudo
, 原因是最终提交镜像的时候,需要 root
权限。
[aaron@micrograils.com helloworld]$ sudo -E $GRADLE_HOME/bin/gradle buildImage
> Task :buildImage
Building image using context '/home/aaron/workspaces/helloworld/build/docker'.
Using images 'com.micrograils/helloworld:0.1'.
Step 1/5 : FROM adoptopenjdk:11-jre-hotspot
...
BUILD SUCCESSFUL in 28s
14 actionable tasks: 14 executed
[aaron@micrograils.com helloworld]$
崭新的镜像就做好了:
[aaron@micrograils.com helloworld]$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
com.micrograils/helloworld 0.1 6b9c8e23f6c9 18 seconds ago 327MB
3.3 部署验证
一路下来还算一帆风顺,略。
第三部分 一些补充
1. 关于 adoptopenjdk:11-jre-hotspot
项目都是同一个项目的前提下,FROM
不同的镜像,最终的文件大小差距较大:
| REPOSITORY | FROM | SIZE |
|---|---|---|
com.micrograils/helloworld:0.1 | adoptopenjdk:11-jre-hotspot | 327MB |
micrograils/hello:0.2 | adoptopenjdk:11-jre-hotspot | 333MB |
micrograils/hello:0.1 | openjdk:11 | 743MB |
而且现在主流也都是用 JDK11
了,这样这两天正火热的 log4j
风险跟你也没啥关系。
2. 镜像内部结构对比
| REPOSITORY | /app |
|---|---|
com.micrograils/helloworld:0.1 | application.jar |
micrograils/hello:0.2 | 解压后的目录(参见layers.idx) |
micrograils/hello:0.1 | application.war |
1和3其实本质是一样的。
对于迭代部署,如果客观上有快速部署、批量拉起的需求,方式2会效率高些。
第四部分 参考资料
为了保证文章质量,本文相关的参考资料如下:
[Spring Boot Gradle Plugin Reference Guide] https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/
[Spring Boot with Docker] https://spring.io/guides/gs/spring-boot-docker/
[Creating Optimized Docker Images for a Spring Boot Application] https://reflectoring.io/spring-boot-docker/
[Grails as a Docker Container] https://guides.grails.org/grails-as-docker-container/guide/index.html
[Gradle Docker Plugin] https://bmuschko.github.io/gradle-docker-plugin/current/user-guide/
---- [我是分割线] ----
Q:怎样能第一时间收到推送?
A:点击文章右上角的 ...
找到公众号页面,继续点击右上角的 ...
后选择 设为星标
,这样就不会漏掉推送了。
转载请发邮件:aaron#micrograils.com




