כאשר Runtime.exec () לא

כחלק משפת Java, java.langהחבילה מיובאת באופן מרומז לכל תוכנית Java. מלכודות החבילה הזו עולות לעיתים קרובות ומשפיעות על מרבית המתכנתים. החודש אדון במלכודות האורבות Runtime.exec()בשיטה.

מלכודת 4: כאשר Runtime.exec () לא

הכיתה java.lang.Runtimeכוללת שיטה סטטית הנקראת getRuntime(), המאחזרת את סביבת Java Runtime הנוכחית. זו הדרך היחידה להשיג התייחסות Runtimeלאובייקט. עם התייחסות זו, תוכל להפעיל תוכניות חיצוניות על ידי הפעלת שיטת Runtimeהכיתה exec(). מפתחים קוראים לרוב לשיטה זו כדי להפעיל דפדפן להצגת דף עזרה ב- HTML.

ישנן ארבע גרסאות עמוסות של exec()הפקודה:

  • public Process exec(String command);
  • public Process exec(String [] cmdArray);
  • public Process exec(String command, String [] envp);
  • public Process exec(String [] cmdArray, String [] envp);

עבור כל אחת משיטות אלה מועברת פקודה - ואולי מערכת ארגומנטים - לשיחת פונקציה ספציפית למערכת ההפעלה. זה יוצר בהמשך תהליך ספציפי למערכת ההפעלה (תוכנית פועלת) עם התייחסות Processלמחלקה שהוחזרה ל- VM VM. Processבכיתה הוא המעמד המופשט, כי תת מחלקה ספציפית של Processקיימת עבור כל מערכת הפעלה.

ניתן להעביר שלושה פרמטרי קלט אפשריים לשיטות הבאות:

  1. מחרוזת יחידה המייצגת הן את התוכנית לביצוע והן את כל הטיעונים לתכנית זו
  2. מערך מחרוזות המפריד בין התוכנית לטיעוניה
  3. מערך משתני סביבה

העבירו את משתני הסביבה בצורה name=value. אם אתה משתמש בגרסה של exec()עם מחרוזת יחידה הן עבור התוכנית והן עבור הטיעונים שלה, שים לב שהמחרוזת מנותחת באמצעות רווח לבן כמפריד דרך StringTokenizerהמחלקה.

נקלע לתפיסה לא חוקית של ThreadState

המלכודת הראשונה שמתייחסת אליה Runtime.exec()היא IllegalThreadStateException. המבחן הראשון הנפוץ של API הוא קידוד השיטות הברורות ביותר שלו. לדוגמא, כדי לבצע תהליך שהוא חיצוני ל- Java VM, אנו משתמשים exec()בשיטה. כדי לראות את הערך שהתהליך החיצוני מחזיר, אנו משתמשים exitValue()בשיטה Processבכיתה. בדוגמה הראשונה שלנו, ננסה לבצע את מהדר Java ( javac.exe):

רישום 4.1 BadExecJavac.java

ייבא java.util. *; ייבא java.io. *; מחלקה ציבורית BadExecJavac {ציבורי ריק סטטי ראשי (String args []) {try {Runtime rt = Runtime.getRuntime (); תהליך proc = rt.exec ("javac"); int exitVal = proc.exitValue (); System.out.println ("Process exitValue:" + exitVal); } לתפוס (ניתן לזרוק) {t.printStackTrace (); }}}

ריצת BadExecJavacתוצרת:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java BadExecJavac java.lang.IllegalThreadStateException: התהליך לא יצא ב- java.lang.Win32Process.exitValue (שיטה מקורית) ב- BadExecJavac.main (BadExecJavac.java:13) 

אם טרם הושלם תהליך חיצוני, exitValue()השיטה תזרוק IllegalThreadStateException; לכן התוכנית הזו נכשלה. בעוד שהתיעוד מציין עובדה זו, מדוע שיטה זו אינה יכולה לחכות עד שתוכל לתת תשובה תקפה?

מבט מעמיק יותר על השיטות הקיימות Processבכיתה מגלה waitFor()שיטה שעושה בדיוק את זה. למעשה, waitFor()גם מחזיר את ערך היציאה, מה שאומר שאתה לא תשתמש exitValue()ו waitFor()בשיתוף עם שני, אלא יבחר אחד מהשניים. הפעם היחידה האפשרית שתשתמש בה exitValue()במקום waitFor()תהיה כאשר אינך מעוניין שהתוכנית שלך תחסום המתנה בתהליך חיצוני שאולי לעולם לא יושלם. במקום להשתמש waitFor()בשיטה, היה מעדיף עובר פרמטר בוליאני בשם waitForלתוך exitValue()השיטה כדי לקבוע אם או לא החוט הנוכחי צריך לחכות. בוליאני יהיה מועיל יותר בגללexitValue()הוא שם מתאים יותר לשיטה זו, ואין צורך בשתי שיטות לבצע את אותה פונקציה בתנאים שונים. אפליה כה פשוטה של ​​תנאים היא נחלתו של פרמטר קלט.

