1. 程式人生 > >Java之怎麼通過java去呼叫並執行shell指令碼以及問題總結(轉)

Java之怎麼通過java去呼叫並執行shell指令碼以及問題總結(轉)

背景
我們在開發過程中,大部分是java開發, 而在文字處理過程中,主要就是指令碼進行開發。 java開發的特點就是我們可以很早地進行TDDL, METAQ 等等地對接; 而指令碼開發的特點就是在進行批處理的時候非常方便。 前陣子我遇到這麼一個需求場景: 對抓取的資料進行打包, 後來又遇到我要通過指令碼進行抓取,比如nodejs下基於phantomjs的casperjs爬蟲。

解決方法
對於第一個問題:java抓取,並且把結果打包。那麼比較直接的做法就是,java接收各種訊息(db,metaq等等),然後藉助於jstorm叢集進行排程和抓取。 最後把抓取的結果儲存到一個檔案中,並且通過呼叫shell打包, 回傳。 也許有同學會問, 為什麼不直接把java呼叫odps直接儲存檔案,答案是,我們的叢集不是hz叢集,直接上傳odps速度很有問題,因此先打包比較合適。(這裡不糾結設計了,我們回到正題)

java呼叫shell的方法
通過ProcessBuilder進行排程
這種方法比較直觀,而且引數的設定也比較方便, 比如我在實踐中的程式碼(我隱藏了部分業務程式碼):

ProcessBuilderpb = new ProcessBuilder(“./” + RUNNING_SHELL_FILE, param1,
param2, param3);
pb.directory(new File(SHELL_FILE_DIR));
int runningStatus = 0;
String s = null;
try {
Process p = pb.start();
try {
runningStatus = p.waitFor();
} catch (InterruptedException e) {
}

    } catch (IOException e) {
    }
    if (runningStatus != 0) {
    }
    return;

這裡有必要解釋一下幾個引數:

RUNNING_SHELL_FILE:要執行的指令碼

SHELL_FILE_DIR:要執行的指令碼所在的目錄; 當然你也可以把要執行的指令碼寫成全路徑。

runningStatus:執行狀態,0標識正常。 詳細可以看java文件。

param1, param2, param3:可以在RUNNING_SHELL_FILE指令碼中直接通過1,2,$3分別拿到的引數。

直接通過系統Runtime執行shell
這個方法比較暴力,也比較常用, 程式碼如下:

p = Runtime.getRuntime().exec(SHELL_FILE_DIR + RUNNING_SHELL_FILE + ” “+param1+” “+param2+” “+param3);
p.waitFor();
我們發現,通過Runtime的方式並沒有builder那麼方便,特別是引數方面,必須自己加空格分開,因為exec會把整個字串作為shell執行。

可能存在的問題以及解決方法
如果你覺得通過上面就能滿足你的需求,那麼可能是要碰壁了。你會遇到以下情況。

沒許可權執行
這個情況我們團隊的朱東方就遇到了, 在做DTS遷移的過程中,要執行包裡面的shell指令碼, 解壓出來了之後,發現執行不了。 那麼就按照上面的方法授權吧

java進行一直等待shell返回
這個問題估計更加經常遇到。 原因是, shell指令碼中有echo或者print輸出, 導致緩衝區被用完了! 為了避免這種情況, 一定要把緩衝區讀一下, 好處就是,可以對shell的具體執行狀態進行log出來。 比如上面我的例子中我會變成:

ProcessBuilderpb = new ProcessBuilder(“./” + RUNNING_SHELL_FILE, keyword.trim(),
taskId.toString(), fileName);
pb.directory(new File(CASPERJS_FILE_DIR));
int runningStatus = 0;
String s = null;
try {
Process p = pb.start();
BufferedReaderstdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
BufferedReaderstdError = new BufferedReader(new InputStreamReader(p.getErrorStream()));
while ((s = stdInput.readLine()) != null) {
LOG.error(s);
}
while ((s = stdError.readLine()) != null) {
LOG.error(s);
}
try {
runningStatus = p.waitFor();
} catch (InterruptedException e) {
}
記得在start()之後, waitFor()之前把緩衝區讀出來打log, 就可以看到你的shell為什麼會沒有按照預期執行。 這個還有一個好處是,可以讀shell裡面輸出的結果, 方便java程式碼進一步操作。

也許你還會遇到這個問題,明明手工可以執行的命令,java呼叫的shell中某一些命令居然不能執行,報錯:命令不存在!

比如我在使用casperjs的時候,手工去執行shell明明是可以執行的,但是java呼叫的時候,發現總是出錯。 通過讀取緩衝區就能發現錯誤日誌了。 我發現即便自己把安裝的casperjs的bin已經加入了path中(/etc/profile, 各種bashrc中)還不夠。 比如:

exportNODE_HOME=”/home/admin/node”
exportCASPERJS_HOME=”/home/admin/casperjs”
exportPHANTOMJS_HOME=”/home/admin/phantomjs”
exportPATH=PATH:JAVA_HOME/bin:/root/bin:NODEHOME/bin:CASPERJS_HOME/bin:$PHANTOMJS_HOME/bin
原來是因為java在呼叫shell的時候,預設用的是系統的/bin/下的指令。特別是你用root許可權執行的時候。 這時候,你要在/bin下加軟鏈了。針對我上面的例子,就要在/bin下加軟鏈:

ln -s /home/admin/casperjs/bin/casperjscasperjs;
ln -s /home/admin/node/bin/nodenode;
ln -s /home/admin/phantomjs/bin/phantomjsphantomjs;
這樣,問題就可以解決了。

如果是通過java呼叫shell進行打包,那麼要注意路徑的問題了
因為shell裡面tar的壓縮和解壓可不能直接寫:

tar -zcf /home/admin/data/result.tar.gz /home/admin/data/result

直接給你報錯,因為tar的壓縮源必須到路徑下面, 因此可以寫成

tar -zcf /home/admin/data/result.tar.gz -C /home/admin/data/ result

如果我的shell是在jar包中怎麼辦?
答案是:解壓出來。再按照上面指示進行操作。(1)找到路徑

String jarPath = findClassJarPath(ClassLoaderUtil.class);
JarFiletopLevelJarFile = null;
try {
topLevelJarFile = new JarFile(jarPath);
Enumeration entries = topLevelJarFile.entries();
while (entries.hasMoreElements()) {
JarEntryentry = entries.nextElement();
if (!entry.isDirectory() && entry.getName().endsWith(“.sh”)) {
對你的shell檔案進行處理
}
}
對檔案處理的方法就簡單了,直接touch一個臨時檔案,然後把資料流寫入,程式碼:

FileUtils.touch(tempjline);
tempjline.deleteOnExit();
FileOutputStreamfos = new FileOutputStream(tempjline);
IOUtils.copy(ClassLoaderUtil.class.getResourceAsStream(r), fos);
fos.close();
有這個這個東東,相信大家會減少踩坑,而且大膽地使用java和指令碼之間的互動吧。 java可以呼叫shell,那麼shell再呼叫其他就方便了。 記得一點, 不要過度地依賴緩衝區進行執行緒之間的通訊。原因自己去學習吧。