java 执行shell命令及日志收集避坑指南( 三 )

<>();/*** 主动读取进程的标准输出信息日志** @param process 进程实体* @param outputCharset 日志字符集* @throws IOException 读取异常时抛出*/private static void readProcessOutLogStream(Process process,Charset outputCharset) throws IOException {try (BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream(), outputCharset))) {Thread parentThread = Thread.currentThread();// 另起一个线程读取错误消息 , 必须先启该线程errReadThreadPool.submit(() -> {try {try (BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream(), outputCharset))) {String err;while ((err = stdError.readLine()) != null) {log.error("【cli】{}", err);setProcessLastError(parentThread, err);}}}catch (IOException e) {log.error("读取进程错误日志输出时发生了异常", e);setProcessLastError(parentThread, e.getMessage());}});// 外部线程读取标准输出消息String stdOut;while ((stdOut = stdInput.readLine()) != null) {log.info("【cli】{}", stdOut);}}}/*** 新建一个进程错误信息容器** @param logFilePath 日志文件路径,如无则为 null*/private static void initErrorLogHolder(String logFilePath, Charset outputCharset) {lastErrorHolder.put(Thread.currentThread(),new ProcessErrorLogDescriptor(logFilePath, outputCharset));}/*** 移除错误日志监听*/private static void removeErrorLogHolder() {lastErrorHolder.remove(Thread.currentThread());}/*** 获取进程的最后错误信息**注意: 该方法只会在父线程中调用*/private static String getProcessLastError() {Thread thread = Thread.currentThread();return lastErrorHolder.get(thread).getLastError();}/*** 设置最后一个错误信息描述**使用当前线程或自定义*/private static void setProcessLastError(String lastError) {lastErrorHolder.get(Thread.currentThread()).setLastError(lastError);}private static void setProcessLastError(Thread thread, String lastError) {lastErrorHolder.get(thread).setLastError(lastError);}/*** 判断当前系统是否是 windows*/public static boolean isWindowsSystemOs() {return System.getProperty("os.name").toLowerCase().startsWith("win");}/*** 进程错误信息描述封装类*/private static class ProcessErrorLogDescriptor {/*** 错误信息记录文件*/private String logFile;/*** 最后一行错误信息*/private String lastError;private Charset charset;ProcessErrorLogDescriptor(String logFile, Charset outputCharset) {this.logFile = logFile;charset = outputCharset;}String getLastError() {if(lastError != null) {return lastError;}try{if(logFile == null) {return null;}List lines = FileUtils.readLines(new File(logFile), charset);StringBuilder sb = new StringBuilder();for (int i = lines.size() - 1; i >= 0; i--) {sb.insert(0, lines.get(i) + "\n");if(sb.length() > 200) {break;}}return sb.toString();}catch (Exception e) {log.error("【cli】读取最后一次错误信息失败", e);}return null;}void setLastError(String err) {if(lastError == null) {lastError = err;return;}lastError = lastError + "\n" + err;if(lastError.length() > 200) {lastError = lastError.substring(lastError.length() - 200);}}}}以上实现 , 完成了我们在第2点中讨论的几个问题:
1. 主要使用 ProcessBuilder 完成了shell的调用;2. 支持读取进程的所有输出信息 , 且在必要的时候 , 支持使用单独的文件进行接收输出日志;3. 在进程执行异常时 , 支持抛出对应异常 , 且给出一定的errMessage描述;4. 如果想控制调用进程的数量 , 则在外部调用时控制即可;5. 使用两个线程接收两个输出流 , 避免出现应用假死 , 使用newCachedThreadPool线程池避免过快创建线程;
接下来 , 我们进行下单元测试:
public class ShellCommandExecUtilTest {@Testpublic void testRuntimeShell() throws IOException {int errCode;errCode = ShellCommandExecUtil.runShellWithRuntime("E:\\tmp",new String[] {"cmd", "/c", "dir"}, Charset.forName("gbk"));Assert.assertEquals("进程返回码不正确", 0, errCode);}@Test(expected = ShellProcessExecException.class)public void testRuntimeShellWithErr() throws IOException {int errCode;errCode = ShellCommandExecUtil.runShellWithRuntime("E:\\tmp",new String[] {"cmd", "/c", "dir2"}, Charset.forName("gbk"));Assert.fail("dir2 应该要执行失败 , 但却通过了 , 请查找原因");}@Testpublic void testProcessShell1() throws IOException {int errCode;errCode = ShellCommandExecUtil.runShellCommandSync("/tmp",new String[]{"cmd", "/c", "dir"}, Charset.forName("gbk"));Assert.assertEquals("进程返回码不正确", 0, errCode);String logPath = "/tmp/cmd.log";errCode = ShellCommandExecUtil.runShellCommandSync("/tmp",new String[]{"cmd", "/c", "dir"}, Charset.forName("gbk"), logPath);Assert.assertTrue("结果日志文件不存在", new File(logPath).exists());}@Test(expected = ShellProcessExecException.class)public void testProcessShell1WithErr() throws IOException {int errCode;errCode = ShellCommandExecUtil.runShellCommandSync("/tmp",new String[]{"cmd", "/c", "dir2"}, Charset.forName("gbk"));Assert.fail("dir2 应该要执行失败 , 但却通过了 , 请查找原因");}@Test(expected = ShellProcessExecException.class)public void testProcessShell1WithErr2() throws IOException {int errCode;String logPath = "/tmp/cmd2.log";try {errCode = ShellCommandExecUtil.runShellCommandSync("/tmp",new String[]{"cmd", "/c", "dir2"}, Charset.forName("gbk"), logPath);}catch (ShellProcessExecException e) {e.printStackTrace();throw e;}Assert.assertTrue("结果日志文件不存在", new File(logPath).exists());}}