7.2.2 Runtime类与Java9的ProcessHandle

7.2.2 Runtime类与Java9的ProcessHandle

Runtime类代表Java程序的运行时环境,每个Java程序都有一个与之对应的Runtime实例,应用程序通过该对象与其运行时环境相连。应用程序不能创建自己的Runtime实例,但可以通过getRuntime()方法获取与之关联的Runtime对象。

垃圾回收方法

System类似的是, Runtime类也

  • 提供gc()方法来通知系统进行垃圾回收方法
  • 提供runFinalization()方法用来通知系统调用finalize()方法来进行资源清理

加载文件和加载动态链接库方法

Runtime类提供load(String fileName)loadLibrary(String libName)方法来加载文件加载动态链接库

访问JVM相关信息

Runtime类代表Java程序的运行时环境,可以访问JVM的相关信息,如处理器数量内存信息

1
2
3
4
5
6
7
8
9
10
public class RuntimeTest {
public static void main(String[] args) {
// 获取Java程序关联的运行时对象
Runtime rt = Runtime.getRuntime();
System.out.println("处理器数量:" + rt.availableProcessors());
System.out.println("空闲内存数:" + rt.freeMemory());
System.out.println("总内存数:" + rt.totalMemory());
System.out.println("可用最大内存数:" + rt.maxMemory());
}
}

运行结果:

1
2
3
4
处理器数量:4
空闲内存数:126927264
总内存数:128974848
可用最大内存数:1884815360

启动进程运行命令

Runtime类还可以直接单独启动一个进程来运行操作系统的命令Runtime提供了一系列exec()方法来运行操作系统命令。

实例

启动Windows系统里的”记事本”程序。

1
2
3
4
5
6
7
public class ExecTest {
public static void main(String[] args) throws Exception {
Runtime rt = Runtime.getRuntime();
// 运行记事本程序
rt.exec("notepad.exe");
}
}

运行效果:记事本被打开

获取exec启动的进程信息

获取Process对象

通过exec启动平台上的命令之后,它就变成了一个进程,javaProcess来代表进程,可以使用Process来接收exec方法的返回值。
Java9还新增了一个ProcessHandle接口,通过该接口可获取进程的ID父进程后代进程;通过该接口的onExit()方法可在进程结束时完成某些行为.
ProcessHandle还提供了一个ProcessHandle.Info内部类,用于获取进程的命令、参数、启动时间、累计运行时间、用户等信息。

如何获取ProcessHandle对象

通过Process对象的方法toHandle()可以取得ProcessHandle对象,然后就可以通过ProcessHandle对象来获取进程相关信息。

下面程序示范了通过ProcessHandle获取进程的相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

import java.util.concurrent.*;

public class ProcessHandleTest {
public static void main(String[] args) throws Exception {
Runtime rt = Runtime.getRuntime();
// 运行记事本程序
Process p = rt.exec("notepad.exe");
ProcessHandle ph = p.toHandle();
System.out.println("进程是否运行: " + ph.isAlive());
System.out.println("进程ID: " + ph.pid());
System.out.println("父进程: " + ph.parent());
// 获取ProcessHandle.Info信息
ProcessHandle.Info info = ph.info();
// 通过ProcessHandle.Info信息获取进程相关信息
System.out.println("进程命令: " + info.command());
System.out.println("进程参数: " + info.arguments());
System.out.println("进程启动时间: " + info.startInstant());
System.out.println("进程累计运行时间: " + info.totalCpuDuration());
// 通过CompletableFuture在进程结束时运行某个任务
CompletableFuture<ProcessHandle> cf = ph.onExit();
cf.thenRunAsync(() -> {
System.out.println("程序退出");
});
Thread.sleep(5000);
}
}

实例 执行进程并返回进程的输出

被调用的进程

下面创建一个PrintArgs程序,用来给其他进程调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
package system.test;

public class PrintArgs {
public static void main(String args[]) {
// 获取类名
String simpleName = PrintArgs.class.getSimpleName();
System.out.println("----- " + simpleName + " start -----------------------------------");
for (int i = 0; i < args.length; i++) {
System.out.println("[args-" + i + "]:" + args[i]);
}
System.out.println("----- " + simpleName + " end -----------------------------------");
}
}

编译 打包 测试

先在当前目录下编译这个java文件:

1
E:\dev2\idea_workspace\Test\src\system\test>javac -d . -encoding utf-8 PrintArgs.java

然后打包成可执行jar包:

1
2
3
4
5
E:\dev2\idea_workspace\Test\src\system\test>jar cvfe PrintArgs.jar system.test.PrintArgs system
已添加清单
正在添加: system/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: system/test/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: system/test/PrintArgs.class(输入 = 1094) (输出 = 620)(压缩了 43%)

接着,查看jar包是否打包正确

1
2
3
4
5
6
E:\dev2\idea_workspace\Test\src\system\test>jar tf PrintArgs.jar
META-INF/
META-INF/MANIFEST.MF
system/
system/test/
system/test/PrintArgs.class

最后,运行这个可执行jar包

1
2
3
4
5
6
7
8
E:\dev2\idea_workspace\Test\src\system\test>java -jar PrintArgs.jar 1 2 3
----- PrintArgs start -----------------------------------
工作目录:E:\dev2\idea_workspace\Test\src\system\test
[args-0]:1
[args-1]:2
[args-2]:3
----- PrintArgs end -----------------------------------

