רישום Java משופר עם הקשר אבחוני ממופה (MDC)

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

במאמר זה נחקור את השימוש ב- הקשר אבחוני ממופה (MDC) לשיפור רישום היישומים.

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

2. מדוע להשתמש ב- MDC

נתחיל בדוגמא. נניח שעלינו לכתוב תוכנה שמעבירה כסף. הקמנו א לְהַעֲבִיר בכיתה לייצג מידע בסיסי: מזהה העברה ייחודי ושם השולח:

העברה בכיתה ציבורית {Transaction String פרטי; שולח מחרוזת פרטי; סכום פרטי פרטי; העברה ציבורית (מחרוזת עסקית, שולח מחרוזת, סכום ארוך) {this.transactionId = transactionId; this.sender = שולח; this.amount = סכום; } ציבורי מחרוזת getSender () {שולח החזרה; } מחרוזת ציבורית getTransactionId () {return transactionId; } ציבורי Long getAmount () {סכום החזר; }} 

כדי לבצע את ההעברה, עלינו להשתמש בשירות המגובה על ידי ממשק API פשוט:

מחלקת מופשט ציבורי TransferService {העברה בוליאנית ציבורית (סכום ארוך) {// מתחברת לשירות מרחוק להעברת כסף בפועל} ריק מוגן מופשט לפני העברה (סכום ארוך); ריק מוגן מופשט לאחר העברה (כמות ארוכה, תוצאה בוליאנית); } 

ה לפני העברה () ו afterTransfer () ניתן לבטל שיטות להפעלת קוד מותאם אישית ממש לפני ומיד לאחר סיום ההעברה.

אנחנו הולכים למנף לפני העברה () ו afterTransfer () ל רשום מידע על ההעברה.

בואו ניצור את יישום השירות:

יבוא org.apache.log4j.Logger; יבוא com.baeldung.mdc.TransferService; מחלקה ציבורית Log4JTransferService מרחיב את TransferService {לוגר לוגר פרטי = Logger.getLogger (Log4JTransferService.class); @Override מוגן ריק לפני העברה (כמות ארוכה) {logger.info ("מתכונן להעברה" + סכום + "$."); } @Override מוגן ריק לאחר העברה (כמות ארוכה, תוצאה בוליאנית) {logger.info ("האם ההעברה של" + סכום + "$ הושלמה בהצלחה?" + תוצאה + "."); }} 

הנושא העיקרי שיש לציין כאן הוא כאשר נוצרת הודעת היומן, לא ניתן לגשת אל לְהַעֲבִיר לְהִתְנַגֵד - רק הסכום נגיש, מה שלא מאפשר לרשום את מזהה העסקה או את השולח.

בואו נקבע את הרגיל log4j.properties קובץ להיכנס למסוף:

log4j.appender.consoleAppender = org.apache.log4j.ConsoleAppender log4j.appender.consoleAppender.layout = org.apache.log4j.PatternLayout log4j.appender.consoleAppender.layout.ConversionPattern =% - 4r [% t]% 5p% c% x -% m% n log4j.rootLogger = TRACE, consoleAppender 

בואו סוף סוף להגדיר יישום קטן שמסוגל להריץ העברות מרובות בו זמנית דרך שירות ExecutorService:

class class TransferDemo {public static void main (String [] args) {ExecutorService executor = Executors.newFixedThreadPool (3); TransactionFactory transactionFactory = TransactionFactory חדש (); עבור (int i = 0; i <10; i ++) {Transfer tx = transactionFactory.newInstance (); משימה ניתנת להפעלה = Log4JRunnable חדש (tx); executor.submit (משימה); } executor.shutdown (); }}

נציין כי על מנת להשתמש ב- שירות ExecutorService, עלינו לעטוף את ביצוע ה- Log4JTransferService במתאם כי executor.submit () מצפה א ניתן לרוץ:

מחלקה ציבורית Log4JRunnable מיישמת Runnable {Private Transfer tx; Log4JRunnable ציבורי (העברת טקס) {this.tx = tx; } הפעלה בטלנית ציבורית () {log4jBusinessService.transfer (tx.getAmount ()); }} 

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

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

... 519 [pool-1-thread-3] INFO Log4JBusinessService - מתכונן להעברת 1393 $. 911 [pool-1-thread-2] INFO Log4JBusinessService - האם ההעברה של 1065 $ הושלמה בהצלחה? נָכוֹן. 911 [pool-1-thread-2] INFO Log4JBusinessService - מתכונן להעברת 1189 $. 989 [pool-1-thread-1] INFO Log4JBusinessService - האם ההעברה של 1350 $ הושלמה בהצלחה? נָכוֹן. 989 [pool-1-thread-1] INFO Log4JBusinessService - מתכונן להעברת 1178 $. 1245 [pool-1-thread-3] INFO Log4JBusinessService - האם ההעברה של 1393 $ הושלמה בהצלחה? נָכוֹן. 1246 [pool-1-thread-3] INFO Log4JBusinessService - מתכונן להעברת 1133 $. 1507 [pool-1-thread-2] INFO Log4JBusinessService - האם ההעברה של 1189 $ הושלמה בהצלחה? נָכוֹן. 1508 [pool-1-thread-2] INFO Log4JBusinessService - מתכונן להעברת 1907 $. 1639 [pool-1-thread-1] INFO Log4JBusinessService - האם ההעברה של 1178 $ הושלמה בהצלחה? נָכוֹן. 1640 [pool-1-thread-1] INFO Log4JBusinessService - מתכונן להעברת 674 $. ... 

לְמַרְבֶּה הַמַזָל, MDC יכול לעזור.

3. MDC ב- Log4j

בואו נציג MDC.

MDC ב- Log4j מאפשר לנו למלא מבנה דמוי מפה עם פיסות מידע הנגישות למתווך כאשר הודעת היומן נכתבת בפועל.

מבנה ה- MDC מחובר פנימית לשרשור הביצוע באותו אופן ThreadLocal משתנה יהיה.

וכך, הרעיון ברמה הגבוהה הוא:

  1. למלא את ה- MDC בפיסות מידע שאנו רוצים להעמיד לרשות המועמד
  2. ואז רשום הודעה
  3. ולבסוף, נקה את ה- MDC

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

אז בואו ונשנה את הקוד בהתאם להנחיות הבאות:

יבוא org.apache.log4j.MDC; מחלקה ציבורית Log4JRunnable מיישמת Runnable {Private Transfer tx; פרטי סטטי Log4JTransferService log4jBusinessService = Log4JTransferService חדש (); Log4JRunnable ציבורי (העברת טקס) {this.tx = tx; } הפעלה בטלנית ציבורית () {MDC.put ("transaction.id", tx.getTransactionId ()); MDC.put ("transaction.owner", tx.getSender ()); log4jBusinessService.transfer (tx.getAmount ()); MDC. ברור (); }} 

באופן לא מפתיע MDC.put () משמש להוספת מפתח וערך מקביל ב- MDC תוך כדי MDC ברור () מרוקן את ה- MDC.

בואו עכשיו נשנה את log4j.properties כדי להדפיס את המידע שרק אחסנו ב- MDC. זה מספיק כדי לשנות את דפוס ההמרה באמצעות %איקס{} מציין מיקום עבור כל ערך הכלול ב- MDC נרצה להירשם:

log4j.appender.consoleAppender.layout.ConversionPattern =% -4r [% t]% 5p% c {1}% x -% m - tx.id =% X {transaction.id} tx.owner =% X {transaction. בעלים}% n

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

638 [pool-1-thread-2] INFO Log4JBusinessService - האם ההעברה של 1104 $ הושלמה בהצלחה? נָכוֹן. - tx.id = 2 tx.owner = Marc 638 [pool-1-thread-2] INFO Log4JBusinessService - מתכונן להעברת 1685 $. - tx.id = 4 tx.owner = John 666 [pool-1-thread-1] INFO Log4JBusinessService - האם ההעברה של 1985 $ הסתיימה בהצלחה? נָכוֹן. - tx.id = 1 tx.owner = Marc 666 [pool-1-thread-1] INFO Log4JBusinessService - מתכונן להעברת 958 $. - tx.id = 5 tx.owner = Susan 739 [pool-1-thread-3] INFO Log4JBusinessService - האם ההעברה של 783 $ הושלמה בהצלחה? נָכוֹן. - tx.id = 3 tx.owner = Samantha 739 [pool-1-thread-3] INFO Log4JBusinessService - מתכונן להעברת 1024 $. - tx.id = 6 tx.owner = John 1259 [pool-1-thread-2] INFO Log4JBusinessService - האם העברה של 1685 $ הושלמה בהצלחה? שֶׁקֶר. - tx.id = 4 tx.owner = John 1260 [pool-1-thread-2] INFO Log4JBusinessService - מתכונן להעברת 1667 $. - tx.id = 7 tx.owner = מארק 

4. MDC ב- Log4j2

אותה תכונה ממש זמינה גם ב- Log4j2, אז בואו נראה כיצד להשתמש בה.

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

ייבא org.apache.logging.log4j.LogManager; ייבא org.apache.logging.log4j.Logger; מחלקה ציבורית Log4J2TransferService מרחיב את TransferService {לוגר לוגר סופי סטטי פרטי = LogManager.getLogger (); @Override מוגן ריק לפני העברה (כמות ארוכה) {logger.info ("מתכונן להעברה {} $.", סכום); } @Override מוגן ריק לאחר העברה (כמות ארוכה, תוצאה בוליאנית) {logger.info ("האם העברה של {} $ הושלמה בהצלחה? {}.", סכום, תוצאה); }} 

בואו ונשנה את הקוד המשתמש ב- MDC, שנקרא בפועל ThreadContext ב- Log4j2:

יבוא org.apache.log4j.MDC; מחלקה ציבורית Log4J2Runnable מיישמת Runnable {Private final Transaction tx; פרטי Log4J2BusinessService log4j2BusinessService = Log4J2BusinessService חדש (); Log4J2Runnable ציבורי (טקס עסקאות) {this.tx = tx; } הפעלה בטלנית ציבורית () {ThreadContext.put ("transaction.id", tx.getTransactionId ()); ThreadContext.put ("transaction.owner", tx.getOwner ()); log4j2BusinessService.transfer (tx.getAmount ()); ThreadContext.clearAll (); }} 

שוב, ThreadContext.put () מוסיף ערך ב- MDC ו- ThreadContext.clearAll () מסיר את כל הערכים הקיימים.

אנחנו עדיין מתגעגעים ל log4j2.xml קובץ כדי להגדיר את הרישום. כפי שאנו יכולים לציין, התחביר כדי לציין אילו ערכי MDC יש לרשום זהה לזה ששימש ב- Log4j:

שוב, בוא נבצע את היישום ונראה את המידע של MDC מודפס ביומן:

1119 [pool-1-thread-3] INFO Log4J2BusinessService - האם ההעברה של 1198 $ הושלמה בהצלחה? נָכוֹן. - tx.id = 3 tx.owner = Samantha 1120 [pool-1-thread-3] INFO Log4J2BusinessService - מתכונן להעברת 1723 $. - tx.id = 5 tx.owner = Samantha 1170 [pool-1-thread-2] INFO Log4J2BusinessService - האם ההעברה של 701 $ הושלמה בהצלחה? נָכוֹן. - tx.id = 2 tx.owner = Susan 1171 [pool-1-thread-2] INFO Log4J2BusinessService - מתכונן להעברת 1108 $. - tx.id = 6 tx.owner = Susan 1794 [pool-1-thread-1] INFO Log4J2BusinessService - האם העברה של 645 $ הושלמה בהצלחה? נָכוֹן. - tx.id = 4 tx.owner = סוזן 

5. MDC ב- SLF4J / Logback

MDC זמין גם ב- SLF4J, בתנאי שהוא נתמך על ידי ספריית הרישום הבסיסית.

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

בואו נכין את הרגיל העברת שירות מחלקה משנה, הפעם באמצעות חזית רישום פשוטה עבור Java:

יבוא org.slf4j.Logger; יבוא org.slf4j.LoggerFactory; המחלקה הסופית Slf4TransferService מרחיב את TransferService {לוגר לוגר לוגר סופי פרטי = LoggerFactory.getLogger (Slf4TransferService.class); @Override מוגן ריק לפני העברה (כמות ארוכה) {logger.info ("מתכונן להעברה {} $.", סכום); } @Override מוגן ריק לאחר העברה (כמות ארוכה, תוצאה בוליאנית) {logger.info ("האם העברה של {} $ הושלמה בהצלחה? {}.", סכום, תוצאה); }} 

בואו נשתמש כעת בטעם ה- MDC של SLF4J. במקרה זה, התחביר והסמנטיקה זהים לזה ב- log4j:

יבוא org.slf4j.MDC; מחלקה ציבורית Slf4jRunnable מיישמת Runnable {Private Transaction final tx; ציבורי Slf4jRunnable (טקס עסקאות) {this.tx = tx; } הפעלה בטלנית ציבורית () {MDC.put ("transaction.id", tx.getTransactionId ()); MDC.put ("transaction.owner", tx.getOwner ()); חדש Slf4TransferService (). העברה (tx.getAmount ()); MDC. ברור (); }} 

עלינו לספק את קובץ התצורה של Logback, logback.xml:

   % -4r [% t]% 5p% c {1} -% m - tx.id =% X {transaction.id} tx.owner =% X {transaction.owner}% n 

שוב, נראה שהמידע ב- MDC מתווסף כראוי להודעות המחוברות, למרות שמידע זה אינו מסופק במפורש ב log.info () שיטה:

1020 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - האם ההעברה של 1869 $ הושלמה בהצלחה? נָכוֹן. - tx.id = 3 tx.owner = John 1021 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - מתכונן להעברת 1303 $. - tx.id = 6 tx.owner = Samantha 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - האם ההעברה של 1498 $ הושלמה בהצלחה? נָכוֹן. - tx.id = 4 tx.owner = Marc 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - מתכונן להעברת 1528 $. - tx.id = 7 tx.owner = Samantha 1492 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - האם ההעברה של 1110 $ הושלמה בהצלחה? נָכוֹן. - tx.id = 5 tx.owner = Samantha 1493 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - מתכונן להעברת 644 $. - tx.id = 8 tx.owner = ג'ון

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

6. בריכות MDC וחוטים

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

בואו נראה איך השילוב של ThreadLocalMDC מבוססות ובריכות חוטים יכולות להיות מסוכנות:

  1. אנו מקבלים שרשור ממאגר החוטים.
  2. לאחר מכן אנו שומרים מידע קונטקסטואלי ב- MDC באמצעות MDC.put () אוֹ ThreadContext.put ().
  3. אנו משתמשים במידע זה בכמה יומנים ואיכשהו שכחנו לנקות את הקשר MDC.
  4. החוט המושאל חוזר למאגר החוטים.
  5. לאחר זמן מה, היישום מקבל את אותו השרשור מהבריכה.
  6. מכיוון שלא ניקינו את ה- MDC בפעם האחרונה, עדיין שרשור זה מחזיק בנתונים מהביצוע הקודם.

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

גישה אחרת היא להשתמש ThreadPoolExecutor ווים ולבצע ניקוי הכרחי לאחר כל ביצוע. לשם כך, אנו יכולים להרחיב את ThreadPoolExecutor בכיתה ולדרוס את afterExecute () וו:

מחלקה ציבורית MdcAwareThreadPoolExecutor מרחיב את ThreadPoolExecutor {public MdcAwareThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandoolPlease, גודל מטען), SuperTimeTime, גודל שגיאה, מטפלת שגיאה, שגיאת עבודה, שאלה } @Override מוגן ריק לאחר ביצוע (Runnable r, Throwable t) {System.out.println ("ניקוי הקשר MDC"); MDC.clear (); org.apache.log4j.MDC.clear (); ThreadContext.clearAll (); }}

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

@ עקוף ריצה בטלנית ציבורית () {MDC.put ("transaction.id", tx.getTransactionId ()); MDC.put ("transaction.owner", tx.getSender ()); חדש Slf4TransferService (). העברה (tx.getAmount ()); }

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

ExecutorService executor = MdcAwareThreadPoolExecutor חדש (3, 3, 0, MINUTES, חדש LinkedBlockingQueue (), אשכול :: AbortPolicy חדש, חדש); TransactionFactory transactionFactory = TransactionFactory חדש (); עבור (int i = 0; i <10; i ++) {העבר tx = transactionFactory.newInstance (); משימה ניתנת להפעלה = Slf4jRunnable חדש (tx); executor.submit (משימה); } executor.shutdown ();

7. מסקנה

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

וכפי שראינו, הוא נתמך על ידי שלוש ממסגרות הרישום הנפוצות ביותר ב- Java.

כרגיל, תוכלו למצוא את המקורות ב- GitHub.


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