Java עם ANTLR

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

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

2. ANTLR

ANTLR (ANother Tool for Recognition Language) הוא כלי לעיבוד טקסט מובנה.

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

זה משמש לעתים קרובות לבניית כלים ומסגרות. לדוגמה, Hibernate משתמש ב- ANTLR לניתוח ועיבוד של שאילתות HQL ו- Elasticsearch משתמש בה ללא כאבים.

וג'אווה היא רק מחייב אחד. ANTLR מציע גם כריכות עבור C #, Python, JavaScript, Go, C ++ ו- Swift.

3. תצורה

קודם כל, נתחיל בהוספת זמן ריצה antlr לזו שלנו pom.xml:

 org.antlr antlr4-runtime 4.7.1 

וגם התוסף antlr-maven:

 org.antlr antlr4-maven-plugin 4.7.1 antlr4 

תפקיד התוסף ליצור קוד מהדקדוקים שאנו מציינים.

4. איך זה עובד?

בעיקרון, כאשר אנו רוצים ליצור את המנתח באמצעות תוסף ANTLR Maven, עלינו לבצע שלושה שלבים פשוטים:

  • להכין קובץ דקדוק
  • לייצר מקורות
  • ליצור את המאזין

אז בואו נראה את השלבים האלה בפעולה.

5. שימוש בדקדוק קיים

ראשית נשתמש ב- ANTLR לניתוח קוד לשיטות עם מעטפת גרועה:

class public SampleClass {public void DoSomethingElse () {// ...}}

במילים פשוטות, נוודא שכל שמות השיטות בקוד שלנו מתחילים באות קטנה.

5.1. הכן קובץ דקדוק

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

בואו נשתמש בקובץ הדקדוק Java8.g4 שמצאנו בתיקון הדקדוק של Github של ANTLR.

אנחנו יכולים ליצור את src / main / antlr4 מדריך ולהוריד אותו שם.

5.2. צור מקורות

ANTLR עובד על ידי יצירת קוד Java המתאים לקבצי הדקדוק שאנו נותנים לו, ותוסף ה- maven מקל על כך:

חבילת mvn

כברירת מחדל, זה ייצור מספר קבצים תחת ה- יעד / מקורות שנוצרו / antlr4 מַדרִיך:

