سلام! امروز شما را با JNDI آشنا می کنیم. بیایید دریابیم که چیست، چرا به آن نیاز است، چگونه کار می کند، چگونه می توانیم با آن کار کنیم. و سپس یک تست واحد Spring Boot می نویسیم که در داخل آن با همین JNDI بازی می کنیم. استفاده از JNDI در جاوا - 1

معرفی. خدمات نامگذاری و دایرکتوری

قبل از پرداختن به JNDI، بیایید بفهمیم که خدمات نامگذاری و دایرکتوری چیست. بارزترین مثال از چنین سرویسی، سیستم فایل در هر رایانه شخصی، لپ تاپ یا گوشی هوشمند است. سیستم فایل فایل ها را (به اندازه کافی عجیب) مدیریت می کند. فایل ها در چنین سیستم هایی در یک ساختار درختی گروه بندی می شوند. هر فایل یک نام کامل منحصر به فرد دارد، به عنوان مثال: C:\windows\notepad.exe. لطفاً توجه داشته باشید: نام کامل فایل مسیری است از یک نقطه ریشه (درایو C) به خود فایل (notepad.exe). گره های میانی در چنین زنجیره ای دایرکتوری ها (دایرکتوری ویندوز) هستند. فایل های داخل دایرکتوری ها دارای ویژگی هایی هستند. به عنوان مثال، "Hidden"، "Read-Only"، و غیره. شرح دقیق یک چیز ساده مانند یک سیستم فایل به درک بهتر تعریف خدمات نامگذاری و فهرست کمک می کند. بنابراین، یک سرویس نام و دایرکتوری سیستمی است که نگاشت بسیاری از نام ها به بسیاری از اشیاء را مدیریت می کند. در سیستم فایل ما، ما با نام فایل‌هایی که اشیاء را پنهان می‌کنند، تعامل داریم - خود فایل‌ها در قالب‌های مختلف. در سرویس نامگذاری و فهرست، اشیاء نامگذاری شده در یک ساختار درختی سازماندهی می شوند. و اشیاء دایرکتوری دارای ویژگی هستند. نمونه دیگری از خدمات نام و فهرست، DNS (سیستم نام دامنه) است. این سیستم نقشه برداری بین نام دامنه های قابل خواندن توسط انسان (به عنوان مثال، https://javarush.com/) و آدرس های IP قابل خواندن توسط ماشین (به عنوان مثال، 18.196.51.113) را مدیریت می کند. علاوه بر DNS و فایل سیستم‌ها، خدمات دیگری نیز وجود دارد، مانند:

JNDI

JNDI یا Java Naming and Directory Interface یک API جاوا برای دسترسی به خدمات نامگذاری و دایرکتوری است. JNDI یک API است که مکانیسم یکنواختی را برای یک برنامه جاوا برای تعامل با سرویس‌های نامگذاری و دایرکتوری مختلف فراهم می‌کند. در زیر هود، ادغام بین JNDI و هر سرویس داده شده با استفاده از یک رابط ارائه دهنده خدمات (SPI) انجام می شود. SPI اجازه می دهد تا سرویس های مختلف نامگذاری و دایرکتوری به طور شفاف متصل شوند و به یک برنامه جاوا اجازه می دهد تا از JNDI API برای دسترسی به خدمات متصل استفاده کند. شکل زیر معماری JNDI را نشان می دهد: استفاده از JNDI در جاوا - 2

منبع: Oracle Java Tutorials

JNDI. معنی در کلمات ساده

سوال اصلی این است: چرا به JNDI نیاز داریم؟ JNDI مورد نیاز است تا بتوانیم یک شی جاوا را از "ثبت" اشیاء از کد جاوا با نام شی محدود شده به این شیء دریافت کنیم. بیایید بیانیه بالا را به تزها تقسیم کنیم تا فراوانی کلمات تکراری ما را گیج نکند:
  1. در نهایت باید یک شی جاوا بدست آوریم.
  2. ما این شی را از برخی از رجیستری دریافت خواهیم کرد.
  3. دسته ای از اشیاء در این رجیستری وجود دارد.
  4. هر شیء در این رجیستری یک نام منحصر به فرد دارد.
  5. برای دریافت یک شی از رجیستری، باید یک نام را در درخواست خود وارد کنیم. انگار می‌گوید: «لطفاً آنچه را که داری به فلان اسم به من بده.»
  6. ما نه تنها می‌توانیم اشیاء را با نام آنها از رجیستری بخوانیم، بلکه می‌توانیم اشیاء را در این رجیستری با نام‌های خاصی ذخیره کنیم (به نحوی که به آنجا ختم می‌شوند).
بنابراین، ما نوعی رجیستری یا ذخیره‌سازی شی یا درخت JNDI داریم. در مرحله بعد، با استفاده از یک مثال، سعی می کنیم معنای JNDI را درک کنیم. شایان ذکر است که در اکثر موارد JNDI در توسعه سازمانی استفاده می شود. و چنین برنامه هایی در داخل برخی از سرورهای برنامه کار می کنند. این سرور می تواند یک سرور برنامه کاربردی Java EE یا یک کانتینر سرولت مانند تامکت یا هر ظرف دیگری باشد. خود رجیستری شی، یعنی درخت JNDI، معمولاً در داخل این سرور برنامه قرار دارد. دومی همیشه ضروری نیست (شما می توانید چنین درختی را به صورت محلی داشته باشید)، اما معمولی ترین است. درخت JNDI را می توان توسط یک شخص خاص (مدیر سیستم یا متخصص DevOps) مدیریت کرد که اشیاء را با نام آنها "در رجیستری" ذخیره می کند. هنگامی که برنامه ما و درخت JNDI در داخل یک کانتینر قرار می گیرند، می توانیم به راحتی به هر شی جاوا که در چنین رجیستری ذخیره شده است دسترسی داشته باشیم. علاوه بر این، رجیستری و برنامه ما را می توان در کانتینرهای مختلف و حتی در ماشین های فیزیکی مختلف قرار داد. JNDI حتی پس از آن به شما امکان می دهد از راه دور به اشیاء جاوا دسترسی داشته باشید. مورد معمولی مدیر سرور Java EE یک شی را در رجیستری قرار می دهد که اطلاعات لازم برای اتصال به پایگاه داده را ذخیره می کند. بر این اساس، برای کار با پایگاه داده، به سادگی شی مورد نیاز را از درخت JNDI درخواست کرده و با آن کار می کنیم. خیلی راحت است. راحتی نیز در این واقعیت نهفته است که در توسعه سازمانی محیط های مختلفی وجود دارد. سرورهای تولیدی و سرورهای آزمایشی وجود دارد (و اغلب بیش از 1 سرور آزمایشی وجود دارد). سپس با قرار دادن یک آبجکت برای اتصال به دیتابیس در هر سرور در داخل JNDI و استفاده از این شی در داخل اپلیکیشن خود، هنگام استقرار اپلیکیشن خود از یک سرور (تست، انتشار) به سرور دیگر، نیازی به تغییر چیزی نخواهیم داشت. دسترسی به پایگاه داده در همه جا وجود خواهد داشت. البته مثال تا حدودی ساده شده است، اما امیدوارم به شما کمک کند تا بهتر بفهمید چرا به JNDI نیاز است. در ادامه، با برخی از عناصر حمله، با JNDI در جاوا بیشتر آشنا خواهیم شد.

JNDI API

JNDI در بستر Java SE ارائه شده است. برای استفاده از JNDI، باید کلاس‌های JNDI و همچنین یک یا چند ارائه‌دهنده خدمات را وارد کنید تا به خدمات نام‌گذاری و دایرکتوری دسترسی داشته باشید. JDK شامل ارائه دهندگان خدمات برای خدمات زیر است:
  • پروتکل دسترسی به دایرکتوری سبک (LDAP)؛
  • معماری کارگزار درخواست شی مشترک (CORBA);
  • خدمات نام مشترک شیء (COS)؛
  • رجیستری فراخوانی روش راه دور جاوا (RMI).
  • سرویس نام دامنه (DNS).
کد JNDI API به چندین بسته تقسیم می شود:
  • javax.name;
  • javax.name.directory;
  • javax.name.ldap;
  • javax.name.event;
  • javax.name.spi.
ما معرفی خود را به JNDI با دو رابط - Name و Context، که دارای عملکرد کلیدی JNDI هستند، آغاز خواهیم کرد.

نام رابط

رابط Name به شما امکان می دهد نام اجزا و همچنین نحو نامگذاری JNDI را کنترل کنید. در JNDI، تمام عملیات نام و دایرکتوری نسبت به زمینه انجام می شود. هیچ ریشه مطلقی وجود ندارد. بنابراین، JNDI یک InitialContext را تعریف می کند، که نقطه شروعی را برای عملیات نامگذاری و دایرکتوری فراهم می کند. پس از دسترسی به بافت اولیه، می توان از آن برای جستجوی اشیا و سایر زمینه ها استفاده کرد.
Name objectName = new CompositeName("java:comp/env/jdbc");
در کد بالا، نامی را تعریف کردیم که تحت آن یک شی قرار دارد (ممکن است قرار نداشته باشد، اما روی آن حساب می کنیم). هدف نهایی ما به دست آوردن یک مرجع برای این شی و استفاده از آن در برنامه است. بنابراین، نام شامل چندین بخش (یا نشانه) است که با یک اسلش از هم جدا شده اند. چنین نشانه هایی را زمینه می نامند. اولین مورد صرفاً زمینه است، همه موارد بعدی زیر زمینه هستند (از این پس به عنوان زیر زمینه نامیده می شود). درک زمینه‌ها آسان‌تر است اگر آنها را مشابه فهرست‌ها یا فهرست‌ها یا فقط پوشه‌های معمولی در نظر بگیرید. زمینه ریشه، پوشه ریشه است. Subcontext یک زیرپوشه است. با اجرای کد زیر می‌توانیم تمام مؤلفه‌ها (زمینه و زمینه‌های فرعی) یک نام معین را ببینیم:
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. همه چیز بستگی به محیطی دارد که برنامه جاوا در آن قرار دارد. به عنوان مثال، اگر یک برنامه جاوا و یک درخت 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 (رابط ارائه دهنده سرویس) است که به کمک آن یکپارچگی بین جاوا و سرویس نامگذاری انجام می شود. JDK استاندارد با چندین SPI مختلف (ما آنها را در بالا لیست کردیم) ارائه می شود که هر کدام برای اهداف نمایشی جالب توجه نیستند. بالا بردن یک برنامه JNDI و جاوا در داخل یک کانتینر تا حدودی جالب است. با این حال، نویسنده این مقاله یک فرد تنبل است، بنابراین برای نشان دادن نحوه عملکرد JNDI، او مسیر کمترین مقاومت را انتخاب کرد: JNDI را در یک تست واحد برنامه کاربردی SpringBoot اجرا کنید و با استفاده از یک هک کوچک از Spring Framework به زمینه JNDI دسترسی پیدا کنید. بنابراین، طرح ما:
  • بیایید یک پروژه خالی بهار بوت بنویسیم.
  • بیایید یک تست واحد در داخل این پروژه ایجاد کنیم.
  • در داخل آزمون کار با JNDI را نشان خواهیم داد:
    • دسترسی به زمینه؛
    • اتصال (پیوند) برخی از شی ها تحت نامی در JNDI.
    • دریافت شی با نام آن (جستجو)؛
    • بیایید بررسی کنیم که شی تهی نباشد.
بیایید به ترتیب شروع کنیم. File->New->Project... استفاده از JNDI در جاوا - 3 سپس، مورد Spring Initializr را انتخاب کنید : استفاده از JNDI در جاوا - 4ابرداده مربوط به پروژه را پر کنید: استفاده از JNDI در جاوا - 5سپس اجزای Spring Framework مورد نیاز را انتخاب کنید. ما برخی از آبجکت های DataSource را متصل می کنیم، بنابراین برای کار با پایگاه داده به اجزایی نیاز داریم:
  • JDBC API;
  • H2 DDatabase.
استفاده از JNDI در جاوا - 6بیایید مکان را در سیستم فایل تعیین کنیم: استفاده از JNDI در جاوا - 7و پروژه ایجاد می شود. در واقع یک تست واحد به طور خودکار برای ما تولید شد که از آن برای اهداف نمایشی استفاده خواهیم کرد. در زیر ساختار پروژه و آزمایشی که نیاز داریم آمده است: استفاده از JNDI در جاوا - 8بیایید شروع به نوشتن کد در داخل تست contextLoads کنیم. یک هک کوچک از Spring که در بالا مورد بحث قرار گرفت، کلاس است 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در این خط، شی کلاس را به name متصل کرده ایم java:comp/env/jdbc/datasource. در مرحله بعد، می‌توانیم شی را با نام از متن دریافت کنیم. ما چاره ای نداریم جز اینکه شیئی را که قرار داده ایم بدست آوریم، زیرا هیچ شیء دیگری در متن وجود ندارد =(
final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");
حالا بیایید بررسی کنیم که DataSource ما یک اتصال دارد (اتصال، اتصال یا اتصال یک کلاس جاوا است که برای کار با پایگاه داده طراحی شده است):
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=

نتیجه

امروز ما به JNDI نگاه کردیم. ما در مورد خدمات نامگذاری و دایرکتوری یاد گرفتیم که JNDI یک API جاوا است که به شما امکان می دهد به طور یکسان با سرویس های مختلف از یک برنامه جاوا تعامل داشته باشید. یعنی با کمک JNDI می توانیم اشیایی را در درخت JNDI با نام خاصی ثبت کنیم و همین اشیاء را با نام دریافت کنیم. به عنوان یک کار پاداش، می توانید نمونه ای از نحوه کار JNDI را اجرا کنید. شیء دیگری را به متن متصل کنید و سپس این شی را با نام بخوانید.