מבוא לפרויקט לומבוק

1. הימנע מקוד חוזר

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

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

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

 ... org.projectlombok lombok 1.18.10 סיפק ... 

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

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

2. גטרים / סטרים, קונסטרוקטורים - כל כך חוזרים על עצמם

הקפדה על מאפייני אובייקט באמצעות שיטות גטר ושירות ציבורי היא נוהג כה נפוץ בעולם ג'אווה, והרבה מסגרות נשענות על תבנית "Java Bean" בהרחבה: כיתה עם קונסטרוקטור ריק ושיטות קבל / הגדר ל"מאפיינים ".

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

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

מחלקה ציבורית @Entity מיישמת משתמשים באמצעות Serialize {private @Id Long id; // יוגדר כאשר נמשך מחרוזת פרטית שם פרטי; שם משפחה פרטי מחרוזת; גיל פרטי פרטי; משתמש ציבורי () {} משתמש ציבורי (שם מחרוזת, שם משפחה מחרוזת, גיל) {this.firstName = שם פרטי; this.lastName = lastname; this.age = גיל; } // גטרים וקובעים: ~ 30 שורות קוד נוספות}

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

תן לנו עכשיו Lombok-ize הכיתה הזאת:

@Entity @ Getter @ Setter @ NoArgsConstructor // <--- זהו המעמד הציבורי שהמשתמש מיישם ניתן להתבצע באמצעות Serialize {private @Id Long ID; // יוגדר כאשר נמשך מחרוזת פרטית שם פרטי; שם משפחה פרטי מחרוזת; גיל פרטי פרטי; משתמש ציבורי (שם מחרוזת, שם משפחה מחרוזת, int age) {this.firstName = firstName; this.lastName = lastname; this.age = גיל; }}

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

שים לב שזה ה- כֹּל קוד כיתה, אני לא משמיט שום דבר בניגוד לגרסה שלמעלה עם // גטרים וקובעים תגובה. עבור שלוש מחלקות תכונות רלוונטיות, זהו חיסכון משמעותי בקוד!

אם אתה מוסיף תכונות (מאפיינים) שלך מִשׁתַמֵשׁ בכיתה, אותו דבר יקרה: החלת את ההערות על הסוג עצמו, כך שהם יטפלו בכל השדות כברירת מחדל.

מה אם היית רוצה לחדד את הנראות של כמה נכסים? לדוגמא, אני אוהב לשמור על הישויות שלי תְעוּדַת זֶהוּת משתני שטח חֲבִילָה אוֹ מוּגָן גלויים מכיוון שהם צפויים להיקרא אך לא מוגדרים במפורש על ידי קוד יישום. פשוט השתמש בגרגר עדין יותר @ סטר לתחום מסוים זה:

פרטי @Id @Setter (AccessLevel.PROTECTED) מזהה ארוך;

3. גטר עצלנית

לעתים קרובות, יישומים צריכים לבצע פעולה יקרה ולשמור את התוצאות לשימוש עוקב.

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

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

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

לומבוק מאפשר זאת באמצעות עָצֵל פרמטר ב- @גטר ביאור ראינו למעלה.

לדוגמה, שקול מחלקה פשוטה זו:

מעמד ציבורי GetterLazy {@ Getter (עצלן = אמיתי) עסקיות מפת פרטיות סופיות = getTransactions (); מפה פרטית getTransactions () {מטמון מפה סופית = HashMap חדש (); רשימה txnRows = readTxnListFromFile (); txnRows.forEach (s -> {String [] txnIdValueTuple = s.split (DELIMETER); cache.put (txnIdValueTuple [0], Long.parseLong (txnIdValueTuple [1]));}); החזר מטמון; }}

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

אם נסתכל כעת על הקוד המהולל של מחלקה זו, נראה a שיטת getter אשר מעדכנת את המטמון אם היה ריק ואז מחזיר את הנתונים במטמון:

מעמד ציבורי GetterLazy {עסקאות AtomicReference סופיות פרטיות = AtomicReference חדש (); ציבורי GetterLazy () {} // שיטות אחרות ציבורי getTransactions מפה () {Object value = this.transactions.get (); אם (value == null) {מסונכרן (this.transactions) {value = this.transactions.get (); אם (value == null) {Map actualValue = this.readTxnsFromFile (); value = actualValue == null? this.transactions: actualValue; this.transactions.set (ערך); }}} החזר (Map) ((Map) (value == this.transactions? null: value)); }}

מעניין לציין זאת לומבוק עטף את שדה הנתונים ב- AtomicReference.זה מבטיח עדכונים אטומיים ל עסקאות שדה. ה getTransactions () השיטה גם מקפידה לקרוא את הקובץ אם עסקאות הוא ריק.

השימוש ב- עסקאות AtomicReference השדה ישירות מתוך הכיתה הוא מיואש. מומלץ להשתמש ב- getTransactions () שיטת גישה לשדה.

מסיבה זו, אם אנו משתמשים בהערה אחרת כמו לומבוק ToString באותה הכיתה, זה ישתמש getTransactions () במקום לגשת ישירות לשדה.

4. שיעורי ערך / DTO

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

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

כיתה ציבורית LoginResult {פרטי כניסה מיידי פרטיים סופיים; סופי פרטי מחרוזת authToken; פרק הסיום הפרטי סופי תקפות; כתובת אתר סופית פרטית tokenRefreshUrl; // בונה לוקח כל שדה ובודק אפסים // אביזר לקריאה בלבד, לאו דווקא כטופס get * ()}

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

@RequiredArgsConstructor @Accessors (שוטף = נכון) @Getter כיתה ציבורית LoginResult {פרטי גמר @NonNull כניסה מיידית של פרטי; גמר פרטי @NonNull מחרוזת authToken; גמר פרטי @NonNull משך זמן tokenValidity; גמר פרטי @NonNull URL tokenRefreshUrl; }

פשוט הוסף את @RequiredArgsConstructor ביאור ותקבל קונסטרוקטור לכל השדות הסופיים בכיתה, בדיוק כפי שהצהרת עליהם. מוֹסִיף @NonNull לתכונות גורם לבנאי שלנו לבדוק אפסות וזריקה NullPointerExceptions בהתאם לכך. זה יקרה גם אם השדות לא היו סופיים והוספנו @ סטר בשבילם.

אתה לא רוצה ישן משעמם לקבל*() טופס עבור הנכסים שלך? כי הוספנו @Accessors (שוטף = נכון) בדוגמה זו ל- "getters" יהיה שם שיטה זהה לתכונות: getAuthToken () פשוט הופך להיות authToken ().

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

// דמיין ששדות כבר לא היו סופיים, כעת תחזיר LoginResult חדש () .loginTs (Instant.now ()) .authToken ("asdasd"). // וכולי

5. Core Java Boilerplate

מצב נוסף בו בסופו של דבר אנו כותבים קוד שעלינו לשמור עליו הוא בעת יצירת toString (), שווים() ו hashCode () שיטות. IDEs מנסים לעזור בתבניות לייצור אוטומטי של אלה במונחים של תכונות הכיתה שלנו.

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

  • @ToString: ייצור a toString () שיטה הכוללת את כל מאפייני הכיתה. אין צורך לכתוב אחד בעצמנו ולשמור עליו כאשר אנו מעשירים את מודל הנתונים שלנו.
  • @EqualsAndHashCode: יפיק את שניהם שווים() ו hashCode () שיטות כברירת מחדל בהתחשב בכל התחומים הרלוונטיים, ועל פי סמנטיקה טובה מאוד.

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

עוד על כך: נניח שהיה לנו מִשׁתַמֵשׁ דוגמה של ישות JPA כוללת התייחסות לאירועים המשויכים למשתמש זה:

@OneToMany (mappedBy = "user") אירועי רשימה פרטיים;

לא היינו רוצים שזרקו את כל רשימת האירועים בכל פעם שאנחנו קוראים toString () שיטת המשתמש שלנו, רק בגלל שהשתמשנו ב- @ToString ביאור. אין בעיה: פשוט פרמטרים את זה כך: @ TooString (exclude = {“events”})וזה לא יקרה. זה מועיל גם כדי להימנע מהפניות מעגליות אם, למשל, UserEventל- S הייתה התייחסות ל- מִשׁתַמֵשׁ.

בשביל ה LoginResult לדוגמא, אולי נרצה להגדיר שוויון וחישוב קוד חשיש רק במונחים של האסימון עצמו ולא את התכונות הסופיות האחרות בכיתה שלנו. ואז פשוט כתוב משהו כמו @EqualsAndHashCode (of = {“authToken”}).

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

5.1. (לא) באמצעות @EqualsAndHashCode עם גופי JPA

האם להשתמש בברירת המחדל שווים() ו hashCode () או ליצור שיטות מותאמות אישית עבור ישויות JPA, הוא נושא שנדון לעתים קרובות בקרב מפתחים. ישנן מספר גישות שאנו יכולים לנקוט בהן; שלכל אחד היתרונות והחסרונות שלו.

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

בכללי, עלינו להימנע משימוש בלומבוק כדי ליצור את שווים() ו hashCode () שיטות עבור ישויות JPA שלנו!

6. תבנית הבנאי

הדברים הבאים יכולים ליצור מחלקת תצורה לדוגמה עבור לקוח REST API:

מחלקה ציבורית ApiClientConfiguration {מארח מחרוזת פרטי; נמל אינטר פרטי; שימוש בוליאני פרטי Https; connect long timeTimeout פרטי; readTimeout פרטי פרטי; שם משתמש פרטי מחרוזת; סיסמת מחרוזת פרטית; // איזה אפשרויות אחרות אתה יכול לעשות. // בונה ריק? כל השילובים? // גטררים ... וקובעים? }

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

במקום זאת, אנו יכולים לומר לכלי ליצור בּוֹנֶה דפוס, ומונע מאיתנו לכתוב תוספת בּוֹנֶה בכיתה ושיטות דומות למגדיר שוטף הקשורות פשוט על ידי הוספת ההערה של @ Builder ל- שלנו תצורת ApiClientConfiguration.

@ Builder בכיתה ציבורית ApiClientConfiguration {// ... כל השאר נשאר זהה}

השארת הגדרת הכיתה לעיל ככזו (אין להכריז על בונים ולא מגדירים + @בּוֹנֶהנוכל להשתמש בו כ:

ApiClientConfiguration config = ApiClientConfiguration.builder () .host ("api.server.com") .port (443) .useHttps (true) .connectTimeout (15_000L) .readTimeout (5_000L). Username ("myusername"). סיסמה (" סוד ") .build ();

7. נטל חריגים בדוק

הרבה ממשקי API של Java מתוכננים כך שיוכלו לזרוק מספר חריגים מסומנים שנלקח על ידי לקוח לתפוס או להכריז בפני זורק. כמה פעמים הפכת את החריגים האלה שאתה יודע שלא יקרה למשהו כזה?

ציבור מחרוזת resourceAsString () {try (InputStream הוא = this.getClass (). getResourceAsStream ("sure_in_my_jar.txt")) {BufferedReader br = חדש BufferedReader (InputStreamReader חדש (הוא, "UTF-8")); להחזיר br.lines (). collect (Collectors.joining ("\ n")); } לתפוס (IOException | לשעבר CharsetException לא נתמך) {// אם זה יקרה אי פעם, זה באג. לזרוק RuntimeException חדש (לשעבר); <--- מתמצת באקס של זמן ריצה. }}

אם אתה רוצה להימנע מדפוסי קוד זה מכיוון שהמהדר לא יהיה מאושר אחרת (והרי אתה לָדַעַת השגיאות המסומנות אינן יכולות לקרות), השתמש בשם המתאים @ SneakyTrow:

@SneakyThrows מחרוזת resourceAsString ציבורית () {try (InputStream is = this.getClass (). GetResourceAsStream ("sure_in_my_jar.txt")) {BufferedReader br = BufferedReader חדש (InputStreamReader חדש (הוא, "UTF-8")); להחזיר br.lines (). collect (Collectors.joining ("\ n")); }}

8. וודאו שהמשאבים שלכם משוחררים

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

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

@Cleanup InputStream הוא = this.getClass (). GetResourceAsStream ("res.txt");

לשיטת השחרור שלך יש שם אחר? אין בעיה, פשוט התאם אישית את ההערה:

@Cleanup ("להיפטר") JFrame mainFrame = JFrame חדש ("חלון ראשי");

9. הוסף הערה לשיעור שלך כדי להשיג לוגר

רבים מאיתנו מוסיפים הצהרות רישום לקוד שלנו במשורה על ידי יצירת מופע של כּוֹרֵת עֵצִים ממסגרת הבחירה שלנו. תגיד, SLF4J:

מחלקה ציבורית ApiClientConfiguration {לוגר סטטי פרטי LOG = LoggerFactory.getLogger (ApiClientConfiguration.class); // LOG.debug (), LOG.info (), ...}

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

@ Slf4j // או: @Log @CommonsLog @ Log4j @ Log4j2 @ XSlf4j מחלקה ציבורית ApiClientConfiguration {// log.debug (), log.info (), ...}

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

10. כתוב שיטות חוט בטוח יותר

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

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

@ סינכרון ציבורי / * טוב יותר מ: מסונכרן * / בטל putValueInCache (מפתח מחרוזת, ערך אובייקט) {// מה שיהיה כאן יהיה קוד בטוח לשרשור}

11. אוטומציה של הרכבת אובייקטים

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

Lombok's @Delegate מועיל מאוד כשרוצים להשתמש בדפוס תכנות זה. הבה נבחן דוגמה:

  • אנחנו רוצים מִשׁתַמֵשׁs ו- צרכןכדי לחלוק כמה תכונות נפוצות עבור שמות ומספר טלפון
  • אנו מגדירים הן ממשק והן מחלקת מתאם עבור שדות אלה
  • יהיה לנו המודלים שלנו ליישם את הממשק ו @נָצִיג למתאם שלהם, ביעילות הַלחָנָה אותם עם פרטי הקשר שלנו

ראשית, בואו נגדיר ממשק:

ממשק ציבורי HasContactInformation {String getFirstName (); בטל setFirstName (מחרוזת firstName); מחרוזת getFullName (); מחרוזת getLastName (); בטל setLastName (שם משפחה מחרוזת); מחרוזת getPhoneNr (); בטל setPhoneNr (String phoneNr); }

ועכשיו מתאם כ תמיכה מעמד:

מחלקה ציבורית @Data ContactInformationSupport מיישם את HasContactInformation {פרטי מחרוזת שם פרטי; שם משפחה פרטי מחרוזת; פרטי מחרוזת טלפון; מחרוזת ציבורית @Override getFullName () {return getFirstName () + "" + getLastName (); }}

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

מחלקה ציבורית משתמש ביישום HasContactInformation {// מאפיינים ספציפיים למשתמש האחרים @Delegate (types = {HasContactInformation.class}) פרטי פרטי ContactInformationSupport contactInformation = חדש ContactInformationSupport (); // המשתמש עצמו יישם את כל פרטי הקשר על ידי המשלחת}

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

12. מתגולל לומבוק גב?

תשובה קצרה: בכלל לא ממש.

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

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

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

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

13. מסקנה

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

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

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

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


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