יצירת תוסף Java Compiler

1. סקירה כללית

Java 8 מספק ממשק API ליצירה ג'אבאק תוספים. למרבה הצער, קשה למצוא תיעוד טוב לכך.

במאמר זה אנו נציג את כל התהליך של יצירת סיומת מהדר המוסיפה קוד מותאם אישית ל *.מעמד קבצים.

2. התקנה

ראשית, עלינו להוסיף JDK tools.jar כתלות בפרויקט שלנו:

 מערכת com.sun 1.8.0 מערכת $ {java.home} /../ lib / tools.jar 

כל סיומת מהדר היא מחלקה המיושמת com.sun.source.util.Plugin מִמְשָׁק. בואו ניצור את זה בדוגמה שלנו:

בואו ניצור את זה בדוגמה שלנו:

class class SampleJavacPlugin מיישם תוסף {@Override public String getName () {return "MyPlugin"; } @Override init void public (משימת JavacTask, String ... args) {Context context = ((BasicJavacTask) task) .getContext (); Log.instance (הקשר) .printRawLines (Log.WriterKind.NOTICE, "שלום מ-" + getName ()); }}

לעת עתה, אנו רק מדפיסים "שלום" כדי להבטיח שהקוד שלנו נאסף בהצלחה ונכלל באוסף.

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

יש עוד צעד הכרחי כדי להפוך את הרחבה לגלויה על ידי ג'אבאק:זה צריך להיחשף דרך ServiceLoader מִסגֶרֶת.

כדי להשיג זאת, עלינו ליצור קובץ בשם com.sun.source.util.Plugin עם תוכן שהוא שם הכיתה המלא של התוסף שלנו (com.baeldung.javac.SampleJavacPlugin) והניחו אותו ב META-INF / שירותים מַדרִיך.

אחרי זה אנחנו יכולים להתקשר ג'אבאק עם ה -Xplugin: MyPlugin החלף:

baeldung / tutorials $ javac -cp ./core-java/target/classes -Xplugin: MyPlugin ./core-java/src/main/java/com/baeldung/javac/TestClass.java שלום מ- MyPlugin

ציין זאת עלינו תמיד להשתמש ב- חוּט חזר מהתוספים getName () שיטה כ -פלוגין ערך אופציה.

3. מחזור חיים של תוסף

א התוסף נקרא על ידי המהדר רק פעם אחת, דרך ה- init () שיטה.

כדי לקבל הודעה על אירועים הבאים, עלינו לרשום התקשרות חוזרת. אלה מגיעים לפני ואחרי כל שלב עיבוד לכל קובץ מקור:

  • לְנַתֵחַ - בונה עץ תחביר מופשט (AST)
  • להיכנס - ייבוא ​​קוד המקור נפתר
  • לְנַתֵחַ - פלט מנתח (AST) מנותח על שגיאות
  • לִיצוֹר - יצירת קבצים בינאריים עבור קובץ מקור היעד

ישנם שני סוגים נוספים של אירועים - ANNOTATION_PROCESSING ו ANNOTATION_PROCESSING_ROUND אבל אנחנו לא מעוניינים בהם כאן.

לדוגמה, כאשר אנו רוצים לשפר את ההידור על ידי הוספת כמה בדיקות המבוססות על מידע על קוד המקור, זה סביר לעשות זאת ב- PARSE הסתיים מנהל אירועים:

