最近在项目中需要将一个java工程打成一个jar包,并在运行jar包后启动通过java中的runtime类来启动一个nodejs的服务,在做的过程中遇到了一些不小的坑,下面就将其记录下来。
Runtime.class是java.lang包下的一个类,在开发Android过程中我们有时需要与jni进行交互,我们使用System.load来加载so库,其底层也是调用了Runtime中的loadLibrary0方法,这个和今天的没有太多关系就不讨论了。下面我们先来看一个简单的在使用Runtime执行命令行的代码:
@Test
public void test1() throws IOException, InterruptedException, ExecutionException{
Process process = Runtime.getRuntime().exec("cmd.exe /c dir");
Future future = executor.submit(new WatchProcess(process, WatchType.NORMAL));
future.get();
}
输出结果:
������ E �еľ�û�б�ǩ��
�������� 0007-F0C8
E:\MyEclipse2016\oa ��Ŀ¼
2016/12/30 ���� ���� 11:35 .
2016/12/30 ���� ���� 11:35 ..
2017/02/28 �ܶ� ���� 08:01 1,308 .classpath
2017/02/28 �ܶ� ���� 07:51 1,236 .project
2017/02/28 �ܶ� ���� 07:42 .settings
2016/12/30 ���� ���� 11:35 logs
2017/03/07 �ܶ� ���� 08:11 7,036 pom.xml
2016/12/31 ���� ���� 06:29 src
2017/02/28 �ܶ� ���� 07:42 target
3 ���ļ� 9,580 �ֽ�
6 ��Ŀ¼ 301,133,688,832 �����ֽ�
上面的代码很简单,我们通过runtime执行了一个dir的指令,打印当前目录的文件列表,结果由于编码的问题出现了乱码,但是我们得到了想要的结果。具体的指令我们下面介绍,这里先介绍runtime类。
在代码的最开始,我们通过Runtime.getRuntime()
方法获取了一个Runtime类的实例,我们进入Runtime的源码可以看见,这是一个静态方法可以获取一个单例的Runtime实例,源码如下:
java.lang.Runtime.class
line:58 private static Runtime currentRuntime = new Runtime();
line:91 public static Runtime getRuntime() {
return currentRuntime;
}
获取了实例后,我们调用Runtime的exec方法运行命令,这个方法有好几个重载的方法,源码如下:
line:419 public Process exec(String command) throws IOException {
return exec(command, null, null);
}
line:461 public Process exec(String command, String[] envp) throws IOException {
return exec(command, envp, null);
}
line:515 public Process exec(String command, String[] envp, File dir)
throws IOException {
if (command.length() == 0)
throw new IllegalArgumentException("Empty command");
StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}
...
方法不止上面的几个,我们就不一一列举了,第一个参数是我们执行的命令和参数,例如cmd.exe/nodepad.exe这些都是命令。第二个是当前运行进程的环境,第三个是工作目录。
当我们调用该方法后会返回一个Process的对象,该类是一个抽象类,为我们提供了几个方法来获取我们的我们进程的信息,如下:
介绍了上面的方法,那我们就来看一下如何打印我们的日志,并同时打印多个日志,此处有个坑,当我们使用BufferedReader的形式打印控制台信息,result = reader.readLine()) != null
这样的退出方式会有个问题,它会一直阻塞,直到进程结束,所以我们为了能正常的打印多个日志,需要在子集成中去打印,为了方便管理,我们使用一个线程池,具体方法如下所示:
//创建一个固定大小的线程池用于执行子进程
ExecutorService executor = Executors.newFixedThreadPool(3, new ExecutorFactory());
//我们为了能获取一些特征值时能返回,我们定义一个callable接口
class WatchProcess implements Callable<String> {
private WeakReference reference;
private WatchType type;
public WatchProcess(Process p, WatchType type) {
reference = new WeakReference(p);
this.type = type;
}
@Override
public String call() throws Exception {
BufferedReader reader = null;
Process process = reference.get();
if (process == null)
return "error";
switch (type) {
case ERROR:
reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
break;
case NORMAL:
reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
break;
default:
reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
break;
}
return logEvent(reader);
}
//如果不了解callable的可以先去看一下,该接口可以让我们将其传入线程次执行并返回一个结果。
//接下来就是打印数据
private String logEvent(BufferedReader reader) throws IOException{
String result = null;
W: while ((result = reader.readLine()) != null) {
System.out.println(result);
//这里定义一个结束flag
if (result.toLowerCase().contains("app is end")) {
result = "success";
break W;
}
}
reader.close();
return result;
}
//为了打印不同的输出等级,我们定义了一个枚举类,这个不重要
enum WatchType {
ERROR, NORMAL,BEGIN,OUTPUT;
}
//接下来我们就可以调用了,如前面代码所示,New一个callable放入线程池中
Future future = executor.submit(new WatchProcess(process, WatchType.NORMAL));
//该方法会阻塞知道有返回值
String result=future.get();
上面我们做了一最简单的例子,我们只是运行了一个简单的命令,程序结束后调用destory方法就可以结束了,但是我们需要另外起一个进程怎么办了?下面就先介绍一下在Window和Mac中如何运行命令。
在Window系统中:
我们经常通过在window的dos窗口中运行一些命令,在程序中我们可以使用Runtime来运行,如下:
cmd.exe /c 该命令的意识是使用cmd来执行指令 /c 后跟的就是我们的指令,例如前面的dir
在Mac系统中:
/bin/sh -c 该命令和window中的差不多,只是运行的环境不一样了
/bin/bash -c 该命令也可以执行
在我们运行的过程中有时候会提示我们找不到某指令的情况,此时我们需要确定我们已经配置了系统的环境变量,在Window中的PATH,Mac系统中的/etc/profile文件中的PATH路径已经配置好了。
注意:在之前的测试过程中出现过在Mac的控制台中可以运行命令,但是在Eclipse中却无法运行,我们使用System.getEnv()打印系统的环境变量,发现Eclipse中的PATH和Mac中使用echo $PATH
打印的不一样导致的问题,解决办法就是把$PATH
的值拷贝一份配置到Eclipse的环境变量中。
当我们明白上面这些后,我们分别启动一个nodejs的服务,如下:
Window:
cmd.exe /c node server.js
Mac:
/bin/sh -c node server.js
在测试的过程中如上代码无法运行,需要定义一个字符数组传入exec方法如下:
String[] cmds={"/bin/sh -c","node server.js"}
运行如上代码后我们的服务就启动起来了,此时,我们就会遇到一个问题,我们如何关闭这个服务了?destory方法是不行的,即使使用System.exit(0)退出进程也不行,我们还是可以在控制台看到服务,下面是几个通过端口查看进程和杀掉进程的方法:
Widnow:
查看端口被进程占用 netstat -ano|findstr portNum
杀掉指定的进程 taskkill /pid yourPid -t -f
Mac:
查看端口被进程占用 lsof -i tcp:portNum
杀掉指定的进程 kill -9 yourPid
我们杀掉进程的方法就和上面的指令有关,我们通过runtime运行如上命令,然后杀掉指定pid的进程,下面给出一个Window的方法,实现方法比较粗糙,可以优化下:
public static Integer killProcessByPort(Integer port){
Runtime runtime = Runtime.getRuntime();
Integer pid=null;
try {
Process process = runtime.exec("cmd.exe /c netstat -ano|findstr "+port);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String result = null;
W: while ((result = reader.readLine()) != null) {
Pattern pattern=Pattern.compile("[\\s]+(\\S+)");
Matcher matcher=pattern.matcher(result);
while(matcher.find()){
try {
pid = Integer.parseInt(matcher.group(1));
break W;
} catch (Exception e) {
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
if (pid!=null) {
try {
runtime.exec("cmd.exe /c taskkill /pid "+pid+" -t -f");
} catch (IOException e) {
e.printStackTrace();
}
}
return pid;
}
我们通过传入一个端口号然后杀掉进程,Mac的方式和这个一样,只是运行的命令不一样而已,注意下字符串的截取pid就行。下面介绍一个在Mac中的特别方法:
public void killProcess(Process process) {
if(process != null){
try {
Field field = process.getClass().getDeclaredField("pid");
field.setAccessible(true);
int pid = field.getInt(process);
pid+=1;
Runtime.getRuntime.exec("/bin/sh kill -9 "+pid);
return;
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
return;
}
}
这个方法在Mac上测试有效,可能我们发现在我们反射获得pid后进行了加1,这是通过测试发现获取到的进程是runtime.exec的返回的pid,但是启动服务的进程号比它大一,所以这么做(此方法不保证好使)。
还有一种在Window中的方法,但是实测没有通过所以放弃,而且需要导入别的东西:
try {
Field field = process.getClass().getDeclaredField("handle");
field.setAccessible(true);
Kernel32 kernel32 = Kernel32.INSTANCE;
WinNT.HANDLE handle = new WinNT.HANDLE();
handle.setPointer(Pointer.createConstant(field.getLong(process)));
int pid = kernel32.GetProcessId(handle);
RuntimeProcess killProcess = new RuntimeProcess("Taskkill /PID " + pid + " /F");
killProcess.isStdInput = false;
killProcess.execProcess();
return;
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
return;
}
以上就是全部内容。