适用于Java开发人员的Docker:在Docker上构建

本文是我们学院课程中名为《 面向Java开发人员的Docker教程 》的一部分。

在本课程中,我们提供了一系列教程,以便您可以开发自己的基于Docker的应用程序。 我们涵盖了广泛的主题,从通过命令行的Docker到开发,测试,部署和持续集成。 通过我们简单易懂的教程,您将能够在最短的时间内启动并运行自己的项目。 在这里查看

1.简介

在本教程的前几部分中,我们介绍了Docker的基础知识以及与之交互的多种方法。 现在是时候将我们获得的知识应用于实际的Java项目,从Docker如何影响完善的构建过程和实践这一主题开始讨论。

公平地讲,本部分的目标是双重的。 首先,我们将看看现有的构建工具(即Apache MavenGradle )如何将Java应用程序打包为Docker容器。 其次,我们将进一步推动这一想法,并学习如何使用Docker来完全封装Java应用程序的构建管道,并最终生成最终的Docker映像。

2.在放大镜下

为了进行试验,我们将设计两个简单的Java Web应用程序,这些Web应用程序将实现并公开用于任务管理的REST(ful)API

第一个应用程序将使用Gradle作为构建和依赖项管理工具在Spring BootSpring Webflux之上开发。 在版本方面,我们将使用Spring Boot最新里程碑2.0.0.M6Spring Webflux最新版本5.0.1Gradle最新版本4.3

第二个应用程序在功能上等同于第一个应用程序,将在另一个流行的Java框架Dropwizard的基础上开发 ,这次使用Apache Maven进行构建和依赖项管理。 在版本方面,我们将带来Dropwizard最新版本1.2.0Apache Maven最新版本3.5.2

如前所述,这两个应用程序都将实现并公开用于任务管理的REST(ful)API ,从本质上包装了CRUD (创建,读取,更新和删除)操作。

GET     /tasks
    POST    /tasks
    DELETE  /tasks/{id}
    GET     /tasks/{id}

任务本身被建模为将由Hibernate ORM管理并存储在MySQL关系数据库中的持久实体

@Entity 
@Table(name = "tasks")
public class Task {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;
    
    @Column(name = "title", nullable = false, length = 255)
    private String title;
    
    @Column(name = "description", nullable = true, columnDefinition = "TEXT")
    private String description;
    
    // Getters and setters are omitted
    ...
}

至此,两个应用程序之间的相似性结束了,并且每个应用程序都将遵循其自己惯用的开发方式。

3. Gradle和Docker

因此,我们已经做好了准备,让我们开始探索将Docker集成到典型Gradle构建中所需要的过程。 对于本小节,您将需要在开发计算机上安装Gradle 4.3 。 如果尚未安装 ,请按照安装说明进行操作 ,选择所需的任何建议方法。

为了使用Gradle将典型的Spring Boot应用程序打包为Docker映像,我们只需要在build.gradle文件中包括两个其他插件build.gradle

构建管道基本上将依赖于Spring Boot Gradle插件来生成uber-jar (该术语通常用于描述生成单个可运行的应用程序JAR存档的技术),稍后将被Palantir Docker Gradle用来组装Docker映像。 这是构建定义build.gradle文件的外观。

buildscript {
    repositories {
        maven { url 'https://repo.spring.io/libs-milestone' }
    }
  
    dependencies {
        classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.M6"
    }
}

plugins {
    id 'com.palantir.docker' version '0.13.0'
}

apply plugin: "org.springframework.boot"
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'application'

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.flywaydb:flyway-core:4.2.0")
    compile("org.springframework.boot:spring-boot-starter-webflux:2.0.0.M6")
    compile("org.springframework.boot:spring-boot-starter-data-jpa:2.0.0.M6")
    compile("org.springframework.boot:spring-boot-starter-actuator:2.0.0.M6")
    compile("mysql:mysql-connector-java:8.0.7-dmr")
}

repositories {
    maven {
        mavenCentral()
        url 'https://repo.spring.io/libs-milestone'
    }
}

springBoot {
    mainClassName = "com.javacodegeeks.spring.AppStarter"
}

jar {
    mainClassName = "com.javacodegeeks.spring.AppStarter"
    baseName = 'spring-boot-webapp '
    version = project.version
}