init חלל ציבורי (משימת JavacTask, מחרוזת ... args) {task.addTaskListener (חדש TaskListener () {חלל ציבורי התחיל (TaskEvent e) {} חלל ציבורי הסתיים (TaskEvent e) {if (e.getKind ()! = TaskEvent .Kind.PARSE) {return;} // ביצוע מכשור}}); }

4. חלץ נתוני AST

אנחנו יכולים להשיג AST שנוצר על ידי מהדר Java דרך ה- TaskEvent.getCompilationUnit (). ניתן לבחון את פרטיו באמצעות TreeVisitor מִמְשָׁק.

שים לב שרק א עֵץ אלמנט, שעבורו לְקַבֵּל() שיטה נקראת, שולחת אירועים למבקר הנתון.

למשל, כשאנחנו מבצעים ClassTree.accept (מבקר), רק visitClass () מופעלת; אנחנו לא יכולים לצפות לזה, נגיד, visitMethod () מופעל גם לכל שיטה בשיעור הנתון.

אנחנו יכולים להשתמש TreeScanner כדי להתגבר על הבעיה:

הריק הציבורי הסתיים (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {return; } e.getCompilationUnit (). accept (TreeScanner חדש () {@Override public Void visitClass (ClassTree node, Void aVoid) {return super.visitClass (node, aVoid); @Override Public Void visitMethod (MethodTree node, Void aVoid) { החזר super.visitMethod (node, aVoid);}}, null); }

בדוגמה זו, יש צורך להתקשר super.visitXxx (צומת, ערך) לעבד באופן רקורסיבי את ילדי הצומת הנוכחי.

5. שנה את AST

כדי להציג כיצד אנו יכולים לשנות את ה- AST, נכניס בדיקות זמן ריצה עבור כל הארגומנטים המספריים המסומנים ב- @חִיוּבִי ביאור.

זו הערה פשוטה שניתן להחיל על פרמטרים של שיטה:

@Documented @Retention (RetentionPolicy.CLASS) @Target ({ElementType.PARAMETER}) ציבורי @ ממשק חיובי {}

הנה דוגמה לשימוש בהערה:

שירות חלל ציבורי (@Positive int i) {}

בסופו של דבר, אנו רוצים שקוד הבתים ייראה כאילו הוא מורכב ממקור כזה:

שירות חלל ציבורי (@Positive int i) {if (i <= 0) {זרוק IllegalArgumentException חדש ("טיעון לא חיובי (" + i + ") ניתן כפרמטר @Positive 'i'"); }}

המשמעות של זה היא שאנחנו רוצים IllegalArgumentException להיזרק על כל טיעון המסומן ב @חִיוּבִי ששווה או פחות מ- 0.

5.1. לאן מכשיר

בואו לגלות כיצד אנו יכולים לאתר מקומות יעד בהם יש ליישם את המכשור:

סט סטטי פרטי TARGET_TYPES = Stream.of (byte.class, short.class, char.class, int.class, long.class, float.class, double.class) .map (Class :: getName) .collect (Collectors. כדי להגדיר()); 

לשם פשטות, הוספנו כאן רק סוגים מספריים פרימיטיביים.

לאחר מכן, בואו נגדיר א shouldInstrument () שיטה שבודקת אם לפרמטר יש סוג בקבוצת TARGET_TYPES וכן @חִיוּבִי ביאור:

בוליאני פרטי צריךInstrument (פרמטר VariableTree) {return TARGET_TYPES.contains (parameter.getType (). toString ()) && parameter.getModifiers (). getAnnotations (). stream () .anyMatch (a -> Positive.class.getSimpleName () .equals (a.getAnnotationType (). toString ())); }

ואז נמשיך את גָמוּר() השיטה שלנו SampleJavacPlugin בכיתה עם החלת המחאה על כל הפרמטרים העומדים בתנאינו:

הריק הציבורי הסתיים (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {return; } e.getCompilationUnit (). קבל (TreeScanner חדש () {@Override Public Void visitMethod (MethodTree method, Void v) {List parametersToInstrument = method.getParameters (). stream () .filter (SampleJavacPlugin.this :: shouldInstrument). collect (Collectors.toList ()); if (! parametersToInstrument.isEmpty ()) {Collections.reverse (parametersToInstrument); parametersToInstrument.forEach (p -> addCheck (method, p, context));} להחזיר super.visitMethod (שיטה , v);}}, null); 

בדוגמה זו הפכנו את רשימת הפרמטרים מכיוון שיש מקרה אפשרי שמספר יותר מטיעון אחד @חִיוּבִי. מכיוון שכל צ'ק נוסף כהוראת השיטה הראשונה, אנו מעבדים אותם RTL כדי להבטיח את הסדר הנכון.

5.2. כיצד מכשיר

הבעיה היא ש"קרא AST "מונח ב פּוּמְבֵּי אזור API, ואילו פעולות "לשנות AST" כמו "הוסף בדיקות אפס" הן פְּרָטִי ממשק API.

כדי לטפל בזה, ניצור אלמנטים חדשים של AST באמצעות א TreeMaker למשל.

ראשית, עלינו להשיג א הֶקשֵׁר למשל:

@ עקוב על התחל הריק הציבורי (משימת JavacTask, מחרוזת ... טענות) {Context context = ((BasicJavacTask) task) .getContext (); // ...}

ואז נוכל להשיג את TreeMarker חפץ דרך TreeMarker.instance (הקשר) שיטה.

כעת אנו יכולים לבנות אלמנטים חדשים של AST, למשל אם ניתן לבנות את הביטוי על ידי קריאה ל TreeMaker.If ():

סטטי פרטי JCTree.JCIf createCheck (פרמטר VariableTree, הקשר הקשר) {TreeMaker factory = TreeMaker.instance (context); שמות symbolTable = Names.instance (הקשר); return factory.at ((((JCTree) parameter) .pos) .If (factory.Parens (createIfCondition (factory, symbolsTable, parameter)), createIfBlock (factory, symbolsTable, parameter), null); }

שים לב שאנו רוצים להציג את קו העקיבה הנכון של מחסנית כאשר יוצא חריג מהמחאה שלנו. לכן אנו מתאימים את מיקום המפעל AST לפני שנוצר באמצעותו אלמנטים חדשים factory.at (((JCTree) פרמטר) .pos).

ה createIfCondition () השיטה בונה את "parameterId< 0″ אם מַצָב:

פרטי JCTree.JCBinary סטטי פרטי createIfCondition (מפעל TreeMaker, שמות symbolTable, פרמטר VariableTree) {Name parameterId = symbolsTable.fromString (parameter.getName (). toString ()); החזר מפעל. בינארי (JCTree.Tag.LE, factory.Ident (parameterId), factory.Literal (TypeTag.INT, 0)); }

לאחר מכן, createIfBlock () שיטה בונה בלוק שמחזיר יוצא מהכלל לא חוקי:

פרטי JCTree.JCBlock סטטי פרטי createIfBlock (מפעל TreeMaker, שמות symbolTable, פרמטר VariableTree) {String parameterName = parameter.getName (). toString (); שם parameterId = symbolsTable.fromString (parameterName); מחרוזת errorMessagePrefix = String.format ("טיעון '% s' מהסוג% s מסומן על ידי @% s אבל קיבל '", parameterName, parameter.getType (), Positive.class.getSimpleName ()); מחרוזת errorMessageSuffix = "'בשביל זה"; החזר מפעל.בלוק (0, com.sun.tools.javac.util.List.of (factory.Trow) (factory.NewClass (null, nil (), factory.Ident (symbolTable.fromString (IllegalArgumentException.class.getSimpleName ()) )), com.sun.tools.javac.util.List.of (factory.Binary (JCTree.Tag.PLUS, factory.Binary (JCTree.Tag.PLUS, factory.Literal (TypeTag.CLASS, errorMessagePrefix), מפעל. Ident (parameterId)), factory.Literal (TypeTag.CLASS, errorMessageSuffix))), null)))); }

עכשיו, כשאנחנו מסוגלים לבנות אלמנטים חדשים של AST, עלינו להכניס אותם ל- AST שהוכן על ידי המנתח. אנחנו יכולים להשיג זאת על ידי ליהוק פּוּמְבֵּי אפאי אלמנטים ל פְּרָטִי סוגי API:

ריק ריק addCheck (שיטת MethodTree, פרמטר VariableTree, הקשר הקשר) {JCTree.JCIf check = createCheck (פרמטר, הקשר); JCTree.JCBlock body = (JCTree.JCBlock) method.getBody (); body.stats = body.stats.prepend (סימון); }

6. בדיקת התוסף

עלינו להיות מסוגלים לבדוק את התוסף שלנו. זה כולל את הדברים הבאים:

  • הידור מקור הבדיקה
  • הפעל את הבינאריות המהודרות וודא שהם מתנהגים כצפוי

לשם כך עלינו להציג כמה שיעורי עזר.

SimpleSourceFile חושף את הטקסט של קובץ המקור הנתון ל- ג'אבאק:

מחלקה ציבורית SimpleSourceFile מרחיב SimpleJavaFileObject {תוכן מחרוזת פרטי; ציבורי SimpleSourceFile (String qualifiedClassName, String testSource) {super (URI.create (String.format ("file: //% s% s", QualClassName.replaceAll ("\.", "/"), Kind.SOURCE. הרחבה)), Kind.SOURCE); content = testSource; } @Override CharSequence ציבורי getCharContent (בוליאני ignoreEncodingErrors) {להחזיר תוכן; }}

SimpleClassFile מחזיק בתוצאת האוסף כמערך בתים:

מחלקה ציבורית SimpleClassFile מרחיב את SimpleJavaFileObject {ByteArrayOutputStream פרטי; ציבורי SimpleClassFile (URI uri) {super (uri, Kind.CLASS); } @Override פלטפורמת OutputStream openOutputStream () זורק IOException {return out = new ByteArrayOutputStream (); } בתים ציבוריים [] getCompiledBinaries () {return out.toByteArray (); } // גטרס}

SimpleFileManager מבטיח שהמהדר ישתמש בבעל קוד הקוד שלנו:

מחלקה ציבורית SimpleFileManager מרחיב ForwardingJavaFileManager {רשימה פרטית מורכבת = ArrayList חדש (); // קונסטרוקטורים / getters סטנדרטיים @Override ציבור JavaFileObject getJavaFileForOutput (מיקום מיקום, מחרוזת className, JavaFileObject.Kind סוג, אח של FileObject) {SimpleClassFile תוצאה = חדש SimpleClassFile (URI.create ("מחרוזת: //" + className)); compiled.add (תוצאה); תוצאת החזרה; } רשימה ציבורית getCompiled () {return compiled; }}

לבסוף, כל זה קשור לאוסף בזיכרון:

מחלקה ציבורית TestCompiler {בית ציבורי [] קומפילציה (מחרוזת qualifiedClassName, מחרוזת testSource) {פלט StringWriter = חדש StringWriter (); מהדר JavaCompiler = ToolProvider.getSystemJavaCompiler (); SimpleFileManager fileManager = SimpleFileManager חדש (compiler.getStandardFileManager (null, null, null)); רשימה compilationUnits = singletonList (SimpleSourceFile חדש (qualifiedClassName, testSource)); ארגומנטים של רשימה = ArrayList חדש (); arguments.addAll (asList ("- classpath", System.getProperty ("java.class.path"), "-Xplugin:" + SampleJavacPlugin.NAME)); JavaCompiler.CompilationTask task = compiler.getTask (פלט, fileManager, null, ארגומנטים, null, compilationUnits); task.call (); להחזיר fileManager.getCompiled (). iterator (). הבא (). getCompiledBinaries (); }}

לאחר מכן, עלינו רק להריץ את הבינאריות:

מחלקה ציבורית TestRunner {הפעלת אובייקט ציבורי (byte [] byteCode, String qualifiedClassName, String methodName, Class [] argumentTypes, Object ... args) זורק זרק {ClassLoader classLoader = new ClassLoader () {@Override מוגן Class findClass (שם מחרוזת) זורק ClassNotFoundException {return defineClass (שם, byteCode, 0, byteCode.length); }}; קלאז כיתתי; נסה את {clazz = classLoader.loadClass (qualifiedClassName); } לתפוס (ClassNotFoundException e) {לזרוק RuntimeException חדש ("לא ניתן לטעון מחלקת בדיקה מקומפלת", e); } שיטת שיטה; נסה {method = clazz.getMethod (methodName, argumentTypes); } לתפוס (NoSuchMethodException e) {לזרוק RuntimeException חדש ("לא מצליח למצוא את שיטת 'main ()' בכיתת הבדיקה המהודרת", e); } נסה {return method.invoke (null, args); } לתפוס (InvocationTargetException e) {לזרוק e.getCause (); }}}

מבחן עשוי להיראות כך:

מחלקה ציבורית SampleJavacPluginTest {פרטית סטטית סופית מחרוזת CLASS_TEMPLATE = "חבילה com.baeldung.javac; \ n \ n" + "מבחן מחלקה ציבורית {\ n" + "שירות סטטי ציבורי% 1 $ s (@Positive% 1 $ si) { \ n "+" החזר i; \ n "+"} \ n "+"} \ n "+" "; מהדר TestCompiler פרטי = TestCompiler חדש (); רץ TestRunner פרטי = TestRunner חדש (); @Test (צפוי = IllegalArgumentException.class) חלל ציבורי givenInt_whenNegative_thenThrowsException () זורק Throwable {compileAndRun (double.class, -1); } compileAndRun אובייקט פרטי (ארגומנט מחלקה, ארגומנט אובייקט) זורק {מחרוזת qualifiedClassName = "com.baeldung.javac.Test" זורק; בתים [] byteCode = compiler.compile (qualifiedClassName, String.format (CLASS_TEMPLATE, argumentType.getName ())); החזר runner.run (byteCode, qualifiedClassName, "שירות", מחלקה חדשה [] {argumentType}, ארגומנט); }}

כאן אנו מרכיבים א מִבְחָן כיתה עם א שֵׁרוּת() שיטה שפרמטר מסומן בה @חִיוּבִי. ואז, אנחנו מנהלים את מִבְחָן class על ידי הגדרת ערך כפול -1 לפרמטר השיטה.

כתוצאה מהפעלת המהדר עם התוסף שלנו, הבדיקה תשלח IllegalArgumentException לפרמטר השלילי.

7. מסקנה

במאמר זה הראינו את התהליך המלא של יצירה, בדיקה והפעלת תוסף Java Compiler.

קוד המקור המלא של הדוגמאות ניתן למצוא באתר GitHub.


$config[zx-auto] not found$config[zx-overlay] not found