Java11 秘籍(六)
原文:
zh.annas-archive.org/md5/2bf50d1e2a61626a8f3de4e5aae60b76译者:飞龙
第八章:更好地管理操作系统进程
在本章中,我们将介绍以下内容:
-
生成一个新进程
-
将进程输出和错误流重定向到文件
-
更改子进程的工作目录
-
为子进程设置环境变量
-
运行 shell 脚本
-
获取当前 JVM 的进程信息
-
获取生成的进程的进程信息
-
管理生成的进程
-
枚举系统中正在运行的进程
-
使用管道连接多个进程
-
管理子进程
介绍
你有多少次编写了生成新进程的代码?不多。然而,可能有一些情况需要编写这样的代码。在这种情况下,您不得不使用第三方 API,如Apache Commons Exec(commons.apache.org/proper/commons-exec/)等。为什么会这样?Java API 不够用吗?不,不够用;至少在 Java 9 之前是这样。现在,有了 Java 9 及以上版本,我们在进程 API 中添加了更多功能。
直到 Java 7,重定向输入、输出和错误流并不是一件简单的事。在 Java 7 中,引入了新的 API,允许将输入、输出和错误重定向到其他进程(管道)、文件或标准输入/输出。然后,在 Java 8 中,又引入了一些新的 API。在 Java 9 中,现在有了以下领域的新 API:
-
获取进程信息,如进程 ID(PID)、启动进程的用户、运行时间等
-
枚举系统中正在运行的进程
-
通过导航到进程层次结构的上层来管理子进程并访问进程树
在本章中,我们将介绍一些配方,这些配方将帮助您探索进程 API 中的新内容,并了解自Runtime.getRuntime().exec()以来引入的更改。而且你们都知道使用那个是犯罪。
所有这些配方只能在 Linux 平台上执行,因为我们将在 Java 代码中使用特定于 Linux 的命令来生成新进程。在 Linux 上执行脚本run.sh有两种方法:
-
sh run.sh -
chmod +x run.sh && ./run.sh
那些使用 Windows 10 的人不用担心,因为微软发布了 Windows 子系统用于 Linux,它允许您在 Windows 上运行您喜欢的 Linux 发行版,如 Ubuntu、OpenSuse 等。有关更多详细信息,请查看此链接:docs.microsoft.com/en-in/windows/wsl/install-win10。
生成新进程
在这个配方中,我们将看到如何使用ProcessBuilder生成新进程。我们还将看到如何使用输入、输出和错误流。这应该是一个非常简单和常见的配方。然而,引入这个的目的是为了使本章内容更加完整,而不仅仅是关注 Java 9 的特性。
准备工作
Linux 中有一个名为free的命令,它显示系统中空闲的 RAM 量以及被系统使用的量。它接受一个选项-m,以便以兆字节显示输出。因此,只需运行 free -m即可得到以下输出:
我们将在 Java 程序中运行上述代码。
如何做...
按照以下步骤进行:
- 通过提供所需的命令和选项来创建
ProcessBuilder的实例:
ProcessBuilder pBuilder = new ProcessBuilder("free", "-m");
指定命令和选项的另一种方法如下:
pBuilder.command("free", "-m");
- 为进程生成器设置输入和输出流以及其他属性,如执行目录和环境变量。然后,在
ProcessBuilder实例上调用start()来生成进程并获取对Process对象的引用:
Process p = pBuilder.inheritIO().start();
inheritIO()函数将生成的子进程的标准 I/O 设置为与当前 Java 进程相同。
- 然后,我们等待进程的完成,或者等待一秒钟(以先到者为准),如下面的代码所示:
if(p.waitFor(1, TimeUnit.SECONDS)){
System.out.println("process completed successfully");
}else{
System.out.println("waiting time elapsed, process did
not complete");
System.out.println("destroying process forcibly");
p.destroyForcibly();
}
如果在指定的时间内没有完成,我们可以通过调用destroyForcibly()方法来终止进程。
- 使用以下命令编译和运行代码:
$ javac -d mods --module-source-path src
$(find src -name *.java)
$ java -p mods -m process/com.packt.process.NewProcessDemo
- 我们得到的输出如下:
此示例的代码可以在Chapter08/1_spawn_new_process中找到。
工作原理...
有两种方法可以让ProcessBuilder知道要运行哪个命令:
-
通过在创建
ProcessBuilder对象时将命令及其选项传递给构造函数 -
通过将命令及其选项作为参数传递给
ProcessBuilder对象的command()方法
在生成进程之前,我们可以执行以下操作:
-
我们可以使用
directory()方法更改执行目录。 -
我们可以将输入流、输出流和错误流重定向到文件或另一个进程。
-
我们可以为子进程提供所需的环境变量。
我们将在本章的各自示例中看到所有这些活动。
当调用start()方法时,将生成一个新的进程,并且调用者以Process类的实例形式获得对该子进程的引用。使用这个Process对象,我们可以做很多事情,比如以下事情:
-
获取有关进程的信息,包括其 PID
-
获取输出和错误流
-
检查进程的完成情况
-
销毁进程
-
将任务与进程完成后要执行的操作关联起来
-
检查进程生成的子进程
-
查找进程的父进程(如果存在)
在我们的示例中,我们等待一秒钟,或者等待进程完成(以先到者为准)。如果进程已完成,则waitFor返回true;否则返回false。如果进程没有完成,我们可以通过在Process对象上调用destroyForcibly()方法来终止进程。
将进程输出和错误流重定向到文件
在本示例中,我们将看到如何处理从 Java 代码生成的进程的输出和错误流。我们将把生成的进程产生的输出或错误写入文件。
准备工作
在本示例中,我们将使用iostat命令。此命令用于报告不同设备和分区的 CPU 和 I/O 统计信息。让我们运行该命令并查看它报告了什么:
$ iostat
在某些 Linux 发行版(如 Ubuntu)中,默认情况下未安装iostat。您可以通过运行sudo apt-get install sysstat来安装该实用程序。
上述命令的输出如下:
如何做...
按照以下步骤进行:
- 通过指定要执行的命令来创建一个新的
ProcessBuilder对象:
ProcessBuilder pb = new ProcessBuilder("iostat");
- 将输出和错误流重定向到文件的输出和错误流中:
pb.redirectError(new File("error"))
.redirectOutput(new File("output"));
- 启动进程并等待其完成:
Process p = pb.start();
int exitValue = p.waitFor();
- 读取输出文件的内容:
Files.lines(Paths.get("output"))
.forEach(l -> System.out.println(l));
- 读取错误文件的内容。只有在命令出现错误时才会创建此文件:
Files.lines(Paths.get("error"))
.forEach(l -> System.out.println(l));
步骤 4 和 5 是供我们参考的。这与ProcessBuilder或生成的进程无关。使用这两行代码,我们可以检查进程写入输出和错误文件的内容。
完整的代码可以在Chapter08/2_redirect_to_file中找到。
- 使用以下命令编译代码:
$ javac -d mods --module-source-path src $(find src -name
*.java)
- 使用以下命令运行代码:
$ java -p mods -m process/com.packt.process.RedirectFileDemo
我们将得到以下输出:
我们可以看到,由于命令成功执行,错误文件中没有任何内容。
还有更多...
您可以向ProcessBuilder提供错误的命令,然后看到错误被写入错误文件,输出文件中没有任何内容。您可以通过更改ProcessBuilder实例创建来实现这一点,如下所示:
ProcessBuilder pb = new ProcessBuilder("iostat", "-Z");
使用前面在*如何做...*部分中给出的命令进行编译和运行。
您会看到错误文件中报告了一个错误,但输出文件中没有任何内容:
更改子进程的工作目录
通常,您会希望在路径的上下文中执行一个进程,比如列出目录中的文件。为了做到这一点,我们将不得不告诉 ProcessBuilder 在给定位置的上下文中启动进程。我们可以通过使用 directory() 方法来实现这一点。这个方法有两个目的:
-
当我们不传递任何参数时,它返回执行的当前目录。
-
当我们传递参数时,它将执行的当前目录设置为传递的值。
在这个示例中,我们将看到如何执行
tree 命令用于递归遍历当前目录中的所有目录,并以树形式打印出来。
准备工作
通常,tree 命令不是预装的,因此您将不得不安装包含该命令的软件包。要在 Ubuntu/Debian 系统上安装,请运行以下命令:
$ sudo apt-get install tree
要在支持 yum 软件包管理器的 Linux 上安装,请运行以下命令:
$ yum install tree
要验证您的安装,只需运行 tree 命令,您应该能够看到当前目录结构的打印。对我来说,它是这样的:
tree 命令支持多个选项。这是供您探索的。
如何做...
按照以下步骤进行:
- 创建一个新的
ProcessBuilder对象:
ProcessBuilder pb = new ProcessBuilder();
- 将命令设置为
tree,并将输出和错误设置为与当前 Java 进程相同的输出和错误:
pb.command("tree").inheritIO();
- 将目录设置为您想要的任何目录。我将其设置为根文件夹:
pb.directory(new File("/root"));
- 启动进程并等待其退出:
Process p = pb.start();
int exitValue = p.waitFor();
- 使用以下命令进行编译和运行:
$ javac -d mods --module-source-path src $(find src -name *.java)
$ java -p mods -m process/com.packt.process.ChangeWorkDirectoryDemo
- 输出将是指定在
ProcessBuilder对象的directory()方法中的目录的递归内容,以树状格式打印出来。
完整的代码可以在 Chapter08/3_change_work_directory 找到。
它是如何工作的...
directory() 方法接受 Process 的工作目录的路径。路径被指定为 File 的实例。
为子进程设置环境变量
环境变量就像我们在编程语言中拥有的任何其他变量一样。它们有一个名称并保存一些值,这些值可以变化。这些被 Linux/Windows 命令或 shell/batch 脚本用来执行不同的操作。它们被称为环境变量,因为它们存在于正在执行的进程/命令/脚本的环境中。通常,进程从父进程继承环境变量。
它们在不同的操作系统中以不同的方式访问。在 Windows 中,它们被访问为 %ENVIRONMENT_VARIABLE_NAME%,在基于 Unix 的操作系统中,它们被访问为 $ENVIRONMENT_VARIABLE_NAME。
在基于 Unix 的系统中,您可以使用 printenv 命令打印出进程可用的所有环境变量,在基于 Windows 的系统中,您可以使用 SET 命令。
在这个示例中,我们将向子进程传递一些环境变量,并使用 printenv 命令打印所有可用的环境变量。
如何做...
按照以下步骤进行:
- 创建一个
ProcessBuilder的实例:
ProcessBuilder pb = new ProcessBuilder();
- 将命令设置为
printenv,并将输出和错误流设置为与当前 Java 进程相同的输出和错误:
pb.command("printenv").inheritIO();
- 提供环境变量
COOKBOOK_VAR1的值为First variable,COOKBOOK_VAR2的值为Second variable,以及COOKBOOK_VAR3的值为Third variable:
Map<String, String> environment = pb.environment();
environment.put("COOKBOOK_VAR1", "First variable");
environment.put("COOKBOOK_VAR2", "Second variable");
environment.put("COOKBOOK_VAR3", "Third variable");
- 启动进程并等待其完成:
Process p = pb.start();
int exitValue = p.waitFor();
这个示例的完整代码可以在 Chapter08/4_environment_variables 找到。
- 使用以下命令编译和运行代码:
$ javac -d mods --module-source-path src $(find src -name
*.java)
$ java -p mods -m
process/com.packt.process.EnvironmentVariableDemo
您得到的输出如下:
您可以看到三个变量打印在其他变量中。
它是如何工作的...
当您在ProcessBuilder的实例上调用environment()方法时,它会复制当前进程的环境变量,将它们填充到HashMap的一个实例中,并将其返回给调用者代码。
加载环境变量的所有工作都是由一个包私有的最终类ProcessEnvironment完成的,它实际上扩展了HashMap。
然后我们利用这个映射来填充我们自己的环境变量,但我们不需要将映射设置回ProcessBuilder,因为我们将有一个对映射对象的引用,而不是一个副本。对映射对象所做的任何更改都将反映在ProcessBuilder实例持有的实际映射对象中。
运行 shell 脚本
我们通常会收集在文件中执行操作的一组命令,称为 Unix 世界中的shell 脚本和 Windows 中的批处理文件。这些文件中的命令按顺序执行,除非脚本中有条件块或循环。
这些 shell 脚本由它们执行的 shell 进行评估。可用的不同类型的 shell 包括bash、csh、ksh等。bash shell 是最常用的 shell。
在这个示例中,我们将编写一个简单的 shell 脚本,然后使用ProcessBuilder和Process对象从 Java 代码中调用它。
准备工作
首先,让我们编写我们的 shell 脚本。这个脚本做了以下几件事:
-
打印环境变量
MY_VARIABLE的值 -
执行
tree命令 -
执行
iostat命令
让我们创建一个名为script.sh的 shell 脚本文件,其中包含以下命令:
echo $MY_VARIABLE;
echo "Running tree command";
tree;
echo "Running iostat command"
iostat;
您可以将script.sh放在您的主文件夹中;也就是说,在/home/<username>中。现在让我们看看我们如何从 Java 中执行它。
如何做...
按照以下步骤进行:
- 创建
ProcessBuilder的一个新实例:
ProcessBuilder pb = new ProcessBuilder();
- 将执行目录设置为指向 shell 脚本文件的目录:
pb.directory(new File("/root"));
请注意,在创建File对象时传递的先前路径将取决于您放置脚本script.sh的位置。在我们的情况下,我们将它放在/root中。您可能已经将脚本复制到了/home/yourname中,因此File对象将相应地创建为newFile("/home/yourname")。
- 设置一个将被 shell 脚本使用的环境变量:
Map<String, String> environment = pb.environment();
environment.put("MY_VARIABLE", "Set by Java process");
- 设置要执行的命令,以及要传递给命令的参数。还要将进程的输出和错误流设置为与当前 Java 进程相同的流:
pb.command("/bin/bash", "script.sh").inheritIO();
- 启动进程并等待它完全执行:
Process p = pb.start();
int exitValue = p.waitFor();
您可以从Chapter08/5_running_shell_script获取完整的代码。
您可以使用以下命令编译和运行代码:
$ javac -d mods --module-source-path src $(find src -name *.java)
$ java -p mods -m process/com.packt.process.RunningShellScriptDemo
我们得到的输出如下:
它是如何工作的...
在这个示例中,您必须记下两件事:
-
将进程的工作目录更改为 shell 脚本的位置。
-
使用
/bin/bash执行 shell 脚本。
如果你没有记下第一步,那么你将不得不使用 shell 脚本文件的绝对路径。然而,在这个示例中,我们做了这个,因此我们只需使用 shell 脚本名称来执行/bin/bash命令。
第 2 步基本上是您希望执行 shell 脚本的方式。要执行此操作的方法是将 shell 脚本传递给解释器,解释器将解释和执行脚本。以下代码行就是这样做的:
pb.command("/bin/bash", "script.sh")
获取当前 JVM 的进程信息
运行中的进程有一组与之关联的属性,例如以下内容:
-
PID:这个唯一标识进程
-
所有者:这是启动进程的用户的名称
-
命令:这是在进程下运行的命令
-
CPU 时间:这表示进程已经活动的时间
-
开始时间:这表示进程启动的时间
这些是我们通常感兴趣的一些属性。也许我们还对 CPU 使用率或内存使用率感兴趣。现在,在 Java 9 之前,从 Java 中获取这些信息是不可能的。然而,在 Java 9 中,引入了一组新的 API,使我们能够获取有关进程的基本信息。
在本示例中,我们将看到如何获取当前 Java 进程的进程信息;也就是说,正在执行您的代码的进程。
如何做...
按照以下步骤进行:
- 创建一个简单的类,并使用
ProcessHandle.current()来获取当前 Java 进程的ProcessHandle:
ProcessHandle handle = ProcessHandle.current();
- 我们添加了一些代码,这将为代码增加一些运行时间:
for ( int i = 0 ; i < 100; i++){
Thread.sleep(1000);
}
- 在
ProcessHandle实例上使用info()方法获取ProcessHandle.Info的实例:
ProcessHandle.Info info = handle.info();
- 使用
ProcessHandle.Info的实例获取接口提供的所有信息:
System.out.println("Command line: " +
info.commandLine().get());
System.out.println("Command: " + info.command().get());
System.out.println("Arguments: " +
String.join(" ", info.arguments().get()));
System.out.println("User: " + info.user().get());
System.out.println("Start: " + info.startInstant().get());
System.out.println("Total CPU Duration: " +
info.totalCpuDuration().get().toMillis() +"ms");
- 使用
ProcessHandle的pid()方法获取当前 Java 进程的进程 ID:
System.out.println("PID: " + handle.pid());
- 我们还将打印结束时间,使用代码即将结束时的时间。这将让我们了解进程的执行时间:
Instant end = Instant.now();
System.out.println("End: " + end);
您可以从Chapter08/6_current_process_info获取完整的代码。
使用以下命令编译和运行代码:
$ javac -d mods --module-source-path src $(find src -name *.java)
$ java -p mods -m process/com.packt.process.CurrentProcessInfoDemo
您将看到的输出将类似于这样:
程序执行完成需要一些时间。
需要注意的一点是,即使程序运行了大约两分钟,总 CPU 持续时间也只有 350 毫秒。这是 CPU 繁忙的时间段。
它是如何工作的...
为了给本地进程更多的控制并获取其信息,Java API 中添加了一个名为ProcessHandle的新接口。使用ProcessHandle,您可以控制进程执行并获取有关进程的一些信息。该接口还有一个名为ProcessHandle.Info的内部接口。该接口提供了一些 API 来获取有关进程的信息。
有多种方法可以获取进程的ProcessHandle对象。以下是其中一些方法:
-
ProcessHandle.current(): 用于获取当前 Java 进程的ProcessHandle实例。 -
Process.toHandle(): 用于获取给定Process对象的ProcessHandle。 -
ProcessHandle.of(pid): 用于获取由给定 PID 标识的进程的ProcessHandle。
在我们的示例中,我们使用第一种方法,即使用ProcessHandle.current()。这使我们可以处理当前的 Java 进程。在ProcessHandle实例上调用info()方法将为我们提供ProcessHandle.Info接口的实现的实例,我们可以利用它来获取进程信息,如示例代码所示。
ProcessHandle和ProcessHandle.Info都是接口。JDK 提供的 Oracle JDK 或 Open JDK 将为这些接口提供实现。Oracle JDK 有一个名为ProcessHandleImpl的类,它实现了ProcessHandle,还有一个名为Info的ProcessHandleImpl内部类,它实现了ProcessHandle.Info接口。因此,每当调用上述方法之一来获取ProcessHandle对象时,都会返回ProcessHandleImpl的实例。
Process类也是如此。它是一个抽象类,Oracle JDK 提供了一个名为ProcessImpl的实现,该实现实现了Process类中的抽象方法。
在本章的所有示例中,对ProcessHandle实例或ProcessHandle对象的任何提及都将指的是ProcessHandleImpl的实例或对象,或者是您正在使用的 JDK 提供的任何其他实现类。
此外,对ProcessHandle.Info的实例或ProcessHandle.Info对象的任何提及都将指的是ProcessHandleImpl.Info的实例或对象,或者是您正在使用的 JDK 提供的任何其他实现类。
获取生成的进程的进程信息
在我们之前的示例中,我们看到了如何获取当前 Java 进程的进程信息。在这个示例中,我们将看看如何获取由 Java 代码生成的进程的进程信息;也就是说,由当前 Java 进程生成的进程。使用的 API 与我们在之前的示例中看到的相同,只是ProcessHandle实例的实现方式不同。
准备工作
在这个示例中,我们将使用 Unix 命令sleep,它用于暂停执行一段时间(以秒为单位)。
如何做...
按照以下步骤进行:
- 从 Java 代码中生成一个新的进程,运行
sleep命令:
ProcessBuilder pBuilder = new ProcessBuilder("sleep", "20");
Process p = pBuilder.inheritIO().start();
- 获取此生成的进程的
ProcessHandle实例:
ProcessHandle handle = p.toHandle();
- 等待生成的进程完成执行:
int exitValue = p.waitFor();
- 使用
ProcessHandle获取ProcessHandle.Info实例,并使用其 API 获取所需信息。或者,我们甚至可以直接使用Process对象通过Process类中的info()方法获取ProcessHandle.Info:
ProcessHandle.Info info = handle.info();
System.out.println("Command line: " +
info.commandLine().get());
System.out.println("Command: " + info.command().get());
System.out.println("Arguments: " + String.join(" ",
info.arguments().get()));
System.out.println("User: " + info.user().get());
System.out.println("Start: " + info.startInstant().get());
System.out.println("Total CPU time(ms): " +
info.totalCpuDuration().get().toMillis());
System.out.println("PID: " + handle.pid());
您可以从Chapter08/7_spawned_process_info获取完整的代码。
使用以下命令编译和运行代码:
$ javac -d mods --module-source-path src $(find src -name *.java)
$ java -p mods -m process/com.packt.process.SpawnedProcessInfoDemo
另外,在Chapter08/7_spawned_process_info中有一个run.sh脚本,您可以在任何基于 Unix 的系统上运行/bin/bash run.sh。
您看到的输出将类似于这样:
管理生成的进程
有一些方法,如destroy()、destroyForcibly()(在 Java 8 中添加)、isAlive()(在 Java 8 中添加)和supportsNormalTermination()(在 Java 9 中添加),可以用于控制生成的进程。这些方法既可以在Process对象上使用,也可以在ProcessHandle对象上使用。在这里,控制只是检查进程是否存活,如果是,则销毁进程。
在这个示例中,我们将生成一个长时间运行的进程,并执行以下操作:
-
检查其是否存活
-
检查它是否可以正常停止;也就是说,根据平台的不同,进程可以通过 destroy 或 force destroy 来停止
-
停止进程
如何做...
- 从 Java 代码中生成一个新的进程,运行
sleep命令,比如一分钟或 60 秒:
ProcessBuilder pBuilder = new ProcessBuilder("sleep", "60");
Process p = pBuilder.inheritIO().start();
- 等待,比如 10 秒:
p.waitFor(10, TimeUnit.SECONDS);
- 检查进程是否存活:
boolean isAlive = p.isAlive();
System.out.println("Process alive? " + isAlive);
- 检查进程是否可以正常停止:
boolean normalTermination = p.supportsNormalTermination();
System.out.println("Normal Termination? " + normalTermination);
- 停止进程并检查其是否存活:
p.destroy();
isAlive = p.isAlive();
System.out.println("Process alive? " + isAlive);
您可以从Chapter08/8_manage_spawned_process获取完整的代码。
我们提供了一个名为run.sh的实用脚本,您可以使用它来编译和运行代码——sh run.sh。
我们得到的输出如下:
如果我们在 Windows 上运行程序,supportsNormalTermination()返回false,但在 Unix 上,supportsNormalTermination()返回true(如前面的输出中所见)。
枚举系统中的活动进程
在 Windows 中,您可以打开 Windows 任务管理器来查看当前活动的进程,在 Linux 中,您可以使用ps命令及其各种选项来查看进程以及其他详细信息,如用户、时间、命令等。
在 Java 9 中,添加了一个名为ProcessHandle的新 API,用于控制和获取有关进程的信息。API 的一个方法是allProcesses(),它返回当前进程可见的所有进程的快照。在这个示例中,我们将看看这个方法的工作原理以及我们可以从 API 中提取的信息。
如何做...
按照以下步骤进行:
- 在
ProcessHandle接口上使用allProcesses()方法,以获取当前活动进程的流:
Stream<ProcessHandle> liveProcesses =
ProcessHandle.allProcesses();
- 使用
forEach()迭代流,并传递 lambda 表达式以打印可用的详细信息:
liveProcesses.forEach(ph -> {
ProcessHandle.Info phInfo = ph.info();
System.out.println(phInfo.command().orElse("") +" " +
phInfo.user().orElse(""));
});
您可以从Chapter08/9_enumerate_all_processes获取完整的代码。
我们提供了一个名为run.sh的实用脚本,您可以使用它来编译和运行代码——sh run.sh。
我们得到的输出如下:
在前面的输出中,我们打印了命令名称以及进程的用户。我们展示了输出的一小部分。
使用管道连接多个进程
在 Unix 中,通常使用|符号将一组命令连接在一起,以创建一系列活动的管道,其中命令的输入是前一个命令的输出。这样,我们可以处理输入以获得所需的输出。
一个常见的场景是当您想要在日志文件中搜索某些内容或模式,或者在日志文件中搜索某些文本的出现时。在这种情况下,您可以创建一个管道,通过一系列命令,即cat、grep、wc -l等,传递所需的日志文件数据。
在这个示例中,我们将使用 UCI 机器学习库中提供的 Iris 数据集(archive.ics.uci.edu/ml/datasets/Iris)创建一个管道,我们将统计每种花的出现次数。
准备工作
我们已经下载了 Iris Flower 数据集(archive.ics.uci.edu/ml/datasets/iris),可以在本书的代码下载中的Chapter08/10_connecting_process_pipe/iris.data中找到。
如果您查看Iris数据,您会看到以下格式的 150 行:
4.7,3.2,1.3,0.2,Iris-setosa
这里,有多个由逗号(,)分隔的属性,属性如下:
-
花萼长度(厘米)
-
花萼宽度(厘米)
-
花瓣长度(厘米)
-
花瓣宽度(厘米)
-
类别:
-
Iris setosa
-
Iris versicolour
-
Iris virginica
在这个示例中,我们将找到每个类别中花的总数,即 setosa、versicolour 和 virginica。
我们将使用以下命令的管道(使用基于 Unix 的操作系统):
$ cat iris.data.txt | cut -d',' -f5 | uniq -c
我们得到的输出如下:
50 Iris-setosa
50 Iris-versicolor
50 Iris-virginica
1
末尾的 1 表示文件末尾有一个新行。所以每个类别有 50 朵花。让我们解析上面的 shell 命令管道并理解它们各自的功能:
-
cat:此命令读取作为参数给定的文件。 -
cut:这使用-d选项中给定的字符拆分每一行,并返回由-f选项标识的列中的值。 -
uniq:这从给定的值返回一个唯一列表,当使用-c选项时,它返回列表中每个唯一值的出现次数。
操作步骤
- 创建一个
ProcessBuilder对象的列表,其中将保存参与我们的管道的ProcessBuilder实例。还将管道中最后一个进程的输出重定向到当前 Java 进程的标准输出:
List<ProcessBuilder> pipeline = List.of(
new ProcessBuilder("cat", "iris.data.txt"),
new ProcessBuilder("cut", "-d", ",", "-f", "5"),
new ProcessBuilder("uniq", "-c")
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
);
- 使用
ProcessBuilder的startPipeline()方法,并传递ProcessBuilder对象的列表以启动管道。它将返回一个Process对象的列表,每个对象代表列表中的一个ProcessBuilder对象:
List<Process> processes = ProcessBuilder.startPipeline(pipeline);
- 获取列表中的最后一个进程并
waitFor它完成:
int exitValue = processes.get(processes.size() - 1).waitFor();
您可以从Chapter08/10_connecting_process_pipe获取完整的代码。
我们提供了一个名为run.sh的实用脚本,您可以使用它来编译和运行代码——sh run.sh。
我们得到的输出如下:
工作原理
startPipeline()方法为列表中的每个ProcessBuilder对象启动一个Process。除了第一个和最后一个进程外,它通过使用ProcessBuilder.Redirect.PIPE将一个进程的输出重定向到另一个进程的输入。如果您为任何中间进程提供了redirectOutput,而不是ProcessBuilder.Redirect.PIPE,那么将会抛出错误;类似于以下内容:
Exception in thread "main" java.lang.IllegalArgumentException: builder redirectOutput() must be PIPE except for the last builder: INHERIT.
它指出除了最后一个之外的任何构建器都应将其输出重定向到下一个进程。对于redirectInput也是适用的。
管理子进程
当一个进程启动另一个进程时,启动的进程成为启动进程的子进程。启动的进程反过来可以启动另一个进程,这个链条可以继续下去。这导致了一个进程树。通常,我们可能需要处理一个有错误的子进程,可能想要终止该子进程,或者可能想要知道启动的子进程并可能想要获取有关它们的一些信息。
在 Java 9 中,Process类中添加了两个新的 API——children()和descendants()。children() API 允许您获取当前进程的直接子进程的快照列表,而descendants() API 提供了当前进程递归children()的进程的快照;也就是说,它们在每个子进程上递归地调用children()。
在这个配方中,我们将查看children()和descendants() API,并看看我们可以从进程的快照中收集到什么信息。
准备就绪
让我们创建一个简单的 shell 脚本,我们将在配方中使用它。此脚本可以在Chapter08/11_managing_sub_process/script.sh中找到:
echo "Running tree command";
tree;
sleep 60;
echo "Running iostat command";
iostat;
在上述脚本中,我们运行了tree和iostat命令,中间用一分钟的睡眠时间分隔。如果您想了解这些命令,请参考本章的运行 shell 脚本配方。当从 bash shell 中执行时,睡眠命令每次被调用时都会创建一个新的子进程。
我们将创建,比如说,10 个ProcessBuilder实例来运行上述的 shell 脚本并同时启动它们。
如何做...
- 我们将创建 10 个
ProcessBuilder实例来运行我们的 shell 脚本(位于Chapter08/11_managing_sub_process/script.sh)。我们不关心它的输出,所以让我们通过将输出重定向到预定义的重定向ProcessHandle.Redirect.DISCARD来丢弃命令的输出:
for ( int i = 0; i < 10; i++){
new ProcessBuilder("/bin/bash", "script.sh")
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.start();
}
- 获取当前进程的句柄:
ProcessHandle currentProcess = ProcessHandle.current();
- 使用当前进程通过
children()API 获取其子进程,并迭代每个子进程以打印它们的信息。一旦我们有了ProcessHandle的实例,我们可以做多种事情,比如销毁进程,获取其进程信息等。
System.out.println("Obtaining children");
currentProcess.children().forEach(pHandle -> {
System.out.println(pHandle.info());
});
- 使用当前进程通过使用
descendants()API 获取所有子进程,然后迭代每个子进程以打印它们的信息:
currentProcess.descendants().forEach(pHandle -> {
System.out.println(pHandle.info());
});
您可以从Chapter08/11_managing_sub_process获取完整的代码。
我们提供了一个名为run.sh的实用脚本,您可以使用它来编译和运行代码——sh run.sh。
我们得到的输出如下:
工作原理...
children()和descendants() API 返回当前进程的直接子进程或后代进程的ProcessHandler的Stream。使用ProcessHandler的实例,我们可以执行以下操作:
-
获取进程信息
-
检查进程的状态
-
停止进程
第九章:使用 Spring Boot 创建 RESTful Web 服务
在本章中,我们将涵盖以下示例:
-
创建一个简单的 Spring Boot 应用程序
-
与数据库交互
-
创建一个 RESTful web 服务
-
为 Spring Boot 创建多个配置文件
-
将 RESTful web 服务部署到 Heroku
-
使用 Docker 将 RESTful web 服务容器化
-
使用 Micrometer 和 Prometheus 监控 Spring Boot 2 应用程序
介绍
近年来,基于微服务架构的推动已经得到了广泛的采用,这要归功于它在正确的方式下提供的简单性和易于维护性。许多公司,如 Netflix 和 Amazon,已经从单片系统转移到了更专注和轻量级的系统,它们之间通过 RESTful web 服务进行通信。RESTful web 服务的出现及其使用已知的 HTTP 协议创建 web 服务的简单方法,使得应用程序之间的通信比旧的基于 SOAP 的 web 服务更容易。
在本章中,我们将介绍Spring Boot框架,它提供了一种方便的方式来使用 Spring 库创建可投入生产的微服务。使用 Spring Boot,我们将开发一个简单的 RESTful web 服务并将其部署到云端。
创建一个简单的 Spring Boot 应用程序
Spring Boot 有助于轻松创建可投入生产的基于 Spring 的应用程序。它支持几乎所有 Spring 库的工作,而无需显式配置它们。提供了自动配置类,以便轻松集成大多数常用的库、数据库和消息队列。
在本示例中,我们将介绍如何创建一个简单的 Spring Boot 应用程序,其中包含一个在浏览器中打开时打印消息的控制器。
准备工作
Spring Boot 支持 Maven 和 Gradle 作为其构建工具,我们将在我们的示例中使用 Maven。以下 URL,start.spring.io/,提供了一种方便的方式来创建一个带有所需依赖项的空项目。我们将使用它来下载一个空项目。按照以下步骤创建并下载一个基于 Spring Boot 的空项目:
- 导航到
start.spring.io/,您将看到类似以下截图的内容:
-
您可以选择依赖管理和构建工具,通过在Generate a文本后的下拉菜单中选择适当的选项。
-
Spring Boot 支持 Java、Kotlin 和 Groovy。您可以通过更改with文本后的下拉菜单来选择语言。
-
通过在and Spring Boot文本后的下拉菜单中选择其值来选择 Spring Boot 版本。对于本示例,我们将使用 Spring Boot 2 的最新稳定版本,即 2.0.4。
-
在左侧的项目元数据下,我们需要提供与 Maven 相关的信息,即组 ID 和 artifact ID。我们将使用 Group 作为
com.packt,Artifact 作为boot_demo。 -
在右侧的依赖项下,您可以搜索要添加的依赖项。对于本示例,我们需要 web 和 Thymeleaf 依赖项。这意味着我们想要创建一个使用 Thymeleaf UI 模板的 web 应用程序,并且希望所有依赖项,如 Spring MVC 和嵌入式 Tomcat,都成为应用程序的一部分。
-
单击生成项目按钮以下载空项目。您可以将此空项目加载到您选择的任何 IDE 中,就像加载任何其他 Maven 项目一样。
此时,您将在您选择的任何 IDE 中加载您的空项目,并准备进一步探索。在本示例中,我们将使用 Thymeleaf 模板引擎来定义我们的网页,并创建一个简单的控制器来呈现网页。
此处的完整代码可以在Chapter09/1_boot_demo中找到。
如何做...
-
如果您按照“准备就绪”部分提到的组 ID 和工件 ID 命名约定进行了跟随,您将拥有一个包结构
com.packt.boot_demo,以及一个BootDemoApplication.java主类已经为您创建。在tests文件夹下将有一个等效的包结构和一个BootDemoApplicationTests.java主类。 -
在
com.packt.boot_demo包下创建一个名为SimpleViewController的新类,其中包含以下代码:
@Controller
public class SimpleViewController{
@GetMapping("/message")
public String message(){
return "message";
}
}
- 在
src/main/resources/templates下创建一个名为message.html的网页,其中包含以下代码:
<h1>Hello, this is a message from the Controller</h1>
<h2>The time now is [[${#dates.createNow()}]]</h2>
- 从命令提示符中,导航到项目根文件夹,并发出
mvn spring-boot:run命令;您将看到应用程序正在启动。一旦完成初始化并启动,它将在默认端口8080上运行。导航到http://localhost:8080/message以查看消息。
我们使用 Spring Boot 的 Maven 插件,它为我们提供了方便的工具来在开发过程中启动应用程序。但是对于生产环境,我们将创建一个 fat JAR,即一个包含所有依赖项的 JAR,并将其部署为 Linux 或 Windows 服务。我们甚至可以使用java -jar命令运行这个 fat JAR。
工作原理
我们不会深入讨论 Spring Boot 或其他 Spring 库的工作原理。Spring Boot 创建了一个嵌入式 Tomcat,运行在默认端口8080上。然后,它注册了所有被@SpringBootApplication注解的类所在包及其子包中可用的控制器、组件和服务。
在我们的示例中,com.packt.boot_demo包中的BootDemoApplication类被注解为@SpringBootApplication。因此,所有被注解为@Controller、@Service、@Configuration和@Component的类都会被 Spring 框架注册为 bean,并由其管理。现在,这些可以通过使用@Autowired注解注入到代码中。
我们可以通过两种方式创建一个 web 控制器:
-
使用
@Controller进行注解 -
使用
@RestController进行注解
在第一种方法中,我们创建了一个既可以提供原始数据又可以提供 HTML 数据(由模板引擎如 Thymeleaf、Freemarker 和 JSP 生成)的控制器。在第二种方法中,控制器支持只能提供 JSON 或 XML 形式的原始数据的端点。在我们的示例中,我们使用了前一种方法,如下所示:
@Controller
public class SimpleViewController{
@GetMapping("/message")
public String message(){
return "message";
}
}
我们可以用@RequestMapping注解类,比如@RequestMapping("/api")。在这种情况下,控制器中暴露的任何 HTTP 端点都会以/api开头。对于 HTTP 的GET、POST、DELETE和PUT方法,有专门的注解映射,分别是@GetMapping、@PostMapping、@DeleteMapping和@PutMapping。我们也可以将我们的控制器类重写如下:
@Controller
@RequestMapping("/message")
public class SimpleViewController{
@GetMapping
public String message(){
return "message";
}
}
我们可以通过在application.properties文件中提供server.port = 9090来修改端口。这个文件可以在src/main/resources/application.properties中找到。有一整套属性(docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html)可以用来自定义和连接不同的组件。
与数据库交互
在这个示例中,我们将看看如何与数据库集成,以创建、读取、修改和删除数据。为此,我们将设置一个带有所需表的 MySQL 数据库。随后,我们将从我们的 Spring Boot 应用程序中更新表中的数据。
我们将使用 Windows 作为这个示例的开发平台。您也可以在 Linux 上执行类似的操作,但首先必须设置您的 MySQL 数据库。
准备就绪
在我们开始将应用程序与数据库集成之前,我们需要在开发机器上本地设置数据库。在接下来的几节中,我们将下载和安装 MySQL 工具,然后创建一个带有一些数据的示例表,我们将在应用程序中使用。
安装 MySQL 工具
首先,从dev.mysql.com/downloads/windows/installer/5.7.html下载 MySQL 安装程序。这个 MySQL 捆绑包只适用于 Windows。按照屏幕上的说明成功安装 MySQL 以及其他工具,如 MySQL Workbench。要确认 MySQL 守护程序(mysqld)正在运行,打开任务管理器,你应该能够看到一个类似以下的进程:
你应该记住你为 root 用户设置的密码。
让我们运行 MySQL Workbench;启动时,你应该能够看到类似以下截图的东西,以及工具提供的其他内容:
如果你在前面的图像中找不到连接,你可以使用(+)号添加一个。当你点击(+)时,你将看到以下对话框。填写它并点击测试连接以获得成功消息:
成功的测试连接将导致以下消息:
双击连接到数据库,你应该能够在左侧看到一个 DB 列表,在右侧看到一个空白区域,在顶部看到一个菜单和工具栏。从文件菜单中,点击新查询选项卡,或按Ctrl + T获得一个新的查询窗口。在这里,我们将编写我们的查询来创建一个数据库,并在该数据库中创建一个表。
从dev.mysql.com/downloads/windows/installer/5.7.html下载的捆绑安装程序仅适用于 Windows。Linux 用户必须单独下载 MySQL 服务器和 MySQL Workbench(与 DB 交互的 GUI)。
MySQL 服务器可以从dev.mysql.com/downloads/mysql/下载。
MySQL Workbench 可以从dev.mysql.com/downloads/workbench/下载。
创建一个示例数据库
运行以下 SQL 语句创建数据库:
create database sample;
创建一个人员表
运行以下 SQL 语句使用新创建的数据库并创建一个简单的人员表:
create table person(
id int not null auto_increment,
first_name varchar(255),
last_name varchar(255),
place varchar(255),
primary key(id)
);
填充示例数据
让我们继续在我们刚刚创建的表中插入一些示例数据:
insert into person(first_name, last_name, place)
values('Raj', 'Singh', 'Bangalore');
insert into person(first_name, last_name, place)
values('David', 'John', 'Delhi');
现在我们的数据库准备好了,我们将继续从start.spring.io/下载空的 Spring Boot 项目,选项如下:
如何做...
- 创建一个模型类
com.packt.boot_db_demo.Person,代表一个人。我们将使用 Lombok 注解为我们生成 getter 和 setter:
@Data
public class Person{
private Integer id;
private String firstName;
private String lastName;
private String place;
}
- 创建
com.packt.boot_db_demo.PersonMapper将数据库中的数据映射到我们的模型类Person:
@Mapper
public interface PersonMapper {
}
- 让我们添加一个方法来获取表中的所有行。请注意,接下来的几个方法将写在
PersonMapper接口内:
@Select("SELECT * FROM person")
public List<Person> getPersons();
- 通过 ID 标识的单个人的详细信息的另一种方法如下:
@Select("SELECT * FROM person WHERE id = #{id}")
public Person getPerson(Integer id);
- 在表中创建新行的方法如下:
@Insert("INSERT INTO person(first_name, last_name, place) "
+ " VALUES (#{firstName}, #{lastName}, #{place})")
@Options(useGeneratedKeys = true)
public void insert(Person person);
- 更新表中现有行的方法,通过 ID 标识:
@Update("UPDATE person SET first_name = #{firstName},last_name =
#{lastName}, "+ "place = #{place} WHERE id = #{id} ")
public void save(Person person);
- 从表中删除行的方法,通过 ID 标识:
@Delete("DELETE FROM person WHERE id = #{id}")
public void delete(Integer id);
- 让我们创建一个
com.packt.boot_db_demo.PersonController类,我们将用它来编写我们的 web 端点:
@Controller
@RequestMapping("/persons")
public class PersonContoller {
@Autowired PersonMapper personMapper;
}
- 让我们创建一个端点来列出
person表中的所有条目:
@GetMapping
public String list(ModelMap model){
List<Person> persons = personMapper.getPersons();
model.put("persons", persons);
return "list";
}
- 让我们创建一个端点来在
person表中添加一个新行:
@GetMapping("/{id}")
public String detail(ModelMap model, @PathVariable Integer id){
System.out.println("Detail id: " + id);
Person person = personMapper.getPerson(id);
model.put("person", person);
return "detail";
}
- 让我们创建一个端点来在
person表中添加一个新行或编辑一个现有行:
@PostMapping("/form")
public String submitForm(Person person){
System.out.println("Submiting form person id: " +
person.getId());
if ( person.getId() != null ){
personMapper.save(person);
}else{
personMapper.insert(person);
}
return "redirect:/persons/";
}
- 让我们创建一个端点来从
person表中删除一行:
@GetMapping("/{id}/delete")
public String deletePerson(@PathVariable Integer id){
personMapper.delete(id);
return "redirect:/persons";
}
- 更新
src/main/resources/application.properties文件,提供与我们的数据源(即 MySQL 数据库)相关的配置:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/sample?useSSL=false
spring.datasource.username=root
spring.datasource.password=mohamed
mybatis.configuration.map-underscore-to-camel-case=true
您可以使用mvn spring-boot:run命令行运行应用程序。该应用程序在默认端口8080上启动。在浏览器中导航到http://localhost:8080/persons。
这个食谱的完整代码可以在Chapter09/2_boot_db_demo找到。
访问http://localhost:8080/persons,您会看到以下内容:
点击新建人员,您会得到以下内容:
点击编辑,你会得到以下内容:
工作原理...
首先,com.packt.boot_db_demo.PersonMapper使用org.apache.ibatis.annotations.Mapper注解知道如何执行@Select、@Update和@Delete注解中提供的查询,并返回相关结果。这一切都由 MyBatis 和 Spring Data 库管理。
你一定想知道如何实现与数据库的连接。Spring Boot 的一个自动配置类DataSourceAutoConfiguration通过使用application.properties文件中定义的spring.datasource.*属性来设置连接,从而为我们提供了javax.sql.DataSource的实例。然后 MyBatis 库使用这个javax.sql.DataSource对象为我们提供了SqlSessionTemplate的实例,这就是我们的PersonMapper在后台使用的。
然后,我们通过@AutoWired将com.packt.boot_db_demo.PersonMapper注入到com.packt.boot_db_demo.PersonController类中。@AutoWired注解寻找任何 Spring 管理的 bean,这些 bean 要么是确切类型的实例,要么是其实现。查看本章中的创建一个简单的 Spring Boot 应用程序食谱,了解@Controller注解。
凭借极少的配置,我们已经能够快速设置简单的 CRUD 操作。这就是 Spring Boot 为开发人员提供的灵活性和敏捷性!
创建 RESTful Web 服务
在上一个食谱中,我们使用 Web 表单与数据进行交互。在这个食谱中,我们将看到如何使用 RESTful Web 服务与数据进行交互。这些 Web 服务是使用已知的 HTTP 协议及其方法(即 GET、POST 和 PUT)与其他应用程序进行交互的一种方式。数据可以以 XML、JSON 甚至纯文本的形式交换。我们将在我们的食谱中使用 JSON。
因此,我们将创建 RESTful API 来支持检索数据、创建新数据、编辑数据和删除数据。
准备工作
像往常一样,通过选择以下截图中显示的依赖项从start.spring.io/下载起始项目:
如何做...
- 从上一个食谱中复制
Person类:
public class Person {
private Integer id;
private String firstName;
private String lastName;
private String place;
//required getters and setters
}
-
我们将以不同的方式完成
PersonMapper部分。我们将在一个 mapper XML 文件中编写所有的 SQL 查询,然后从PersonMapper接口中引用它们。我们将把 mapper XML 放在src/main/resources/mappers文件夹下。我们将mybatis.mapper-locations属性的值设置为classpath*:mappers/*.xml。这样,PersonMapper接口就可以发现与其方法对应的 SQL 查询。 -
创建
com.packt.boot_rest_demo.PersonMapper接口:
@Mapper
public interface PersonMapper {
public List<Person> getPersons();
public Person getPerson(Integer id);
public void save(Person person);
public void insert(Person person);
public void delete(Integer id);
}
- 在
PersonMapper.xml中创建 SQL。确保<mapper>标签的namespace属性与PersonMapper映射接口的完全限定名称相同:
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.packt.boot_rest_demo.PersonMapper">
<select id="getPersons"
resultType="com.packt.boot_rest_demo.Person">
SELECT id, first_name firstname, last_name lastname, place
FROM person
</select>
<select id="getPerson"
resultType="com.packt.boot_rest_demo.Person"
parameterType="long">
SELECT id, first_name firstname, last_name lastname, place
FROM person
WHERE id = #{id}
</select>
<update id="save"
parameterType="com.packt.boot_rest_demo.Person">
UPDATE person SET
first_name = #{firstName},
last_name = #{lastName},
place = #{place}
WHERE id = #{id}
</update>
<insert id="insert"
parameterType="com.packt.boot_rest_demo.Person"
useGeneratedKeys="true" keyColumn="id" keyProperty="id">
INSERT INTO person(first_name, last_name, place)
VALUES (#{firstName}, #{lastName}, #{place})
</insert>
<delete id="delete" parameterType="long">
DELETE FROM person WHERE id = #{id}
</delete>
</mapper>
- 在
src/main/resources/application.properties文件中定义应用程序属性:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/sample?
useSSL=false
spring.datasource.username=root
spring.datasource.password=mohamed
mybatis.mapper-locations=classpath*:mappers/*.xml
- 为 REST API 创建一个空的控制器。这个控制器将被标记为
@RestController注解,因为其中的所有 API 都将专门处理数据:
@RestController
@RequestMapping("/api/persons")
public class PersonApiController {
@Autowired PersonMapper personMapper;
}
- 添加一个 API 来列出
person表中的所有行:
@GetMapping
public ResponseEntity<List<Person>> getPersons(){
return new ResponseEntity<>(personMapper.getPersons(),
HttpStatus.OK);
}
- 添加一个 API 来获取单个人的详细信息:
@GetMapping("/{id}")
public ResponseEntity<Person> getPerson(@PathVariable Integer id){
return new ResponseEntity<>(personMapper.getPerson(id),
HttpStatus.OK);
}
- 添加一个 API 来向表中添加新数据:
@PostMapping
public ResponseEntity<Person> newPerson
(@RequestBody Person person){
personMapper.insert(person);
return new ResponseEntity<>(person, HttpStatus.OK);
}
- 添加一个 API 来编辑表中的数据:
@PostMapping("/{id}")
public ResponseEntity<Person> updatePerson
(@RequestBody Person person,
@PathVariable Integer id){
person.setId(id);
personMapper.save(person);
return new ResponseEntity<>(person, HttpStatus.OK);
}
- 添加一个 API 来删除表中的数据:
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePerson
(@PathVariable Integer id){
personMapper.delete(id);
return new ResponseEntity<>(HttpStatus.OK);
}
您可以在Chapter09/3_boot_rest_demo找到完整的代码。您可以通过在项目文件夹中使用mvn spring-boot:run来启动应用程序。应用程序启动后,导航到http://localhost:8080/api/persons以查看 person 表中的所有数据。
为了测试其他 API,我们将使用 Google Chrome 的 Postman REST 客户端应用程序。
这就是添加新人的样子。看一下请求体,也就是在 JSON 中指定的人的详细信息:
这就是我们编辑一个人的详细信息的方式:
这就是删除一个人的样子:
它是如何工作的...
首先,让我们看一下PersonMapper接口是如何发现要执行的 SQL 语句的。如果您查看src/main/resources/mappers/PersonMapper.xml,您会发现<mapper>的namespace属性是org.packt.boot_rest_demo.PersonMapper。这是一个要求,即namespace属性的值应该是 mapper 接口的完全限定名称,在我们的例子中是org.packt.boot_rest_demo.PersonMapper。
接下来,<select>、<insert>、<update>和<delete>中定义的各个 SQL 语句的id属性应该与 mapper 接口中方法的名称匹配。例如,PersonMapper接口中的getPersons()方法寻找一个id="getPersons"的 SQL 语句。
现在,MyBatis 库通过读取mybatis.mapper-locations属性的值来发现 mapper XML 的位置。
关于控制器,我们引入了一个新的注解@RestController。这个特殊的注解表示,除了它是一个 web 控制器之外,类中定义的所有方法都返回通过 HTTP 响应体发送的响应;所有的 REST API 都是如此。它们只是处理数据。
通常情况下,您可以通过使用 Maven Spring Boot 插件mvn spring-boot:run或者执行 Maven 包创建的 JARjava -jar my_jar_name.jar来启动 Spring Boot 应用程序。
为 Spring Boot 创建多个配置文件
通常,Web 应用程序在不同的环境上部署 - 首先在开发人员的机器上本地运行,然后部署在测试服务器上,最后部署在生产服务器上。对于每个环境,我们希望应用程序与位于不同位置的组件进行交互。这种情况下的最佳方法是为每个环境维护不同的配置文件。其中一种方法是创建不同版本的application.properties文件,即存储应用程序级属性的文件的不同版本。Spring Boot 中的这些属性文件也可以是 YML 文件,比如application.yml。即使您创建了不同的版本,您也需要一种机制来告诉您的应用程序选择与其部署的环境相关的文件的相关版本。
Spring Boot 为这样的功能提供了令人惊叹的支持。它允许您拥有多个配置文件,每个文件代表一个特定的配置文件,然后,您可以根据部署的环境在不同的配置文件中启动应用程序。让我们看看它是如何运作的,然后我们将解释它是如何工作的。
准备工作
对于这个示例,有两种选项来托管另一个实例的 MySQL 数据库:
-
使用云服务提供商,比如 AWS,并使用其 Amazon 关系型数据库服务(RDS)(
aws.amazon.com/rds/)。它们有一定的免费使用限制。 -
使用云服务提供商,比如 DigitalOcean (
www.digitalocean.com/),以每月 5 美元的价格购买一个 droplet(即服务器)。在其上安装 MySQL 服务器。 -
使用 VirtualBox 在您的机器上安装 Linux,假设我们使用 Windows,或者如果您使用 Linux,则反之。在其上安装 MySQL 服务器。
选项非常多,从托管数据库服务到服务器,都可以让您完全控制 MySQL 服务器的安装。对于这个配方,我们做了以下工作:
-
我们从 DigitalOcean 购买了一个基本的 droplet。
-
我们使用
sudo apt-get install mysql-server-5.7安装了 MySQL,并为 root 用户设置了密码。 -
我们创建了另一个用户
springboot,以便我们可以使用这个用户从我们的 RESTful Web 服务应用程序进行连接:
$ mysql -uroot -p
Enter password:
mysql> create user 'springboot'@'%' identified by 'springboot';
-
我们修改了 MySQL 配置文件,以便 MySQL 允许远程连接。这可以通过编辑
/etc/mysql/mysql.conf.d/mysqld.cnf文件中的bind-address属性来完成。 -
从 MySQL Workbench 中,我们通过使用
IP = <Digital Ocean droplet IP>,username = springboot和password = springboot添加了新的 MySQL 连接。
在 Ubuntu OS 中,MySQL 配置文件的位置是/etc/mysql/mysql.conf.d/mysqld.cnf。找出特定于您的操作系统的配置文件位置的一种方法是执行以下操作:
-
运行
mysql --help。 -
在输出中,搜索
Default options are read from the following files in the given order:。接下来是 MySQL 配置文件的可能位置。
我们将创建所需的表并填充一些数据。但在此之前,我们将以root身份创建sample数据库,并授予springboot用户对其的所有权限:
mysql -uroot
Enter password:
mysql> create database sample;
mysql> GRANT ALL ON sample.* TO 'springboot'@'%';
Query OK, 0 rows affected (0.00 sec)
mysql> flush privileges;
现在,让我们以springboot用户的身份连接到数据库,创建所需的表,并填充一些示例数据:
mysql -uspringboot -pspringboot
mysql> use sample
Database changed
mysql> create table person(
-> id int not null auto_increment,
-> first_name varchar(255),
-> last_name varchar(255),
-> place varchar(255),
-> primary key(id)
-> );
Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO person(first_name, last_name, place) VALUES('Mohamed', 'Sanaulla', 'Bangalore');
mysql> INSERT INTO person(first_name, last_name, place) VALUES('Nick', 'Samoylov', 'USA');
mysql> SELECT * FROM person;
+----+------------+-----------+-----------+
| id | first_name | last_name | place |
+----+------------+-----------+-----------+
| 1 | Mohamed | Sanaulla | Bangalore |
| 2 | Nick | Samoylov | USA |
+----+------------+-----------+-----------+
2 rows in set (0.00 sec)
现在,我们有了云实例的 MySQL 数据库准备就绪。让我们看看如何根据应用程序运行的配置文件管理两个不同连接的信息。
可以在Chapter09/4_boot_multi_profile_incomplete中找到此配方所需的初始示例应用程序。我们将转换此应用程序,使其在不同的环境中运行。
如何做到...
-
在
src/main/resources/application.properties文件中,添加一个新的springboot属性,spring.profiles.active = local。 -
在
src/main/resources/中创建一个新文件application-local.properties。 -
将以下属性添加到
application-local.properties中,并从application.properties文件中删除它们:
spring.datasource.url=jdbc:mysql://localhost/sample?useSSL=false
spring.datasource.username=root
spring.datasource.password=mohamed
-
在
src/main/resources/中创建另一个文件application-cloud.properties。 -
将以下属性添加到
application-cloud.properties中:
spring.datasource.url=
jdbc:mysql://<digital_ocean_ip>/sample?useSSL=false
spring.datasource.username=springboot
spring.datasource.password=springboot
完整的应用程序代码可以在Chapter09/4_boot_multi_profile_incomplete中找到。您可以使用mvn spring-boot:run命令运行应用程序。Spring Boot 从application.properties文件中读取spring.profiles.active属性,并在本地配置文件中运行应用程序。在浏览器中打开http://localhost:8080/api/personsURL,以查看以下数据:
[
{
"id": 1,
"firstName": "David ",
"lastName": "John",
"place": "Delhi"
},
{
"id": 2,
"firstName": "Raj",
"lastName": "Singh",
"place": "Bangalore"
}
]
现在,使用mvn spring-boot:run -Dspring.profiles.active=cloud命令在云配置文件上运行应用程序。然后,在浏览器中打开http://localhost:8080/api/persons,以查看以下数据:
[
{
"id": 1,
"firstName": "Mohamed",
"lastName": "Sanaulla",
"place": "Bangalore"
},
{
"id": 2,
"firstName": "Nick",
"lastName": "Samoylov",
"place": "USA"
}
]
您可以看到相同 API 返回了不同的数据集,并且之前的数据是插入到我们在云上运行的 MySQL 数据库中的。因此,我们已成功地在两个不同的配置文件中运行了应用程序:本地和云端。
工作原理...
Spring Boot 可以以多种方式读取应用程序的配置。以下是一些重要的方式,按其相关性顺序列出(在较早的源中定义的属性会覆盖在后来的源中定义的属性):
-
从命令行。属性使用
-D选项指定,就像我们在云配置文件中启动应用程序时所做的那样,mvn spring-boot:run -Dspring.profiles.active=cloud。或者,如果您使用 JAR,它将是java -Dspring.profiles.active=cloud -jar myappjar.jar。 -
从 Java 系统属性,使用
System.getProperties()。 -
操作系统环境变量。
-
特定配置文件,
application-{profile}.properties或application-{profile}.yml文件,打包在 JAR 之外。 -
特定配置文件,
application-{profile}.properties或application-{profile}.yml文件,打包在 JAR 中。 -
应用程序属性,
application.properties或application.yml定义在打包的 JAR 之外。 -
应用程序属性,
application.properties或application.yml打包在 JAR 中。 -
配置类(即使用
@Configuration注释)作为属性源(使用@PropertySource注释)。 -
Spring Boot 的默认属性。
在我们的示例中,我们在application.properties文件中指定了所有通用属性,例如以下属性,并且任何特定配置文件中的特定配置属性都在特定配置文件中指定:
spring.profiles.active=local
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
mybatis.mapper-locations=classpath*:mappers/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
从上面的列表中,我们可以发现application.properties或application-{profile}.properties文件可以在应用程序 JAR 之外定义。Spring Boot 将搜索属性文件的默认位置,其中一个路径是应用程序正在运行的当前目录的config子目录。
Spring Boot 支持的应用程序属性的完整列表可以在docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html找到。除了这些,我们可以创建自己的属性,这些属性将为我们的应用程序所需。
此示例的完整代码可以在Chapter09/4_boot_multi_profile_complete找到。
还有更多...
我们可以使用 Spring Boot 创建一个配置服务器,它将作为所有应用程序在所有配置文件中的所有属性的存储库。然后客户端应用程序可以连接到配置服务器,根据应用程序名称和应用程序配置读取相关属性。
在配置服务器中,可以使用类路径或 GitHub 存储库从文件系统读取应用程序属性。使用 GitHub 存储库的优势是属性文件可以进行版本控制。配置服务器中的属性文件可以更新,并且可以通过设置消息队列将这些更新推送到客户端应用程序的下游。另一种方法是使用@RefreshScope bean,然后在需要客户端应用程序拉取配置更改时调用/refresh API。
将 RESTful web 服务部署到 Heroku
平台即服务(PaaS)是云计算模型之一(另外两个是软件即服务(SaaS)和基础设施即服务(IaaS)),其中云计算提供商提供托管的计算平台,包括操作系统、编程语言运行时、数据库和其他附加组件,如队列、日志管理和警报。他们还为您提供工具来简化部署和监视应用程序的仪表板。
Heroku 是 PaaS 提供商领域中最早的参与者之一。它支持以下编程语言:Ruby、Node.js、Java、Python、Clojure、Scala、Go 和 PHP。Heroku 支持多个数据存储,如 MySQL、MongoDB、Redis 和 Elasticsearch。它提供与日志记录工具、网络实用程序、电子邮件服务和监视工具的集成。
Heroku 提供了一个名为 heroku-cli(cli.heroku.com)的命令行工具,可用于创建 Heroku 应用程序,部署,监视,添加资源等。其 Web 仪表板提供的功能也受到 CLI 的支持。它使用 Git 存储应用程序的源代码。因此,当您将应用程序代码推送到 Heroku 的 Git 存储库时,它会触发一个构建,根据您使用的构建包进行构建。然后,它要么使用默认方式生成应用程序,要么使用ProcFile来执行您的应用程序。
在本示例中,我们将把基于 Spring Boot 的 RESTful Web 服务部署到 Heroku。我们将继续使用我们在上一个示例“为 Spring Boot 创建多个配置文件”中在另一个云提供商上创建的数据库。
准备工作
在继续在 Heroku 上部署我们的示例应用程序之前,我们需要注册 Heroku 帐户并安装其工具,这将使我们能够从命令行工作。在接下来的章节中,我们将指导您完成注册过程,通过 Web UI 创建示例应用程序,以及通过 Heroku 命令行界面(CLI)。
设置 Heroku 账户
访问www.heroku.com并注册账户。如果您已经有账户,可以登录。要注册,请访问signup.heroku.com:
要登录,URL 是id.heroku.com/login:
成功登录后,您将看到一个仪表板,列出了应用程序的列表,如果有的话:
从 UI 创建新应用
单击 New | Create new app,填写详细信息,然后单击 Create App:
从 CLI 创建新应用程序
执行以下步骤从 CLI 创建一个新应用程序:
-
从
cli.heroku.com安装 Heroku CLI。 -
安装后,Heroku 应该在系统的
PATH变量中。 -
打开命令提示符并运行
heroku create。您将看到类似以下的输出:
Creating app... done, glacial-beyond-27911
https://glacial-beyond-27911.herokuapp.com/ |
https://git.heroku.com/glacial-beyond-27911.git
- 应用程序名称是动态生成的,并创建了一个远程 Git 存储库。您可以通过运行以下命令指定应用程序名称和区域(与 UI 中所做的一样):
$ heroku create test-app-9812 --region us
Creating test-app-9812... done, region is us
https://test-app-9812.herokuapp.com/ |
https://git.heroku.com/test-app-9812.git
通过git push将部署到 Heroku 的代码推送到远程 Git 存储库。我们将在下一节中看到这一点。
我们在Chapter09/5_boot_on_heroku中有应用程序的源代码。因此,复制此应用程序,然后继续在 Heroku 上部署。
在运行 Heroku 的 cli 中的任何命令之前,您必须登录 Heroku 帐户。您可以通过运行heroku login命令来登录。
如何做...
- 运行以下命令创建一个 Heroku 应用程序:
$ heroku create <app_name> -region us
- 在项目文件夹中初始化 Git 存储库:
$ git init
- 将 Heroku Git 存储库添加为本地 Git 存储库的远程存储库:
$ heroku git:remote -a <app_name_you_chose>
- 将源代码,即主分支,推送到 Heroku Git 存储库:
$ git add .
$ git commit -m "deploying to heroku"
$ git push heroku master
- 当代码推送到 Heroku Git 存储库时,会触发构建。由于我们使用 Maven,它运行以下命令:
./mvnw -DskipTests clean dependency:list install
-
代码完成构建和部署后,您可以使用
heroku open命令在浏览器中打开应用程序。 -
您可以使用
heroku logs --tail命令监视应用程序的日志。
应用程序成功部署后,并且在运行heroku open命令后,您应该看到浏览器加载的 URL:
单击Persons链接将显示以下信息:
[
{
"id":1,
"firstName":"Mohamed",
"lastName":"Sanaulla",
"place":"Bangalore"
},
{
"id":2,
"firstName":"Nick",
"lastName":"Samoylov",
"place":"USA"
}
]
有趣的是,我们的应用程序正在 Heroku 上运行,它正在连接到 DigitalOcean 服务器上的 MySQL 数据库。我们甚至可以为 Heroku 应用程序提供数据库并连接到该数据库。在*还有更多...*部分了解如何执行此操作。
还有更多...
- 向应用程序添加新的 DB 附加组件:
$ heroku addons:create jawsdb:kitefin
在这里,addons:create接受附加组件名称和服务计划名称,两者用冒号(:)分隔。您可以在elements.heroku.com/addons/jawsdb-maria上了解有关附加组件详细信息和计划的更多信息。此外,所有附加组件的附加组件详细信息页面末尾都提供了向应用程序添加附加组件的 Heroku CLI 命令。
- 打开 DB 仪表板以查看连接详细信息,如 URL、用户名、密码和数据库名称:
$ heroku addons:open jawsdb
jawsdb仪表板看起来与以下类似:
- 甚至可以从
JAWSDB_URL配置属性中获取 MySQL 连接字符串。您可以使用以下命令列出应用程序的配置:
$ heroku config
=== rest-demo-on-cloud Config Vars
JAWSDB_URL: <URL>
- 复制连接详细信息,在 MySQL Workbench 中创建一个新连接,并连接到此连接。数据库名称也是由附加组件创建的。连接到数据库后运行以下 SQL 语句:
use x81mhi5jwesjewjg;
create table person(
id int not null auto_increment,
first_name varchar(255),
last_name varchar(255),
place varchar(255),
primary key(id)
);
INSERT INTO person(first_name, last_name, place)
VALUES('Heroku First', 'Heroku Last', 'USA');
INSERT INTO person(first_name, last_name, place)
VALUES('Jaws First', 'Jaws Last', 'UK');
- 在
src/main/resources中为 Heroku 配置文件创建一个新的属性文件,名为application-heroku.properties,包含以下属性:
spring.datasource.url=jdbc:mysql://
<URL DB>:3306/x81mhi5jwesjewjg?useSSL=false
spring.datasource.username=zzu08pc38j33h89q
spring.datasource.password=<DB password>
您可以在附加仪表板中找到与连接相关的详细信息。
-
更新
src/main/resources/application.properties文件,将spring.profiles.active属性的值替换为heroku。 -
将更改提交并推送到 Heroku 远程:
$ git commit -am"using heroky mysql addon"
$ git push heroku master
- 部署成功后,运行
heroku open命令。页面在浏览器中加载后,单击Persons链接。这次,您将看到一组不同的数据,这是我们在 Heroku 附加组件中输入的数据:
[
{
"id":1,
"firstName":"Heroku First",
"lastName":"Heroku Last",
"place":"USA"
},
{
"id":2,
"firstName":"Jaws First",
"lastName":"Jaws Last",
"place":"UK"
}
]
通过这种方式,我们已经与在 Heroku 中创建的数据库集成。
使用 Docker 对 RESTful Web 服务进行容器化
我们已经从将应用程序安装在服务器上的时代发展到每个服务器都被虚拟化,然后应用程序安装在这些较小的虚拟机上。通过添加更多虚拟机来解决应用程序的可扩展性问题,使应用程序运行到负载均衡器上。
在虚拟化中,通过在多个虚拟机之间分配计算能力、内存和存储来将大型服务器划分为多个虚拟机。这样,每个虚拟机本身都能够像服务器一样完成所有这些任务,尽管规模较小。通过虚拟化,我们可以明智地利用服务器的计算、内存和存储资源。
然而,虚拟化需要一些设置,即您需要创建虚拟机,安装所需的依赖项,然后运行应用程序。此外,您可能无法 100%确定应用程序是否能够成功运行。失败的原因可能是由于不兼容的操作系统版本,甚至是由于在设置过程中遗漏了一些配置或缺少了一些依赖项。这种设置还会导致水平扩展方面的一些困难,因为在虚拟机的配置和部署应用程序方面需要花费一些时间。
使用 Puppet 和 Chef 等工具确实有助于配置,但是应用程序的设置往往会导致由于缺少或不正确的配置而出现问题。这导致了另一个概念的引入,称为容器化。
在虚拟化世界中,我们有主机操作系统,然后是虚拟化软件,也就是 hypervisor。然后我们会创建多个机器,每台机器都有自己的操作系统,可以在上面部署应用程序。然而,在容器化中,我们不会划分服务器的资源。相反,我们有带有主机操作系统的服务器,然后在其上方有一个容器化层,这是一个软件抽象层。我们将应用程序打包为容器,其中容器只打包了运行应用程序所需的足够操作系统功能、应用程序的软件依赖项,以及应用程序本身。以下图片最好地描述了这一点:docs.docker.com/get-started/#container-diagram。
前面的图片说明了典型的虚拟化系统架构。以下图片说明了典型的容器化系统架构:
容器化的最大优势在于将应用程序的所有依赖项捆绑到一个容器映像中。然后在容器化平台上运行此映像,从而创建一个容器。我们可以在服务器上同时运行多个容器。如果需要添加更多实例,我们只需部署映像,这种部署可以自动化以支持高可伸缩性。
Docker 是一种流行的软件容器化平台。在本示例中,我们将把位于Chapter09/6_boot_with_docker位置的示例应用程序打包成 Docker 映像,并运行 Docker 映像以启动我们的应用程序。
准备工作
对于此示例,我们将使用运行 Ubuntu 16.04.2 x64 的 Linux 服务器:
- 从
download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/下载最新的.deb文件。对于其他 Linux 发行版,您可以在download.docker.com/linux/找到软件包:
$ wget https://download.docker.com/linux/ubuntu/dists/xenial
/pool/stable/amd64/docker-ce_17.03.2~ce-0~ubuntu-xenial_amd64.deb
- 使用
dpkg软件包管理器安装 Docker 软件包:
$ sudo dpkg -i docker-ce_17.03.2~ce-0~ubuntu-xenial_amd64.deb
包的名称将根据您下载的版本而变化。
- 安装成功后,Docker 服务开始运行。您可以使用
service命令验证这一点:
$ service docker status
docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled;
vendor preset: enabled)
Active: active (running) since Fri 2017-07-28 13:46:50 UTC;
2min 3s ago
Docs: https://docs.docker.com
Main PID: 22427 (dockerd)
要 docker 化的应用程序位于Chapter09/6_boot_with_docker,在下载本书的源代码时可获得。
如何做...
- 在应用程序的根目录创建
Dockerfile,内容如下:
FROM ubuntu:17.10
FROM openjdk:9-b177-jdk
VOLUME /tmp
ADD target/boot_docker-1.0.jar restapp.jar
ENV JAVA_OPTS="-Dspring.profiles.active=cloud"
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -jar /restapp.jar" ]
- 运行以下命令使用我们在前面步骤中创建的
Dockerfile构建 Docker 映像:
$ docker build --tag restapp-image .
Sending build context to Docker daemon 18.45 MB
Step 1/6 : FROM ubuntu:17.10
---> c8cdcb3740f8
Step 2/6 : FROM openjdk:9-b177-jdk
---> 38d822ff5025
Step 3/6 : VOLUME /tmp
---> Using cache
---> 38367613d375
Step 4/6 : ADD target/boot_docker-1.0.jar restapp.jar
---> Using cache
---> 54ad359f53f7
Step 5/6 : ENV JAVA_OPTS "-Dspring.profiles.active=cloud"
---> Using cache
---> dfa324259fb1
Step 6/6 : ENTRYPOINT sh -c java $JAVA_OPTS -jar /restapp.jar
---> Using cache
---> 6af62bd40afe
Successfully built 6af62bd40afe
- 您可以使用以下命令查看已安装的映像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
restapp-image latest 6af62bd40afe 4 hours ago 606 MB
openjdk 9-b177-jdk 38d822ff5025 6 days ago 588 MB
ubuntu 17.10 c8cdcb3740f8 8 days ago 93.9 MB
您会看到还有 OpenJDK 和 Ubuntu 映像。这些是下载用于构建我们应用程序的映像的。首先列出的是我们的应用程序。
- 运行映像以创建包含我们正在运行的应用程序的容器:
docker run -p 8090:8080 -d --name restapp restapp-image
d521b9927cec105d8b69995ef6d917121931c1d1f0b1f4398594bd1f1fcbee55
在run命令之后打印的长字符串是容器的标识符。您可以使用前几个字符来唯一标识容器。或者,您可以使用容器名称restapp。
- 应用程序已经启动。您可以通过运行以下命令查看日志:
docker logs restapp
- 您可以使用以下命令查看已创建的 Docker 容器:
docker ps
前面命令的输出类似于以下内容:
- 您可以使用以下命令管理容器:
$ docker stop restapp
$ docker start restapp
应用程序运行后,打开http://<hostname>:8090/api/persons。
它是如何工作的...
通过定义Dockerfile来定义容器结构和其内容。Dockerfile遵循一种结构,其中每一行都是INSTRUCTION arguments的形式。有一组预定义的指令,即FROM、RUN、CMD、LABEL、ENV、ADD和COPY。完整的列表可以在docs.docker.com/engine/reference/builder/#from上找到。让我们看看我们定义的Dockerfile:
FROM ubuntu:17.10
FROM openjdk:9-b177-jdk
VOLUME /tmp
ADD target/boot_docker-1.0.jar restapp.jar
ENV JAVA_OPTS="-Dspring.profiles.active=cloud"
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -jar /restapp.jar" ]
使用FROM指令的前两行指定了我们的 Docker 镜像的基础镜像。我们使用 Ubuntu OS 镜像作为基础镜像,然后将其与 OpenJDK 9 镜像结合在一起。VOLUME指令用于指定镜像的挂载点。这通常是主机操作系统中的路径。
ADD指令用于将文件从源复制到工作目录下的目标目录。ENV指令用于定义环境变量。
ENTRYPOINT指令用于配置容器以作为可执行文件运行。对于此指令,我们传递一个参数数组,否则我们将直接从命令行执行。在我们的场景中,我们使用 bash shell 来运行java -$JAVA_OPTS -jar <jar name>。
一旦我们定义了Dockerfile,我们就指示 Docker 工具使用Dockerfile构建一个镜像。我们还使用--tag选项为镜像提供一个名称。构建我们的应用程序镜像时,它将下载所需的基础镜像,这在我们的情况下是 Ubuntu 和 OpenJDK 镜像。因此,如果列出 Docker 镜像,您将看到基础镜像以及我们的应用程序镜像。
这个 Docker 镜像是一个可重用的实体。如果我们需要更多的应用程序实例,我们可以使用docker run命令生成一个新的容器。当我们运行 Docker 镜像时,有多个选项,其中一个是-p选项,它将容器内的端口映射到主机操作系统。在我们的情况下,我们将 Spring Boot 应用程序的8080端口映射到主机操作系统的8090端口。
现在,要检查我们运行的应用程序的状态,我们可以使用docker logs restapp来检查日志。除此之外,docker工具支持多个命令。强烈建议运行docker help并探索支持的命令。
Docker 是 Docker 容器化平台背后的公司,它创建了一组基础镜像,可以用来创建容器。有用于 MySQL DB、Couchbase、Ubuntu 和其他操作系统的镜像。您可以在store.docker.com/上探索这些软件包。
使用 Micrometer 和 Prometheus 监控 Spring Boot 2 应用程序
监控和收集性能指标是应用程序开发和维护的重要部分。人们对内存使用情况、各个端点的响应时间、CPU 使用情况、机器负载、垃圾收集频率和暂停等指标感兴趣。有不同的方法来启用捕获指标,例如使用 Dropwizard Metrics(metrics.dropwizard.io/4.0.0/)或 Spring Boot 的度量框架。
Spring Boot 2 及更高版本中的代码仪器是使用一个名为 Micrometer 的库(micrometer.io/)来完成的。Micrometer 提供了一个供应商中立的代码仪器,这样您就可以使用任何监控工具,并且 Micrometer 以工具理解的格式提供度量数据。这就像 SLF4J 用于日志记录一样。它是对以供应商中立方式产生输出的度量端点的外观。
Micrometer 支持诸如 Prometheus (prometheus.io/)、Netflix Atlas (github.com/Netflix/atlas)、Datadog (www.datadoghq.com/)以及即将支持的 InfluxDB (www.influxdata.com/)、statsd (github.com/etsy/statsd)和 Graphite (graphiteapp.org/)等工具。使用早期版本的 Spring Boot,如 1.5,的应用程序也可以使用这个新的仪表化库,如*还有更多...*部分所示。
在本食谱中,我们将使用 Micrometer 来为我们的代码进行仪表化,并将指标发送到 Prometheus。因此,首先,我们将从准备工作部分开始设置 Prometheus。
准备工作
Prometheus (prometheus.io/)是一个监控系统和时间序列数据库,允许我们存储时间序列数据,其中包括应用程序随时间变化的指标,一种简单的可视化指标的方法,或者在不同指标上设置警报。
让我们执行以下步骤在我们的机器上运行 Prometheus(在我们的情况下,我们将在 Windows 上运行。类似的步骤也适用于 Linux):
-
从
github.com/prometheus/prometheus/releases/download/v2.3.2/prometheus-2.3.2.windows-amd64.tar.gz下载 Prometheus 分发版。 -
在 Windows 上使用 7-Zip (
www.7-zip.org/)将其提取到一个我们将称为PROMETHEUS_HOME的位置。 -
将
%PROMETHEUS_HOME%添加到您的 PATH 变量(在 Linux 上,它将是$PROMETHEUS_HOME到 PATH 变量)。 -
使用
prometheus --config "%PROMETHEUS_HOME%/prometheus.yml"命令运行 Prometheus。您将看到以下输出:
- 在浏览器中打开
http://localhost:9090,以查看 Prometheus 控制台。在空文本框中输入go_gc_duration_seconds,然后单击执行按钮以显示捕获的指标。您可以切换到图形版本的选项卡以可视化数据:
前面的指标是用于 Prometheus 本身的。您可以导航到http://localhost:9090/targets以查找 Promethues 监视的目标,如下所示:
当您在浏览器中打开http://localhost:9090/metrics时,您将看到当前时间点的指标值。没有可视化很难理解。这些指标在随时间收集并使用图表可视化时非常有用。
现在,我们已经启动并运行了 Prometheus。让我们启用 Micrometer 和以 Prometheus 理解的格式发布指标。为此,我们将重用本章中与数据库交互食谱中使用的代码。此食谱位于Chapter09/2_boot_db_demo。因此,我们将只需将相同的代码复制到Chapter09/7_boot_micrometer,然后增强部分以添加对 Micrometer 和 Prometheus 的支持,如下一节所示。
如何做...
- 更新
pom.xml以包括 Spring boot 执行器和 Micrometer Prometheus 注册表依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.0.6</version>
</dependency>
在 Spring Boot 2 及更高版本中,Micrometer 已配置了执行器,因此我们只需要将执行器作为依赖项添加,然后micrometer-registry-prometheus依赖项会生成一个 Prometheus 理解的指标表示。
- 当我们运行应用程序(一种方式是运行
mvn spring-boot:run)并打开执行器端点时,默认情况下将是<root_url>/actuator。我们会发现默认情况下有一些执行器端点可用,但 Prometheus 指标端点不是其中的一部分:
- 要在执行器中启用 Prometheus 端点,我们需要在
src/main/resources/application.properties文件中添加以下属性:
management.endpoints.web.exposure.include=prometheus
- 重新启动应用程序并浏览
http://localhost:8080/actuator/。现在,您会发现只有 Prometheus 端点可用:
- 打开
http://localhost:8080/actuator/prometheus以查看 Prometheus 理解的格式中的指标:
- 配置 Prometheus 以在特定频率下调用
http://localhost:8080/actuator/prometheus,可以进行配置。这可以通过在%PROMETHEUS_HOME%/prometheus.yml配置文件中在scrape_configs属性下更新新作业来完成:
- job_name: 'spring_apps'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
您会看到,默认情况下,有一个作业来抓取 Prometheus 指标本身。
- 重新启动 Prometheus 服务器并访问
http://localhost:9090/targets。您将看到一个新的部分spring_apps,其中包含我们添加的目标:
- 我们可以通过访问
http://localhost:9090/graph,在文本框中输入jvm_memory_max_bytes,然后单击执行来绘制从指标捕获的指标的图表:
因此,我们最终设置了在 Prometheus 中摄取指标并根据指标值创建图表。
它是如何工作的...
Spring Boot 提供了一个名为执行器的库,具有在部署到生产环境时帮助您监视和管理应用程序的功能。这种开箱即用的功能不需要开发人员进行任何设置。因此,您可以在没有任何工作的情况下进行审计、健康检查和指标收集。
如前所述,执行器使用 Micrometer 从代码中进行仪表化和捕获不同的指标,例如:
-
JVM 内存使用情况
-
连接池信息
-
应用程序中不同 HTTP 端点的响应时间
-
不同 HTTP 端点调用的频率
要使应用程序具有这些生产就绪的功能,如果您使用 Maven,需要将以下依赖项添加到您的pom.xml中(Gradle 也有相应的依赖项):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
默认情况下,执行器位于/actuator端点,但可以通过在src/main/resources/application.properties文件中覆盖management.endpoints.web.base-path属性来配置不同的值,如下所示:
management.endpoints.web.base-path=/metrics
默认情况下,除了/shutdown端点之外,所有用于监视和审计应用程序的端点都是默认启用的。默认情况下禁用此端点。此端点用于关闭应用程序。以下是一些可用的端点:
auditevents | 公开当前应用程序的审计事件信息 |
|---|---|
beans | 显示应用程序中所有 Spring bean 的完整列表 |
env | 公开 Spring 的ConfigurableEnvironment中的属性 |
health | 显示应用程序健康信息 |
info | 显示任意应用程序信息 |
metrics | 显示当前应用程序的指标信息 |
mappings | 显示所有@RequestMapping路径的汇总列表 |
prometheus | 以 Prometheus 服务器可以抓取的格式公开指标 |
您会发现这些是非常敏感的端点,需要进行安全保护。好消息是,Spring Boot 执行器与 Spring Security 很好地集成,以保护这些端点。因此,如果 Spring Security 在类路径上,它将默认安全地保护这些端点。
这些端点可以通过 JMX 或通过 Web 访问。默认情况下,并非所有执行器端点都启用了 Web 访问,而是默认情况下启用了使用 JMX 访问。默认情况下,只有以下属性可以通过 Web 访问:
-
health -
info
这就是我们不得不添加以下配置属性的原因,以便在 Web 上提供 Prometheus 端点,以及健康、信息和指标:
management.endpoints.web.exposure.include=prometheus,health,info,metrics
即使我们启用了 Prometheus,我们也需要在类路径上有micrometer-registry-prometheus库。只有这样,我们才能以 Prometheus 的格式查看指标。因此,我们将以下依赖项添加到我们的 pom 中:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.0.6</version>
</dependency>
Prometheus 处理的输出格式很简单:它以<property_name value>的形式接收,每个属性都在新的一行中。Spring Boot 执行器不会将指标推送到 Prometheus;相反,我们配置 Prometheus 以在其配置中定义的频率从给定 URL 拉取指标。Prometheus 的默认配置,可在其主目录中找到,如下所示:
# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
static_configs:
- targets: ['localhost:9090']
因此,它配置了 Prometheus 将获取指标的间隔和评估rule_files下定义的规则的间隔的默认值。Scrape 是从scrape_configs选项下定义的不同目标中拉取指标的活动,而 evaluate 是评估rule_files中定义的不同规则的行为。为了使 Prometheus 能够从我们的 Spring Boot 应用程序中抓取指标,我们通过提供作业名称、相对于应用程序 URL 的指标路径和应用程序的 URL,在scrape_configs下添加了一个新的作业:
- job_name: 'spring_apps'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
我们还看到了如何从http://localhost:9090/graph查看这些指标的值,以及如何使用 Prometheus 提供的简单图形支持来可视化这些指标。
还有更多
通过配置另一个名为 Alertmanager 的服务(prometheus.io/docs/alerting/alertmanager/),可以在 Prometheus 中启用警报。该服务可用于向电子邮件、寻呼机等发送警报。
Prometheus 中的图形支持是天真的。您可以使用 Grafana(grafana.com/),这是一种领先的开源软件,用于分析时间序列数据,例如存储在 Prometheus 中的数据。通过这种方式,您可以配置 Grafana 从 Prometheus 读取时间序列数据,并构建具有预定义指标的不同类型图表的仪表板。