  • Java8.interp
  • Java8Listener.java
  • Java8BaseListener.java
  • Java8Lexer.java
  • Java8Lexer.interp
  • Java8Parser.java
  • Java8.tokens
  • Java8Lexer.tokens

שימו לב ששמות הקבצים האלה מבוססים על שם קובץ הדקדוק.

אנחנו נצטרך את Java8Lexer וה Java8Parser קבצים מאוחר יותר כשאנחנו בודקים. לעת עתה, עם זאת, אנו זקוקים ל Java8BaseListener ליצירת שלנו MethodUppercaseListener.

5.3. יוצר MethodUppercaseListener

בהתבסס על הדקדוק של Java8 בו השתמשנו, Java8BaseListener יש מספר שיטות שנוכל לעקוף, כל אחת מתאימה לכותרת בקובץ הדקדוק.

לדוגמה, הדקדוק מגדיר את שם השיטה, רשימת הפרמטרים וזורק ככה:

methodDeclarator: מזהה '(' formalParameterList? ')' מתעמעם? ;

וכך Java8BaseListener יש שיטה enterMethodDeclarator שיופעל בכל פעם שנתקל בדפוס זה.

אז בואו נעקוף enterMethodDeclarator, לשלוף את מזהה, ובצע את הבדיקה שלנו:

מחלקה ציבורית UppercaseMethodListener מרחיב את Java8BaseListener {שגיאות רשימה פרטית = ArrayList חדש (); // ... גטר עבור שגיאות @Override public void enterMethodDeclarator (Java8Parser.MethodDeclaratorContext ctx) {TerminalNode node = ctx.Identifier (); Method stringName = node.getText (); אם (Character.isUpperCase (methodName.charAt (0))) {שגיאת מחרוזת = String.format ("שיטה% s מוגדרת באותיות רישיות!", methodName); error.add (שגיאה); }}}

5.4. בדיקה

עכשיו, בואו נעשה בדיקות. ראשית, אנו בונים את הלקסר:

מחרוזת javaClassContent = "מחלקה ציבורית SampleClass {void DoSomething () {}}"; Java8Lexer java8Lexer = Java8Lexer חדש (CharStreams.fromString (javaClassContent));

לאחר מכן אנו מיישרים את המנתח:

אסימונים של CommonTokenStream = CommonTokenStream חדש (לקסר); מנתח Java8Parser = Java8Parser חדש (אסימונים); עץ ParseTree = parser.compilationUnit ();

ואז, ההליכון והמאזין:

ParseTreeWalker הליכון = ParseTreeWalker חדש (); מאזין UppercaseMethodListener = חדש UppercaseMethodListener ();

לבסוף, אנו אומרים ל- ANTLR לעבור בכיתת המדגם שלנו:

הליכון. הליכה (מאזין, עץ); assertThat (listener.getErrors (). size (), הוא (1)); assertThat (listener.getErrors (). get (0), is ("שיטת DoSomething מוגדרת באותיות רישיות!"));

6. בניית הדקדוק שלנו

עכשיו, בואו ננסה משהו מורכב קצת יותר, כמו ניתוח קבצי יומן:

2018-מאי -05 14:20:18 INFO אירעה שגיאה כלשהי 2018-מאי-05 14:20:19 INFO שגיאה נוספת 2018-מאי -05 14:20:20 INFO שיטה כלשהי התחילה 2018-מאי -05 14:20 : 21 DEBUG שיטה אחרת התחילה 2018-מאי -05 14:20:21 DEBUG נכנסת לשיטה מדהימה 2018-מאי-05 14:20:24 שגיאה דבר רע קרה

מכיוון שיש לנו פורמט יומן מותאם אישית, ראשית עלינו ליצור דקדוק משלנו.

6.1. הכן קובץ דקדוק

ראשית, בואו נראה אם ​​נוכל ליצור מפה מנטאלית של איך כל שורת יומן נראית בקובץ שלנו.

או אם נעמיק עוד רמה אחת, אנו עשויים לומר:

:= …

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

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

נתחיל בהגדרת שברים שהם אבני בניין לשימוש חוזר לכללי לקסר.

שבר DIGIT: [0-9]; קטע TWODIGIT: DIGIT DIGIT; שבר LETTER: [A-Za-z];

לאחר מכן, בואו נגדיר את כללי ה- lexer שנותרו:

תאריך: TWODIGIT TWODIGIT '-' LETTER LETTER LETTER '-' TWODIGIT; TIME: TWODIGIT ':' TWODIGIT ':' TWODIGIT; טקסט: LETTER +; CRLF: '\ r'? '\ n' | '\ r';

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

יומן: כניסה +; ערך: הודעת CRLF חותמת זמן '' רמת '';

ואז נוסיף את הפרטים עבור חותמת זמן:

חותמת זמן: DATE '' TIME;

ל רָמָה:

רמה: 'שגיאה' | 'מידע' | 'לנפות';

ועבור הוֹדָעָה:

הודעה: (TEXT | '') +;

וזה הכל! הדקדוק שלנו מוכן לשימוש. אנחנו נשים את זה מתחת ל src / main / antlr4 ספריה כמו קודם.

6.2.צור מקורות

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

6.3. צור את מאזין היומנים שלנו

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

אז בואו נתחיל בשיעור מודל פשוט עבור ערך היומן:

מחלקה ציבורית LogEntry {רמת LogLevel פרטית; הודעת מחרוזת פרטית; חותמת זמן פרטית LocalDateTime; // גטרים וקובעים}

עכשיו, אנחנו צריכים לסווג משנה LogBaseListener כמו קודם:

מחלקה ציבורית LogListener מרחיב את LogBaseListener {ערכי רשימה פרטית = ArrayList חדש (); זרם יומן פרטי פרטי;

נוֹכְחִי נאחז בקו היומן הנוכחי, אותו נוכל לאתחל מחדש בכל פעם שנכנס ל- a logEntry, שוב על סמך הדקדוק שלנו:

 @ ביטול ציבורי ריק ריק EnterEntry (LogParser.EntryContext ctx) {this.current = LogEntry חדש (); }

לאחר מכן נשתמש enterTestestamp, enterLevel, ו enterMessage לקביעת המתאים LogEntry נכסים:

 @ ביטול ציבורי ריק ריק EnterTimestamp (LogParser.TimestampContext ctx) {this.current.setTimestamp (LocalDateTime.parse (ctx.getText (), DEFAULT_DATETIME_FORMATTER)); } @Override public void enterMessage (LogParser.MessageContext ctx) {this.current.setMessage (ctx.getText ()); } @ ביטול ריק ריק ציבורי enterLevel (LogParser.LevelContext ctx) {this.current.setLevel (LogLevel.valueOf (ctx.getText ())); }

ולבסוף, בואו נשתמש ב- exitEntry שיטה על מנת ליצור ולהוסיף את החדש שלנו LogEntry:

 @Override public void exitLogEntry (LogParser.EntryContext ctx) {this.entries.add (this.current); }

שים לב, אגב, שלנו LogListener הוא לא threadsafe!

6.4. בדיקה

ועכשיו נוכל לבדוק שוב כמו בפעם האחרונה:

@ מבחן ציבורי בטל כאשרLogContainsOneErrorLogEntry_thenOneErrorIsReturned () זורק חריג {מחרוזת logLine; // מיישרים את הלקסר, המנתח והמאזין הלוגיסטני ההליכון = לוגליסטנר חדש (); walker.walk (מאזין, logParser.log ()); כניסה של LogEntry = listener.getEntries (). Get (0); assertThat (entry.getLevel (), הוא (LogLevel.ERROR)); assertThat (entry.getMessage (), הוא ("דבר רע קרה")); assertThat (entry.getTimestamp (), הוא (LocalDateTime.of (2018,5,5,14,20,24))); }

7. מסקנה

במאמר זה התמקדנו כיצד ליצור את הניתוח המותאם אישית לשפה הפרטית באמצעות ה- ANTLR.

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

כמו תמיד, ניתן למצוא את כל הקוד המשמש כאן ב- GitHub.


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