לכן, כדי להימנע ממלכודת זו, תפוס את IllegalThreadStateExceptionאו המתן עד לסיום התהליך.

עכשיו, בואו נתקן את הבעיה ברישום 4.1 ונחכה להשלמת התהליך. ברשימה 4.2 התוכנית מנסה שוב לבצע javac.exeואז ממתינה להשלמת התהליך החיצוני:

רישום 4.2 BadExecJavac2.java

ייבא java.util. *; ייבא java.io. *; class public BadExecJavac2 {public static void main (String args []) {try {Runtime rt = Runtime.getRuntime (); תהליך proc = rt.exec ("javac"); int exitVal = proc.waitFor (); System.out.println ("Process exitValue:" + exitVal); } לתפוס (ניתן לזרוק) {t.printStackTrace (); }}}

למרבה הצער, ריצה של BadExecJavac2לא מייצרת תפוקה. התוכנית תלויה ואף פעם לא מסתיימת. מדוע javacהתהליך לעולם אינו מסתיים?

מדוע Runtime.exec () נתקע

תיעוד ה- Javadoc של ה- JDK מספק את התשובה לשאלה זו:

מכיוון שחלק מהפלטפורמות המקוריות מספקות רק גודל חיץ מוגבל לזרמי קלט ופלט סטנדרטיים, כישלון בכתיבת זרם הקלט באופן מיידי או קריאת זרם הפלט של תת התהליך עלול לגרום לחסימת תהליך המשנה ואף למבוי סגור.

האם זה רק מקרה של מתכנתים שלא קוראים את התיעוד, כמשתמע מהעצה שמצוטטת לעתים קרובות: קרא את המדריך המשובח (RTFM)? התשובה היא חלקית חיובית. במקרה זה, קריאת ה- Javadoc תביא אותך באמצע הדרך; זה מסביר שאתה צריך לטפל בזרמים לתהליך החיצוני שלך, אבל זה לא אומר לך איך.

משתנה נוסף משחק כאן, כפי שעולה ממספר רב של שאלות מתכנתים ותפיסות מוטעות הנוגעות ל- API זה בקבוצות הדיון: אם כי Runtime.exec()ממשקי ה- API של התהליך נראים פשוטים ביותר, שהפשטות היא מטעה מכיוון שהשימוש הפשוט או הברור בממשק ה- API נוטה לטעויות. הלקח כאן עבור מעצב ה- API הוא שמירת ממשקי API פשוטים לפעולות פשוטות. פעולות המועדות למורכבויות ותלות ספציפית לפלטפורמה צריכות לשקף את התחום בצורה מדויקת. ייתכן שההפשטה תישא רחוק מדי. JConfigהספרייה מספקת דוגמא API השלמה יותר לפעולות קובץ תהליך ידית (ראה להלן משאבים למידע נוסף).

עכשיו, בואו נעקוב אחר תיעוד ה- JDK ונטפל בפלט javacהתהליך. כאשר אתה פועל javacללא טיעונים, הוא מייצר קבוצה של הצהרות שימוש המתארות כיצד להפעיל את התוכנית ואת המשמעות של כל אפשרויות התוכנית הזמינות. בידיעה שזה הולך stderrלזרם, אתה יכול בקלות לכתוב תוכנית כדי למצות את הזרם לפני שתמתין לסיום התהליך. רישום 4.3 משלים את המשימה. גישה זו אמנם תפעל, אך אין זה פיתרון כללי טוב. לפיכך, התוכנית של Listing 4.3 נקראת MediocreExecJavac; הוא מספק פיתרון בינוני בלבד. פתרון טוב יותר ירוקן הן את זרם השגיאות הסטנדרטי והן את זרם הפלט הסטנדרטי. והפתרון הטוב ביותר ירוקן את הזרמים הללו בו זמנית (אדגים זאת בהמשך).

רישום 4.3 MediocreExecJavac.java

ייבא java.util. *; ייבא java.io. *; class class MediocreExecJavac {main public static void (String args []) {try {Runtime rt = Runtime.getRuntime (); תהליך proc = rt.exec ("javac"); InputStream stderr = proc.getErrorStream (); InputStreamReader isr = InputStreamReader חדש (stderr); BufferedReader br = BufferedReader חדש (isr); קו מחרוזת = null; System.out.println (""); בעוד ((line = br.readLine ())! = null) System.out.println (שורה); System.out.println (""); int exitVal = proc.waitFor (); System.out.println ("Process exitValue:" + exitVal); } לתפוס (ניתן לזרוק) {t.printStackTrace (); }}}