bootJar {
    baseName = 'spring-boot-webapp '
    version = project.version
}

docker {
    name "jcg/spring-boot-webapp:$project.version"
    tags 'latest'
    dependsOn build
    files bootJar
    dockerfile file('src/main/docker/Dockerfile')
    buildArgs([BUILD_VERSION: project.version])
}

这实际上是非常简单的,所有的肉基本上是内docker中的部分build.gradle文件。 您可能还会注意到,我们正在使用自己的Dockerfile src/main/docker/DockerfileDocker提供有关如何构建映像的说明。

FROM openjdk:8-jdk-alpine
ARG BUILD_VERSION

ENV DB_HOST localhost
ENV DB_PORT 3306

ADD spring-boot-webapp-${BUILD_VERSION}.jar spring-boot-webapp.jar

EXPOSE 19900

ENTRYPOINT exec java $JAVA_OPTS -Ddb.host=$DB_HOST -Ddb.port=$DB_PORT -jar /spring-boot-webapp.jar

确实,它尽可能地简单。 请注意,我们如何使用ARG指令(以及build.gradle文件中的buildArgs设置)将参数传递给图像。 在这种情况下,我们将传递项目的版本,以便找到最终的构建工件。 另一个有趣的细节是使用ENV指令来连接要连接的MySQL实例主机和端口。 而且,您可能已经猜到了, EXPOSE指令通知Docker容器在运行时在端口19900上进行侦听。

太好了,接下来该怎么办? 好吧,我们只需要触发我们的Gradle构建,就像这样:

> gradle clean docker dockerTag
...
BUILD SUCCESSFUL in 12s
15 actionable tasks: 14 executed, 1 up-to-date

dockerTag任务并不是真正必要的,但是由于针对Palantir Docker Gradle插件报告了此问题,我们应该显式调用它以对我们的图像进行正确标记。 让我们检查是否在本地可以使用我们的图像。

> docker image ls
REPOSITORY               TAG            IMAGE ID      CREATED             SIZE
jcg/spring-boot-webapp   0.0.1-SNAPSHOT 65057c7ae9ba  21 seconds ago      133MB
jcg/spring-boot-webapp   latest         65057c7ae9ba  21 seconds ago      133MB
...

好的,新图像就在烤箱中。 我们可以使用docker命令行工具立即运行它,但是首先我们需要在某个地方使用MySQL容器。 幸运的是,我们已经做了很多次,以至于不会迷惑我们。

docker run --rm -d \
  --name mysql \
  -e MYSQL_ROOT_PASSWORD='p$ssw0rd' \
  -e MYSQL_DATABASE=my_app_db \
  -e MYSQL_ROOT_HOST=% \
  mysql:8.0.2

现在我们准备将应用程序作为Docker容器运行。 我们可以使用多种方式来引用MySQL容器,其中用户定义网络是首选。 对于像我们这样的简单情况,我们可以通过将正在运行的MySQL容器的IP地址分配给DB_HOST环境变量来引用它,例如:

docker run -d --rm \
  --name spring-boot-webapp \
  -p 19900:19900 \
  -e DB_HOST=`docker inspect --format '{{ .NetworkSettings.IPAddress }}' mysql` \
  jcg/spring-boot-webapp:0.0.1-SNAPSHOT

通过将端口19900从容器映射到主机,我们可以通过使用localhost作为主机名从curl访问其REST(ful)API来与我们的应用程序对话。 让我们立即这样做。

$ curl -X POST http://localhost:19900/tasks \
   -d '[{"title": "Task #1", "description": "Sample Task"}]' \
   -H "Content-Type: application/json"

[
  {
    "id":1,
    "title":"Task #1",
    "description":"Sample Task"
  }
]
$ curl http://localhost:19900/tasks

[
  {
    "id":1,
    "title":"Task #1",
    "description":"Sample Task"
  }
]
$ curl http://localhost:19900/tasks/1

{
  "id":1,
  "title":"Task #1",
  "description":"Sample Task"
}

引擎盖下有许多活动部件,例如,使用Flyway进行自动数据库迁移以及使用Spring Boot Actuator进行开箱即用的运行状况检查支持。 其中一些将在本教程的后续部分中弹出,但看起来使用GradleSpring Boot应用程序构建并打包为Docker映像是多么简单自然。

