原文链接:https://blog.csdn.net/u014628771/article/details/108308337
需求
在Linux服务器上的SpringBoot程序中,调用git clone,之后遍历git仓库中的所有文件。
遍历git仓库可以使用File类实现,现在的问题是需要在SpringBoot程序中调用git clone命令。
实现方式
使用Java native的Process类和ProcessBuilder类来执行命令行。
话不多说,直接上代码(解释都在注解中)
关键代码
注意,这段代码实际上有几个坑,不建议直接复制使用(优化后的代码在文章末尾,建议直接复制使用),具体请看下文
private String executeGitClone() {
Process p = null;
// git clone命令
String cmd = "git clone https://{username}:{pwd}@github.com/{group}/{repo}.git";
try {
// 起子进程执行cmd命令
ProcessBuilder pb = new ProcessBuilder(cmd);
p = pb.start();
// 等待命令执行结束
int exitValue = p.waitFor();
// 创建readers, resReader用于读取标准输出,errReader用于读取错误输出
BufferedReader resReader = new BufferedReader(new InputStreamReader((p.getInputStream())));
BufferedReader errReader = new BufferedReader(new InputStreamReader((p.getErrorStream())));
StringBuilder resStringBuilder = new StringBuilder();
StringBuilder errStringBuilder = new StringBuilder();
String line;
while ((line = resReader.readLine()) != null) {
resStringBuilder.append(line);
}
while ((line = errReader.readLine()) != null) {
errStringBuilder.append(line);
}
// linux标准, exitValue为0时,表示执行正确结束
// 当exitValue > 0时,抛出异常,并将获取的错误信息包装在Exception中
if (exitValue > 0) {
throw new RuntimeException(errStringBuilder.toString());
}
// 返回标准输出
return resStringBuilder.toString();
} catch (Exception e) {
throw new CodeSearchException(e, SYSTEM_COMMAND_ERROR, e.getMessage());
} finally {
// 销毁子进程,释放资源
if (p != null) {p.destroy();}
}
}
需要注意的坑&问题
进程阻塞
代码中的顺序是,先执行命令,等命令执行结束后。
再读取标准输出流和错误输出流中的内容。
但是,标准输出流和错误输出流其实是先输出到缓冲区,当缓冲区写满时,进程会被挂起。
所以实际上,我们应该在执行pb.start()之后就启动两个线程,异步地读取标准输入流和错误输入流中的内容。
执行参数无效
执行进程还有更源生的方式,那就是使用Runtime.getRuntime().exec()的方式来执行,
ProcessBuilder实际上是封装过的类,如果使用Runtime来执行的话,给命令传多个参数会有问题。
使用ProcessBuilder,并将参数置于入参数组中即可。
不够优雅:起两个线程来读取返回结果(或者是上述代码中启动两个while循环),代码看起来不太优雅
改善&&结合SpringBoot
我们将“执行命令行”封装成一个Spring中的service,入参是命令,当执行顺利时返回标准输入流中的结果,当执行失败时抛出异常。
并使用线程池来用线程读取流中的数据。
// 定义Command Srevice接口
public interface CommandService {
String executeCmd(String cmd);
}
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* @version : CommandServiceImpl.java, v 0.1 2020年08月05日 11:38 AM Exp $
*/
@Service
public class CommandServiceImpl implements CommandService, InitializingBean {
@Value("${cmd.threadname:cmd-executor}")
private String threadName;
@Value("${cmd.taskQueueMaxStorage:20}")
private Integer taskQueueMaxStorage;
@Value("${cmd.corePoolSize:4}")
private Integer corePoolSize;
@Value("${cmd.maximumPoolSize:8}")
private Integer maximumPoolSize;
@Value("${cmd.keepAliveSeconds:15}")
private Integer keepAliveSeconds;
private ThreadPoolExecutor executor;
private static final String BASH = "sh";
private static final String BASH_PARAM = "-c";
// use thread pool to read streams
@Override
public void afterPropertiesSet() {
executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveSeconds, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(taskQueueMaxStorage),
new ThreadFactory() {
public Thread newThread(Runnable r) {
return new Thread(r, threadName + r.hashCode());
}
},
new ThreadPoolExecutor.AbortPolicy());
}
@Override
public String executeCmd(String cmd) {
Process p = null;
String res;
try {
// need to pass command as bash's param,
// so that we can compatible with commands: "echo a >> b.txt" or "bash a && bash b"
List<String> cmds = new ArrayList<>();
cmds.add(BASH);
cmds.add(BASH_PARAM);
cmds.add(cmd);
ProcessBuilder pb = new ProcessBuilder(cmds);
p = pb.start();
Future<String> errorFuture = executor.submit(new ReadTask(p.getErrorStream()));
Future<String> resFuture = executor.submit(new ReadTask(p.getInputStream()));
int exitValue = p.waitFor();
if (exitValue > 0) {
throw new RuntimeException(errorFuture.get());
}
res = resFuture.get();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (p != null) {p.destroy();}
}
// remove System.lineSeparator() (actually it's '\n') in the end of res if exists
if (StringUtils.isNotBlank(res) && res.endsWith(System.lineSeparator())) {
res = res.substring(0, res.lastIndexOf(System.lineSeparator()));
}
return res;
}
class ReadTask implements Callable<String> {
InputStream is;
ReadTask(InputStream is) {
this.is = is;
}
@Override
public String call() throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}
}