ריצה של MediocreExecJavacמחולל:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java MediocreExecJavac שימוש: javac איפה כולל: -g צור את כל המידע על ניפוי באגים -g: אין צור מידע על ניפוי באגים -g: {lines, vars, source} צור רק מידע על ניפוי באגים -O אופטימיזציה; עלול להפריע לניפוי באגים או להגדיל קבצי מחלקה -להזהר לא ליצור אזהרות -להרחיב הודעות פלט אודות מה שהמהדר עושה -התאוששות מיקומי מקור פלט בהם משתמשים ב- APIs שהוצאו משימוש - classpath ציין היכן למצוא קבצי מחלקה של משתמשים -bootclasspath לעקוף מיקום של קבצי מחלקת bootstrap -extdirs לבטל מיקום של סיומות מותקנות -d ציין היכן למקם קבצי מחלקה שנוצרו -קידוד ציין קידוד תווים המשמש קבצי מקור-target צור קבצי מחלקה עבור גרסת VM ספציפית יציאת תהליך ערך: 2

אז, MediocreExecJavacעובד ומייצר ערך יציאה של 2. בדרך כלל, ערך יציאה 0מצביע על הצלחה; כל ערך שאינו אפס מציין שגיאה. המשמעות של ערכי יציאה אלה תלויה במערכת ההפעלה המסוימת. שגיאת Win32 עם הערך 2היא שגיאת "קובץ לא נמצא". זה הגיוני, שכן javacמצפה מאיתנו לעקוב אחר התוכנית עם קובץ קוד המקור.

לפיכך, כדי לעקוף את המלכודת השנייה - תלויה לנצח Runtime.exec()- אם התוכנית שאתה משגר מפיקה פלט או מצפה לקלט, ודא שאתה מעבד את זרמי הקלט והפלט.

בהנחה שפקודה היא תוכנית הפעלה

תחת מערכת ההפעלה Windows, מתכנתים חדשים רבים Runtime.exec()נקלעים אליהם כשמנסים להשתמש בה לפקודות שאינן ניתנות להפעלה כמו dirו copy. לאחר מכן הם נקלעים Runtime.exec()למלכודת השלישית. רישום 4.4 מדגים בדיוק את זה:

רישום 4.4 BadExecWinDir.java

ייבא java.util. *; ייבא java.io. *; בכיתה ציבורית BadExecWinDir {main public static void (String args []) {try {Runtime rt = Runtime.getRuntime (); תהליך proc = rt.exec ("dir"); InputStream stdin = proc.getInputStream (); InputStreamReader isr = InputStreamReader חדש (stdin); BufferedReader br = BufferedReader חדש (isr); קו מחרוזת = null; System.out.println (""); בעוד ((line = br.readLine ())! = null) System.out.println (שורה); System.out.println (""); int exitVal = proc.waitFor (); System.out.println ("Process exitValue:" + exitVal); } לתפוס (ניתן לזרוק) {t.printStackTrace (); }}}

ריצת BadExecWinDirתוצרת:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java BadExecWinDir java.io.IOException: CreateProcess: dir error = 2 ב- java.lang.Win32Process.create (שיטה מקורית) ב- java.lang.Win32Process. (מקור לא ידוע) ב- java.lang.Runtime.execInternal (Native Method) ב- java.lang.Runtime.exec (מקור לא ידוע) ב- java.lang.Runtime.exec (מקור לא ידוע) ב- java.lang.Runtime.exec (מקור לא ידוע) ב- java .lang.Runtime.exec (מקור לא ידוע) ב- BadExecWinDir.main (BadExecWinDir.java:12)

כאמור קודם, ערך השגיאה 2פירושו "קובץ לא נמצא", שמשמעותו, במקרה זה, dir.exeשלא ניתן היה למצוא את ההפעלה ששמה . הסיבה לכך היא שפקודת הספריה היא חלק מתורגמן הפקודה של Windows ולא הפעלה נפרדת. כדי להפעיל את מתורגמן הפקודות של Windows, הפעל command.comאו cmd.exe, בהתאם למערכת ההפעלה Windows שבה אתה משתמש. רישום 4.5 מריץ עותק של מתורגמן הפקודה של Windows ואז מבצע את הפקודה המסופקת על ידי המשתמש (למשל dir).

רישום 4.5 GoodWindowsExec.java