4. Docker上的Gradle

事实证明,使用Gradle构建Docker映像一点也不痛苦。 但是,仍然需要在目标系统上安装Gradle以及JDK / JRE的先决条件需要做一些准备工作。 对于开发来说,这可能不是一个问题,因为无论如何,您很有可能会安装所有这些(以及更多)。

对于云部署或CI / CD管道,这可能是一个问题,会导致工作或维护方面的额外成本。 我们能以某种方式找到摆脱这种开销并完全依赖Docker的方法吗? 是的,事实上,我们可以通过采用多阶段构建 ,来对Docker功能集进行最新添加。

如果您想知道它可能对我们有什么帮助,这里是个主意。 本质上,我们将按照常规过程从Dockerfile构建映像。 但是Dockerfile实际上将包含两个映像定义。 第一个(基于官方Gradle映像之一 )指示Docker运行我们的Spring Boot应用程序的Gradle构建。 第二个将选择第一个映像生成的二进制文件,并使用密封在内部的Spring Boot应用程序创建最终的Docker映像(就像我们之前所做的那样)。

一次查看它可能比尝试解释它更好。 下面的Dockerfile.build文件使用多阶段构建指令说明了这一想法。

FROM gradle:4.3.0-jdk8-alpine

ADD src src
ADD build.gradle .
ADD gradle.properties .

RUN gradle build

FROM openjdk:8-jdk-alpine
ARG BUILD_VERSION

ENV DB_HOST localhost
ENV DB_PORT 3306

COPY --from=0 /home/gradle/build/libs/spring-boot-webapp-${BUILD_VERSION}.jar spring-boot-webapp.jar

EXPOSE 19900

ENTRYPOINT exec java $JAVA_OPTS -Ddb.host=$DB_HOST -Ddb.port=$DB_PORT -jar /spring-boot-webapp.jar

Dockerfile定义的第一部分描述了基于gradle:4.3.0-jdk8-alpine 。 因为我们的项目很小,所以我们只复制映像中的所有源代码并运行gradle build命令(该命令将在gradle build映像时由Docker执行)。 构建的结果将是uber-jar ,我们将其输入到另一个图像定义中,这一次基于openjdk:8-jdk-alpine 。 这将构成我们的最终图像,我们可以使用docker命令行工具生成该图像。

docker image build \
  --build-arg BUILD_VERSION=0.0.1-SNAPSHOT \
  -f Dockerfile.build \
  -t jcg/spring-boot-webapp:latest \
  -t jcg/spring-boot-webapp:0.0.1-SNAPSHOT .

在进行命令竞争后,我们应该在可用的Docker映像列表中看到我们新烘焙的映像。

$ docker image ls
REPOSITORY               TAG            IMAGE ID       CREATED           SIZE
jcg/spring-boot-webapp   0.0.1-SNAPSHOT  02abf724da64  10 seconds ago    133MB
jcg/spring-boot-webapp   latest          02abf724da64  10 seconds ago    133MB
...

多阶段构建背后蕴藏着巨大的潜力,但即使对于像我们这样的简单应用,它们也值得关注。

5. Maven和Docker

让我们稍作调整 ,看看Apache Maven如何使用Dropwizard应用程序的构建管理。 对于本小节,您需要在开发机器上安装Apache Maven 3.2.5 (但是,如果您已经拥有Apache Maven 3.2.1或更高版本,则可以坚持使用它)。

我们必须遵循的步骤与我们在Gradle构建中讨论的步骤基本相同,更改实际上仅在要使用的插件中进行:

Maven Shade插件会生成一个uber-jar ,稍后将被Spotify Docker Maven插件用于构建Docker映像。 不用大惊小怪,让我们看一下pom.xml文件。

