שלום! היום נציג בפניכם את JNDI. בואו לגלות מה זה, למה זה נחוץ, איך זה עובד, איך אנחנו יכולים לעבוד עם זה. ואז נכתוב מבחן יחידת Spring Boot, שבתוכו נשחק עם ה-JNDI הזה.
מבוא. שירותי מתן שמות וספריות
לפני שנצלול לתוך JNDI, בואו נבין מה הם שירותי מתן שמות וספריות. הדוגמה הברורה ביותר לשירות כזה היא מערכת הקבצים בכל מחשב, מחשב נייד או סמארטפון. מערכת הקבצים מנהלת (באופן מוזר) קבצים. קבצים במערכות כאלה מקובצים במבנה עץ. לכל קובץ יש שם מלא ייחודי, לדוגמה: C:\windows\notepad.exe. שימו לב: שם הקובץ המלא הוא הנתיב מנקודת שורש כלשהי (כונן C) לקובץ עצמו (notepad.exe). צמתי הביניים בשרשרת כזו הם ספריות (ספריית windows). לקבצים בתוך ספריות יש תכונות. לדוגמה, "נסתר", "לקריאה בלבד" וכו'. תיאור מפורט של דבר פשוט כמו מערכת קבצים יעזור להבין טוב יותר את ההגדרה של שירותי שמות וספריות. לכן, שירות שמות וספריות הוא מערכת המנהלת את המיפוי של שמות רבים לאובייקטים רבים. במערכת הקבצים שלנו, אנו מקיימים אינטראקציה עם שמות קבצים שמסתירים אובייקטים - הקבצים עצמם בפורמטים שונים. בשירות השמות והספריות, אובייקטים בעלי שם מאורגנים במבנה עץ. ולאובייקטי ספרייה יש תכונות. דוגמה נוספת לשירות שם וספריות היא DNS (מערכת שמות דומיין). מערכת זו מנהלת את המיפוי בין שמות דומיינים הניתנים לקריאה אנושית (לדוגמה, https://javarush.com/) לבין כתובות IP הניתנות לקריאה במכונה (לדוגמה, 18.196.51.113). מלבד DNS ומערכות קבצים, ישנם הרבה שירותים אחרים, כגון:JNDI
JNDI, או Java Naming and Directory Interface, הוא Java API לגישה לשירותי שמות וספריות. JNDI הוא API המספק מנגנון אחיד לתוכנת Java לקיים אינטראקציה עם שירותי שמות וספריות שונים. מתחת למכסה המנוע, האינטגרציה בין JNDI לכל שירות נתון מתבצעת באמצעות ממשק ספק שירות (SPI). SPI מאפשר חיבור שקוף לשירותי שמות וספריות שונים, מה שמאפשר ליישום Java להשתמש ב-JNDI API כדי לגשת לשירותים המחוברים. האיור שלהלן ממחיש את ארכיטקטורת JNDI:מקור: Oracle Java Tutorials
JNDI. הכוונה במילים פשוטות
השאלה העיקרית היא: למה אנחנו צריכים JNDI? יש צורך ב-JNDI כדי שנוכל לקבל אובייקט Java מ"רישום" כלשהו של אובייקטים מקוד ג'אווה לפי שם האובייקט המחובר לאובייקט זה. בואו נחלק את ההצהרה לעיל לתזות כדי ששפע המילים החוזרות לא יבלבלו אותנו:- בסופו של דבר אנחנו צריכים להשיג אובייקט Java.
- נקבל את האובייקט הזה מאיזה רישום.
- יש חבורה של אובייקטים ברישום הזה.
- לכל אובייקט ברישום זה יש שם ייחודי.
- כדי לקבל חפץ מהרישום, עלינו להעביר שם בבקשה שלנו. כאילו אומר: "בבקשה תן לי מה שיש לך תחת שם כזה ואחר".
- אנחנו יכולים לא רק לקרוא אובייקטים לפי שמם מהרישום, אלא גם לשמור אובייקטים ברישום זה תחת שמות מסוימים (איכשהו הם מגיעים לשם).
JNDI API
JNDI מסופק בתוך פלטפורמת Java SE. כדי להשתמש ב-JNDI, עליך לייבא מחלקות JNDI, כמו גם ספק שירות אחד או יותר כדי לגשת לשירותי שמות וספריות. ה-JDK כולל ספקי שירותים עבור השירותים הבאים:- פרוטוקול גישה למדריך קל משקל (LDAP);
- Common Object Request Broker Architecture (CORBA);
- שירות שמות של Common Object Services (COS);
- Registration Remote Method Invocation (RMI) של Java;
- שירות שמות דומיין (DNS).
- javax.name;
- javax.naming.directory;
- javax.naming.ldap;
- javax.naming.event;
- javax.naming.spi.
שם ממשק
ממשק השם מאפשר לך לשלוט בשמות הרכיבים כמו גם בתחביר מתן השמות של JNDI. ב-JNDI, כל פעולות השם והספרייה מבוצעות ביחס להקשר. אין שורשים מוחלטים. לכן, JNDI מגדיר InitialContext, המספק נקודת התחלה לפעולות שמות וספריות. לאחר גישה להקשר הראשוני, ניתן להשתמש בו כדי לחפש אובייקטים והקשרים אחרים.Name objectName = new CompositeName("java:comp/env/jdbc");
בקוד למעלה, הגדרנו שם כלשהו תחתיו נמצא אובייקט כלשהו (אולי הוא לא נמצא, אבל אנחנו סומכים עליו). המטרה הסופית שלנו היא להשיג הפניה לאובייקט זה ולהשתמש בו בתוכנית שלנו. אז השם מורכב מכמה חלקים (או אסימונים), מופרדים על ידי קו נטוי. אסימונים כאלה נקראים הקשרים. הראשון הוא פשוט הקשר, כל אלה שלאחר מכן הם תת-הקשר (להלן מכונה תת-הקשר). קל יותר להבין הקשרים אם אתה חושב עליהם כאנלוגיים לספריות או ספריות, או סתם תיקיות רגילות. הקשר השורש הוא תיקיית השורש. תת-הקשר היא תיקיית משנה. אנו יכולים לראות את כל הרכיבים (הקשר ותת-הקשרים) של שם נתון על ידי הפעלת הקוד הבא:
Enumeration<String> elements = objectName.getAll();
while(elements.hasMoreElements()) {
System.out.println(elements.nextElement());
}
הפלט יהיה כדלקמן:
java:comp
env
jdbc
הפלט מראה שהאסימונים בשם מופרדים זה מזה על ידי קו נטוי (עם זאת, הזכרנו זאת). לכל אסימון שם יש אינדקס משלו. אינדקס אסימון מתחיל ב-0. להקשר השורש יש אינדקס אפס, להקשר הבא יש אינדקס 1, הבא 2 וכו'. נוכל לקבל את שם המשנה לפי האינדקס שלו:
System.out.println(objectName.get(1)); // -> env
אנו יכולים גם להוסיף אסימונים נוספים (בסוף או במיקום ספציפי באינדקס):
objectName.add("sub-context"); // Добавит sub-context в конец
objectName.add(0, "context"); // Добавит context в налачо
רשימה מלאה של שיטות ניתן למצוא בתיעוד הרשמי .
הקשר ממשק
ממשק זה מכיל קבוצה של קבועים לאתחול הקשר, וכן קבוצה של שיטות ליצירה ומחיקה של הקשרים, קשירת אובייקטים לשם וחיפוש ואחזור אובייקטים. בואו נסתכל על כמה מהפעולות המבוצעות באמצעות ממשק זה. הפעולה הנפוצה ביותר היא חיפוש אובייקט לפי שם. זה נעשה באמצעות שיטות:Object lookup(String name)
Object lookup(Name name)
bind
:
void bind(Name name, Object obj)
void bind(String name, Object obj)
Object
הפעולה ההפוכה של כריכה - ביטול קשירה של אובייקט לשם, מתבצעת בשיטות unbind
:
void unbind(Name name)
void unbind(String name)
InitialContext
InitialContext
היא מחלקה המייצגת את אלמנט השורש של עץ JNDI ומיישמת את ה- Context
. אתה צריך לחפש אובייקטים לפי שם בתוך עץ JNDI ביחס לצומת מסוים. צומת השורש של העץ יכול לשמש כצומת כזה InitialContext
. מקרה שימוש טיפוסי עבור JNDI הוא:
- לקבל
InitialContext
. - השתמש
InitialContext
כדי לאחזר אובייקטים לפי שם מעץ JNDI.
InitialContext
. הכל תלוי בסביבה שבה ממוקמת תוכנית Java. לדוגמה, אם תוכנית Java ועץ JNDI פועלות בתוך אותו שרת יישומים, זה InitialContext
די פשוט להשיג:
InitialContext context = new InitialContext();
אם זה לא המקרה, קבלת הקשר הופכת קצת יותר קשה. לפעמים יש צורך להעביר רשימה של מאפייני סביבה כדי לאתחל את ההקשר:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.fscontext.RefFSContextFactory");
Context ctx = new InitialContext(env);
הדוגמה שלמעלה מדגימה את אחת הדרכים האפשריות לאתחל הקשר ואינה נושאת שום עומס סמנטי אחר. אין צורך לצלול לקוד בפירוט.
דוגמה לשימוש ב-JNDI בתוך בדיקת יחידת SpringBoot
לעיל אמרנו שכדי ש-JNDI יתקשר עם שירות השמות והספריות, יש צורך ב-SPI (Service Provider Interface) בהישג יד, בעזרתו תתבצע אינטגרציה בין Java לשירות השמות. ה-JDK הסטנדרטי מגיע עם מספר SPIs שונים (מנינו אותם למעלה), שכל אחד מהם אינו מעניין במיוחד למטרות הדגמה. העלאת אפליקציית JNDI ו-Java בתוך קונטיינר היא קצת מעניינת. עם זאת, מחבר המאמר הזה הוא אדם עצלן, אז כדי להדגים איך JNDI עובד, הוא בחר בדרך של ההתנגדות הקטנה ביותר: הפעל את JNDI בתוך בדיקת יחידת יישום SpringBoot וגישה להקשר JNDI באמצעות פריצה קטנה מ-Spring Framework. אז התוכנית שלנו:- בואו נכתוב פרויקט Spring Boot ריק.
- בואו ניצור מבחן יחידה בתוך הפרויקט הזה.
- בתוך המבחן נדגים עבודה עם JNDI:
- לקבל גישה להקשר;
- bind (bind) אובייקט כלשהו תחת שם כלשהו ב-JNDI;
- קבל את האובייקט לפי שמו (חיפוש);
- בוא נבדוק שהאובייקט אינו null.
- JDBC API;
- H2 DDatabase.
SimpleNamingContextBuilder
. מחלקה זו נועדה להעלות בקלות את JNDI בתוך בדיקות יחידה או יישומים עצמאיים. בוא נכתוב את הקוד כדי לקבל את ההקשר:
final SimpleNamingContextBuilder simpleNamingContextBuilder
= new SimpleNamingContextBuilder();
simpleNamingContextBuilder.activate();
final InitialContext context = new InitialContext();
שתי שורות הקוד הראשונות יאפשרו לנו לאתחל בקלות את ההקשר של JNDI מאוחר יותר. בלעדיהם, InitialContext
ייגרם חריג בעת יצירת מופע: javax.naming.NoInitialContextException
. כתב ויתור. הכיתה SimpleNamingContextBuilder
היא כיתה שהוצאה משימוש. והדוגמה הזו נועדה להראות איך אתה יכול לעבוד עם JNDI. אלו אינן שיטות עבודה מומלצות לשימוש בבדיקות JNDI בתוך יחידות. ניתן לומר שזהו קב לבניית הקשר והדגמת כריכה ושליפה של אובייקטים מ-JNDI. לאחר שקיבלנו הקשר, נוכל לחלץ ממנו אובייקטים או לחפש אובייקטים בהקשר. אין עדיין אובייקטים ב-JNDI, אז זה יהיה הגיוני לשים שם משהו. לדוגמה, DriverManagerDataSource
:
context.bind("java:comp/env/jdbc/datasource", new DriverManagerDataSource("jdbc:h2:mem:mydb"));
בשורה זו, קשרנו את אובייקט המחלקה DriverManagerDataSource
לשם java:comp/env/jdbc/datasource
. לאחר מכן, נוכל לקבל את האובייקט מההקשר לפי שם. אין לנו ברירה אלא לקבל את האובייקט ששמנו, כי אין אובייקטים אחרים בהקשר =(
final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");
עכשיו בואו נבדוק שיש ל-DataSource שלנו חיבור (חיבור, חיבור או חיבור הם מחלקה של Java שנועדה לעבוד עם מסד נתונים):
assert ds.getConnection() != null;
System.out.println(ds.getConnection());
אם עשינו הכל נכון, הפלט יהיה בערך כך:
conn1: url=jdbc:h2:mem:mydb user=
ראוי לומר שחלק מהשורות של קוד עשויות לגרום חריגים. השורות הבאות נזרקות javax.naming.NamingException
:
simpleNamingContextBuilder.activate()
new InitialContext()
context.bind(...)
context.lookup(...)
DataSource
אפשר לזרוק אותו java.sql.SQLException
. בהקשר זה, יש צורך לבצע את הקוד בתוך בלוק try-catch
, או לציין בחתימה של יחידת הבדיקה שהיא יכולה לזרוק חריגים. להלן הקוד המלא של כיתת המבחן:
@SpringBootTest
class JndiExampleApplicationTests {
@Test
void contextLoads() {
try {
final SimpleNamingContextBuilder simpleNamingContextBuilder
= new SimpleNamingContextBuilder();
simpleNamingContextBuilder.activate();
final InitialContext context = new InitialContext();
context.bind("java:comp/env/jdbc/datasource", new DriverManagerDataSource("jdbc:h2:mem:mydb"));
final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");
assert ds.getConnection() != null;
System.out.println(ds.getConnection());
} catch (SQLException | NamingException e) {
e.printStackTrace();
}
}
}
לאחר הפעלת הבדיקה, תוכל לראות את היומנים הבאים:
o.s.m.jndi.SimpleNamingContextBuilder : Activating simple JNDI environment
o.s.mock.jndi.SimpleNamingContext : Static JNDI binding: [java:comp/env/jdbc/datasource] = [org.springframework.jdbc.datasource.DriverManagerDataSource@4925f4f5]
conn1: url=jdbc:h2:mem:mydb user=
GO TO FULL VERSION