调用进程

可以看到通过命令java -jar PrintArgs.jar 1 2 3可以正确运行这个jar包,接下来就是,通过Process来执行这个命令。
创建Process有如下两种方式:

  1. 通过Runtime.exec("cmd命令")来运行一个进程.
  2. 通过ProcessBuilder对象的start方法来运行一个进程.

现在

工具类 运行进程并返回进程的标准输出

注意获取输出的时候要注意控制台的编码,我的控制台默认编码是gbk.如果是其他编码则需要指定编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package system.test;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/**
* 进程执行器.
*/
public class ProcessRunner {
/**
* 运行cmd命令,默认cmd的编码为gbk.
*
* @param commandStr cmd命令.cmd命令以空格作为分隔符,第一个参数表示要执行的程序,后面的的参数是该程序的命令行参数.
* @return 程序的标准输出.
*/
public String runProcess(String commandStr) {
return runProcess(commandStr, "gbk");
}

/**
* @param commandStr cmd命令.cmd命令以空格作为分隔符,第一个参数表示要执行的程序,后面的的参数是该程序的命令行参数.
* @param cmdEncoding cmd的编码.
* @return 程序的标准输出.
*/
public String runProcess(String commandStr, String cmdEncoding) {
BufferedReader br = null;
StringBuilder sb = new StringBuilder();
try {
Process p = Runtime.getRuntime().exec(commandStr);
br = new BufferedReader(new InputStreamReader(p.getInputStream(), cmdEncoding));
String line;
while ((line = br.readLine()) != null) {
sb.append(line.concat("\n"));
}
// 等待进程运行结束.
int exitCode = p.waitFor();
// 如果进程的返回值不是0,则表明进程执行失败.
if (exitCode != 0)
// 返回null表示程序执行失败.
return null;

} catch (Exception e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return sb.toString();
}

/**
* 执行进程生成器中的程序和参数,并返回程序的输出.默认cmd的编码为gbk.
*
* @param processBuilder
* @return 程序执行的结果字符串.
*/
public String runProcess(ProcessBuilder processBuilder) {
return runProcess(processBuilder, "gbk");
}

/**
* 执行进程生成器中的程序和参数,并返回程序的输出.
*
* @param processBuilder 进程生成器.进程生成器中存放了要执行的程序,该程序的参数,该程序的工作空间等.
* @param cmdEncoding cmd的编码.程序将按照这个编码来读取程序的标准输出。
* @return 程序执行的结果字符串.
*/
public String runProcess(ProcessBuilder processBuilder, String cmdEncoding) {
Process process = null;
StringBuffer sb = new StringBuffer();
try {
process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), cmdEncoding));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line + "\n");
}
// 等待进程运行结束
int exitCode = process.waitFor();
// 进程结束了,关闭接收流.
reader.close();
// 如果线程返回值不是0则表示线程执行失败.
if (exitCode != 0)
return null;

} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return sb.toString();
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package system.test;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class ProcessTest {
public static void main(String[] args) {
// 测试通过Runable来执行一个进程.
testRuntime();

// 测试通过ProcessBuilder来执行一个进程.
testProcessBuilder();
}

private static void testRuntime() {
// Runtime启动的进程的工作目录和父进程一样
String jarPath = "E:\\dev2\\idea_workspace\\Test\\src\\system\\test\\PrintArgs.jar";
System.out.println(new ProcessRunner().runProcess("java -jar " + jarPath + " 1 2 3"));
}

private static void testProcessBuilder() {
List<String> command = new ArrayList<>();
command.add("java");
command.add("-jar");
command.add("PrintArgs.jar");
command.add("1");
command.add("2");
command.add("3");
// java -jar PrintArgs.jar 1 2 3
ProcessBuilder processBuilder = new ProcessBuilder(command);
// ProcessBuilder方法启动的进程可以指定工作目录,如果不指定则和父进程的工作目录相同
processBuilder.directory(new File("E:\\dev2\\idea_workspace\\Test\\src\\system\\test"));
String processOutput = new ProcessRunner().runProcess(processBuilder);
System.out.println(processOutput);
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
----- PrintArgs start -----------------------------------
工作目录:E:\dev2\idea_workspace\Test
[args-0]:1
[args-1]:2
[args-2]:3
----- PrintArgs end -----------------------------------
----- PrintArgs start -----------------------------------
工作目录:E:\dev2\idea_workspace\Test\src\system\test
[args-0]:1
[args-1]:2
[args-2]:3
----- PrintArgs end -----------------------------------

Runtime方式和ProcessBuilder方式的对比

ProcessBuilder设置命令比较方便

  • Runtime方式需要把要指定的cmd命令,全部写在一个字符串里,如果命令比较长则容易出现错误.
  • ProcessBuilder方式设置命令比较简单,命令写在List中或者字符串变参中,写起来比较方便,不容易出现错误.

ProcessBuilder可以指定工作目录

  • Runtime方式的工作目录默认与父进程相同,运行程序时,一般需要输入程序的绝对路径,比较繁琐.
  • ProcessBuilder方式可以指定工作目录,这样可执行程序的路径可以使用相对路径

JDK1.5之后推荐使用ProcessBuilder方式来运行进程