<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>

  <groupId>com.javacodegeeks</groupId>
  <artifactId>dropwizard-webapp</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.dropwizard</groupId>
        <artifactId>dropwizard-bom</artifactId>
        <version>1.2.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>io.dropwizard</groupId>
      <artifactId>dropwizard-core</artifactId>
    </dependency>

    <dependency>
      <groupId>io.dropwizard</groupId>
      <artifactId>dropwizard-hibernate</artifactId>
    </dependency>
        
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.7-dmr</version>
    </dependency>

    <dependency>
      <groupId>io.dropwizard.modules</groupId>
      <artifactId>dropwizard-flyway</artifactId>
      <version>1.2.0-1</version>
    </dependency>
        
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
    </dependency>
    
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.0.2</version>
        <configuration>
          <archive>
            <manifest>
              <mainClass>com.javacodegeeks.docker.AllApiApp</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.1.0</version>
        <configuration>
          <filters>
            <filter>
              <artifact>*:*</artifact>
              <excludes>
                <exclude>META-INF/*.SF</exclude>
                <exclude>META-INF/*.DSA</exclude>
                <exclude>META-INF/*.RSA</exclude>
              </excludes>
            </filter>
          </filters>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer 
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">                                  
                  <mainClass>com.javacodegeeks.dw.AppStarter</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>com.spotify</groupId>
        <artifactId>docker-maven-plugin</artifactId>
        <version>1.0.0</version>
        <configuration>
          <imageName>jcg/dropwizard-webapp:${project.version}</imageName>
          <dockerDirectory>src/main/docker</dockerDirectory>
          <resources>
            <resource>
              <targetPath>/</targetPath>
              <directory>${project.build.directory}</directory>
              <include>${project.build.finalName}.jar</include>
            </resource>
            <resource>
              <targetPath>/</targetPath>
              <directory>${project.basedir}</directory>
              <include>application.yml</include>
            </resource>
          </resources>
          <buildArgs>
            <BUILD_VERSION>${project.version}</BUILD_VERSION>
          </buildArgs>
          <imageTags>
            <tag>latest</tag>
          </imageTags>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

公平地说,它看起来比Gradle构建更冗长,但是如果我们想一想所有XML标签都消失了,那么最终我们将得到几乎相同的定义,至少在Docker插件的情况下。 Dockerfile有点不同:

FROM openjdk:8-jdk-alpine
ARG BUILD_VERSION

ENV DB_HOST localhost
ENV DB_PORT 3306

ADD dropwizard-webapp-${BUILD_VERSION}.jar dropwizard-webapp.jar
ADD application.yml application.yml 
ADD docker-entrypoint.sh docker-entrypoint.sh
RUN chmod a+x /docker-entrypoint.sh

EXPOSE 19900 19901

ENTRYPOINT ["/docker-entrypoint.sh"]

由于Dropwizard应用程序的特殊性 ,我们必须将配置文件(在本例中为application.yml )与application捆绑在一起。 我们不必公开一个端口19900 ,而必须公开另一个端口19901来执行管理任务。 最后但并非最不重要的一点是,我们将脚本提供给ENTRYPOINT指令docker-entrypoint.sh

#!/bin/sh

set -e
java $JAVA_OPTS -DDB_HOST=$DB_HOST -DDB_PORT=$DB_PORT -jar /dropwizard-webapp.jar db migrate application.yml

if [ ! $? -ne 0 ]; then
  exec java $JAVA_OPTS -DDB_HOST=$DB_HOST -DDB_PORT=$DB_PORT -jar /dropwizard-webapp.jar server application.yml	
fi

exec "$@"

这里增加一些复杂性的原因是因为默认情况下, Dropwizard Flyway附加软件包不执行自动数据库模式迁移。 我们可以解决该问题,但最干净的方法是在启动Dropwizard应用程序之前运行db migrate migration命令。 这正是我们在上面的shell脚本中执行的操作。 现在,该触发构建了!

>  mvn clean package docker:build

...

Successfully tagged jcg/dropwizard-webapp:0.0.1-SNAPSHOT
[INFO] Built jcg/dropwizard-webapp:0.0.1-SNAPSHOT
[INFO] Tagging jcg/dropwizard-webapp:0.0.1-SNAPSHOT with latest
[INFO] ---------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ---------------------------------------------------------

...

让我们看看我们这次是否可以在本地使用我们的图像。

> docker image ls
REPOSITORY               TAG            IMAGE ID      CREATED             SIZE
jcg/dropwizard-webapp    0.0.1-SNAPSHOT fa9c310683b1  20 seconds ago      128MB
jcg/dropwizard-webapp    latest         fa9c310683b1  20 seconds ago      128MB
...

优秀的,假设MySQL的容器启动并运行(这部分不发生任何变化,我们可以从使用相同的命令一节 ),我们可以只运行我们Dropwizard应用程序容器。

docker run -d --rm \
  --name dropwizard-webapp \
  -p 19900:19900 \
  -p 19901:19901 \
  -e DB_HOST=`docker inspect --format '{{ .NetworkSettings.IPAddress }}' mysql` \
  jcg/dropwizard-webapp:0.0.1-SNAPSHOT

我们还将端口1990019901从容器映射到主机,以便可以在curl中使用localhost作为主机名。

$ curl -X POST http://localhost:19900/tasks \
   -d '[{"title": "Task #1", "description": "Sample Task"}]' \
   -H "Content-Type: application/json"

[
  {
    "id":1,
    "title":"Task #1",
    "description":"Sample Task"
  }
]
$ curl http://localhost:19900/tasks

[
  {
    "id":1,
    "title":"Task #1",
    "description":"Sample Task"
  }
]
$ curl http://localhost:19900/tasks/1

{
  "id":1,
  "title":"Task #1",
  "description":"Sample Task"
}

请注意,通过主机端口映射,我们可以运行jcg/dropwizard-webapp:0.0.1-SNAPSHOT容器或jcg/spring-boot-webapp:0.0.1-SNAPSHOT容器,但不能同时运行这两个容器不可避免的端口冲突。 为了方便起见,我们仅使用同一端口,但是在大多数情况下,您将使用动态端口绑定,并且不会看到此问题的发生。

6. Docker上的Maven

使用多阶段构建的相同技术同样适用于使用Apache Maven进行构建和依赖项管理的项目(幸运的是,在Docker Hub上发布了官方的Apache Maven映像 )。

FROM maven:3.5.2-jdk-8-alpine

ADD src src
ADD pom.xml .

RUN mvn package

FROM openjdk:8-jdk-alpine
ARG BUILD_VERSION

ENV DB_HOST localhost
ENV DB_PORT 3306

COPY --from=0 /target/dropwizard-webapp-${BUILD_VERSION}.jar dropwizard-webapp.jar
ADD application.yml application.yml 
ADD src/main/docker/docker-entrypoint.sh docker-entrypoint.sh
RUN chmod a+x /docker-entrypoint.sh

EXPOSE 19900 19901

ENTRYPOINT ["/docker-entrypoint.sh"]

一旦我们破解了多阶段构建的工作原理后,在这里添加的内容就不多了 ,因此让我们使用docker命令行工具构建最终映像。

docker image build \
  --build-arg BUILD_VERSION=0.0.1-SNAPSHOT \
  -f Dockerfile.build \
  -t jcg/dropwizard-webapp:latest \
  -t jcg/dropwizard-webapp:0.0.1-SNAPSHOT .

并确保该图像出现在可用Docker图像的列表中。

> docker image ls
REPOSITORY             TAG             IMAGE ID       CREATED          SIZE
jcg/dropwizard-webapp  0.0.1-SNAPSHOT  5b006fcc9a1d   26 seconds ago   128MB
jcg/dropwizard-webapp  latest          5b006fcc9a1d   26 seconds ago   128MB
...

老实说,这真是太棒了。 在结束有关多阶段构建的讨论之前,让我们谈谈您肯定会碰到的用例:从源代码控制系统中签出项目。 到目前为止,我们已经看到的示例假定该项目在本地可用,但我们也可以将其从远程存储库中克隆出来,作为多阶段构建定义的一部分。

7.结论

在本教程的这一部分中,我们看到了一些示例,说明了流行的构建和依赖项管理工具Apache MavenGradle如何支持将Java应用程序打包为Docker映像。 我们还花了一些时间讨论多阶段构建以及它们为实现可移植的,零依赖(字面意义上的)构建管道所带来的机遇。

8.接下来

在本教程的下一部分中,我们将研究Docker如何简化开发流程和实践,尤其是在处理数据存储和外部(甚至内部)服务方面。

完整的项目资源可供下载

翻译自: https://www.javacodegeeks.com/2017/11/docker-java-developers-build-docker.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值