JavaRush /Blog Java /Random-VI /Sử dụng JNDI trong Java
Анзор Кармов
Mức độ
Санкт-Петербург

Sử dụng JNDI trong Java

Xuất bản trong nhóm
Xin chào! Hôm nay chúng tôi sẽ giới thiệu với bạn về JNDI. Hãy cùng tìm hiểu xem nó là gì, tại sao nó lại cần thiết, nó hoạt động như thế nào và chúng ta có thể làm việc với nó như thế nào. Và sau đó chúng ta sẽ viết một bài kiểm thử đơn vị Spring Boot, trong đó chúng ta sẽ thử nghiệm chính JNDI này. Sử dụng JNDI trong Java - 1

Giới thiệu. Dịch vụ đặt tên và thư mục

Trước khi đi sâu vào JNDI, hãy tìm hiểu dịch vụ đặt tên và thư mục là gì. Ví dụ rõ ràng nhất về dịch vụ như vậy là hệ thống tệp trên bất kỳ PC, máy tính xách tay hoặc điện thoại thông minh nào. Hệ thống tập tin quản lý các tập tin (đủ kỳ lạ). Các tập tin trong hệ thống như vậy được nhóm lại theo cấu trúc cây. Mỗi file có một tên đầy đủ duy nhất, ví dụ: C:\windows\notepad.exe. Xin lưu ý: tên tệp đầy đủ là đường dẫn từ điểm gốc nào đó (ổ C) đến chính tệp đó (notepad.exe). Các nút trung gian trong chuỗi như vậy là các thư mục (thư mục windows). Các tập tin bên trong thư mục có thuộc tính. Ví dụ: "Ẩn", "Chỉ đọc", v.v. Mô tả chi tiết về một thứ đơn giản như hệ thống tệp sẽ giúp hiểu rõ hơn về định nghĩa của dịch vụ đặt tên và thư mục. Vì vậy, dịch vụ tên và thư mục là một hệ thống quản lý việc ánh xạ nhiều tên tới nhiều đối tượng. Trong hệ thống tệp của chúng tôi, chúng tôi tương tác với các tên tệp ẩn đối tượng—chính các tệp đó ở nhiều định dạng khác nhau. Trong dịch vụ đặt tên và thư mục, các đối tượng được đặt tên được tổ chức thành cấu trúc cây. Và các đối tượng thư mục có thuộc tính. Một ví dụ khác về dịch vụ tên và thư mục là DNS (Hệ thống tên miền). Hệ thống này quản lý việc ánh xạ giữa các tên miền mà con người có thể đọc được (ví dụ: https://javarush.com/) và các địa chỉ IP mà máy có thể đọc được (ví dụ: 18.196.51.113). Ngoài DNS và hệ thống file còn có rất nhiều dịch vụ khác như:

JNDI

JNDI, hay Giao diện thư mục và đặt tên Java, là một API Java để truy cập các dịch vụ đặt tên và thư mục. JNDI là một API cung cấp cơ chế thống nhất để chương trình Java tương tác với các dịch vụ đặt tên và thư mục khác nhau. Về cơ bản, việc tích hợp giữa JNDI và bất kỳ dịch vụ cụ thể nào đều được thực hiện bằng Giao diện nhà cung cấp dịch vụ (SPI). SPI cho phép các dịch vụ đặt tên và thư mục khác nhau được kết nối một cách minh bạch, cho phép ứng dụng Java sử dụng API JNDI để truy cập các dịch vụ được kết nối. Hình dưới đây minh họa kiến ​​trúc JNDI: Sử dụng JNDI trong Java - 2

Nguồn: Hướng dẫn về Oracle Java

JNDI. Ý nghĩa trong những từ đơn giản

Câu hỏi chính là: tại sao chúng ta cần JNDI? JNDI là cần thiết để chúng ta có thể lấy một đối tượng Java từ một số “Đăng ký” đối tượng từ mã Java theo tên của đối tượng được liên kết với đối tượng này. Chúng ta hãy chia câu nói trên thành các luận điểm để lượng từ lặp đi lặp lại không làm chúng ta bối rối:
  1. Cuối cùng chúng ta cần có một đối tượng Java.
  2. Chúng tôi sẽ lấy đối tượng này từ một số sổ đăng ký.
  3. Có một loạt các đối tượng trong sổ đăng ký này.
  4. Mỗi đối tượng trong sổ đăng ký này có một tên duy nhất.
  5. Để lấy một đối tượng từ sổ đăng ký, chúng tôi phải chuyển tên trong yêu cầu của mình. Như muốn nói: “Xin hãy cho tôi biết những gì bạn có dưới cái tên như vậy và như vậy.”
  6. Chúng ta không chỉ có thể đọc các đối tượng theo tên của chúng từ sổ đăng ký mà còn có thể lưu các đối tượng trong sổ đăng ký này dưới những tên nhất định (bằng cách nào đó chúng vẫn ở đó).
Vì vậy, chúng tôi có một số loại sổ đăng ký hoặc lưu trữ đối tượng hoặc Cây JNDI. Tiếp theo, bằng một ví dụ, chúng ta hãy cố gắng hiểu ý nghĩa của JNDI. Điều đáng chú ý là phần lớn JNDI được sử dụng trong quá trình phát triển Doanh nghiệp. Và những ứng dụng như vậy hoạt động bên trong một số máy chủ ứng dụng. Máy chủ này có thể là một số Máy chủ ứng dụng Java EE hoặc một bộ chứa servlet như Tomcat hoặc bất kỳ bộ chứa nào khác. Bản thân sổ đăng ký đối tượng, tức là Cây JNDI, thường nằm bên trong máy chủ ứng dụng này. Cái sau không phải lúc nào cũng cần thiết (bạn có thể có một cây như vậy ở địa phương), nhưng nó là điển hình nhất. Cây JNDI có thể được quản lý bởi một người đặc biệt (quản trị viên hệ thống hoặc chuyên gia DevOps), người sẽ “lưu vào sổ đăng ký” các đối tượng bằng tên của họ. Khi ứng dụng của chúng tôi và Cây JNDI được đặt cùng trong cùng một vùng chứa, chúng tôi có thể dễ dàng truy cập bất kỳ đối tượng Java nào được lưu trữ trong sổ đăng ký như vậy. Hơn nữa, sổ đăng ký và ứng dụng của chúng tôi có thể được đặt trong các vùng chứa khác nhau và thậm chí trên các máy vật lý khác nhau. JNDI thậm chí còn cho phép bạn truy cập các đối tượng Java từ xa. Trường hợp điển hình. Quản trị viên máy chủ Java EE đặt một đối tượng vào sổ đăng ký để lưu trữ thông tin cần thiết để kết nối với cơ sở dữ liệu. Theo đó, để làm việc với cơ sở dữ liệu, chúng ta chỉ cần yêu cầu đối tượng mong muốn từ cây JNDI và làm việc với nó. Nó rất thoải mái. Sự thuận tiện còn nằm ở chỗ, trong quá trình phát triển doanh nghiệp có nhiều môi trường khác nhau. Có máy chủ sản xuất và có máy chủ thử nghiệm (và thường có nhiều hơn 1 máy chủ thử nghiệm). Sau đó, bằng cách đặt một đối tượng để kết nối với cơ sở dữ liệu trên mỗi máy chủ bên trong JNDI và sử dụng đối tượng này bên trong ứng dụng của mình, chúng ta sẽ không phải thay đổi bất cứ điều gì khi triển khai ứng dụng của mình từ máy chủ này (kiểm tra, phát hành) sang máy chủ khác. Sẽ có quyền truy cập vào cơ sở dữ liệu ở mọi nơi. Tất nhiên, ví dụ này có phần đơn giản hóa nhưng tôi hy vọng nó sẽ giúp bạn hiểu rõ hơn tại sao JNDI lại cần thiết. Tiếp theo, chúng ta sẽ tìm hiểu kỹ hơn về JNDI trong Java, với một số yếu tố tấn công.

API JNDI

JNDI được cung cấp trong nền tảng Java SE. Để sử dụng JNDI, bạn phải nhập các lớp JNDI, cũng như một hoặc nhiều nhà cung cấp dịch vụ để truy cập các dịch vụ đặt tên và thư mục. JDK bao gồm các nhà cung cấp dịch vụ cho các dịch vụ sau:
  • Giao thức truy cập thư mục nhẹ (LDAP);
  • Kiến trúc môi giới yêu cầu đối tượng chung (CORBA);
  • Dịch vụ tên Common Object Services (COS);
  • Sổ đăng ký gọi phương thức từ xa Java (RMI);
  • Dịch vụ tên miền (DNS).
Mã API JNDI được chia thành nhiều gói:
  • javax.naming;
  • javax.naming.directory;
  • javax.naming.ldap;
  • javax.naming.event;
  • javax.naming.spi.
Chúng tôi sẽ bắt đầu phần giới thiệu về JNDI với hai giao diện - Tên và Ngữ cảnh, chứa chức năng chính của JNDI

Tên giao diện

Giao diện Tên cho phép bạn kiểm soát tên thành phần cũng như cú pháp đặt tên JNDI. Trong JNDI, tất cả các thao tác về tên và thư mục được thực hiện tương ứng với ngữ cảnh. Không có gốc rễ tuyệt đối. Do đó, JNDI định nghĩa một FirstContext, cung cấp điểm khởi đầu cho các hoạt động đặt tên và thư mục. Khi bối cảnh ban đầu được truy cập, nó có thể được sử dụng để tìm kiếm các đối tượng và các bối cảnh khác.
Name objectName = new CompositeName("java:comp/env/jdbc");
Trong đoạn mã trên, chúng tôi đã xác định một số tên mà một số đối tượng được đặt theo đó (nó có thể không được định vị, nhưng chúng tôi đang tính đến nó). Mục tiêu cuối cùng của chúng tôi là lấy được một tham chiếu đến đối tượng này và sử dụng nó trong chương trình của chúng tôi. Vì vậy, tên bao gồm một số phần (hoặc mã thông báo), được phân tách bằng dấu gạch chéo. Các mã thông báo như vậy được gọi là bối cảnh. Cái đầu tiên chỉ đơn giản là ngữ cảnh, những cái tiếp theo đều là ngữ cảnh phụ (sau đây gọi là ngữ cảnh phụ). Các ngữ cảnh sẽ dễ hiểu hơn nếu bạn coi chúng tương tự như các thư mục hoặc thư mục hoặc chỉ là các thư mục thông thường. Bối cảnh gốc là thư mục gốc. Subcontext là một thư mục con. Chúng ta có thể xem tất cả các thành phần (ngữ cảnh và bối cảnh phụ) của một tên cụ thể bằng cách chạy đoạn mã sau:
Enumeration<String> elements = objectName.getAll();
while(elements.hasMoreElements()) {
  System.out.println(elements.nextElement());
}
Đầu ra sẽ như sau:

java:comp
env
jdbc
Kết quả đầu ra cho thấy các mã thông báo trong tên được phân tách với nhau bằng dấu gạch chéo (tuy nhiên, chúng tôi đã đề cập đến điều này). Mỗi mã thông báo tên có chỉ mục riêng. Lập chỉ mục mã thông báo bắt đầu từ 0. Ngữ cảnh gốc có chỉ số 0, ngữ cảnh tiếp theo có chỉ mục 1, ngữ cảnh tiếp theo 2, v.v. Chúng ta có thể lấy tên ngữ cảnh phụ theo chỉ mục của nó:
System.out.println(objectName.get(1)); // -> env
Chúng tôi cũng có thể thêm mã thông báo bổ sung (ở cuối hoặc tại một vị trí cụ thể trong chỉ mục):
objectName.add("sub-context"); // Добавит sub-context в конец
objectName.add(0, "context"); // Добавит context в налачо
Danh sách đầy đủ các phương pháp có thể được tìm thấy trong tài liệu chính thức .

Bối cảnh giao diện

Giao diện này chứa một tập hợp các hằng số để khởi tạo một ngữ cảnh cũng như một tập hợp các phương thức để tạo và xóa ngữ cảnh, liên kết các đối tượng với một tên cũng như tìm kiếm và truy xuất các đối tượng. Hãy xem xét một số thao tác được thực hiện bằng giao diện này. Hành động phổ biến nhất là tìm kiếm một đối tượng theo tên. Điều này được thực hiện bằng cách sử dụng các phương pháp:
  • Object lookup(String name)
  • Object lookup(Name name)
Việc liên kết một đối tượng với một tên được thực hiện bằng các phương thức bind:
  • void bind(Name name, Object obj)
  • void bind(String name, Object obj)
Cả hai phương pháp sẽ liên kết tên tên với đối tượng.Hoạt Object động ngược lại của liên kết - hủy liên kết một đối tượng khỏi tên, được thực hiện bằng các phương thức unbind:
  • void unbind(Name name)
  • void unbind(String name)
Danh sách đầy đủ các phương pháp có sẵn trên trang web tài liệu chính thức .

Bối cảnh ban đầu

InitialContextlà lớp đại diện cho phần tử gốc của cây JNDI và triển khai Context. Bạn cần tìm kiếm các đối tượng theo tên bên trong cây JNDI liên quan đến một nút nhất định. Nút gốc của cây có thể đóng vai trò như một nút như vậy InitialContext. Trường hợp sử dụng điển hình của JNDI là:
  • Lấy InitialContext.
  • Sử dụng InitialContextđể truy xuất các đối tượng theo tên từ cây JNDI.
Có một số cách để có được nó InitialContext. Tất cả phụ thuộc vào môi trường chứa chương trình Java. Ví dụ: nếu một chương trình Java và cây JNDI đang chạy bên trong cùng một máy chủ ứng dụng thì việc InitialContextnhận được:
InitialContext context = new InitialContext();
Nếu không đúng như vậy thì việc tìm hiểu ngữ cảnh sẽ trở nên khó khăn hơn một chút. Đôi khi cần phải chuyển danh sách các thuộc tính môi trường để khởi tạo ngữ cảnh:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
    "com.sun.jndi.fscontext.RefFSContextFactory");

Context ctx = new InitialContext(env);
Ví dụ trên minh họa một trong những cách có thể khởi tạo ngữ cảnh và không mang bất kỳ tải ngữ nghĩa nào khác. Không cần phải đi sâu vào mã một cách chi tiết.

Một ví dụ về việc sử dụng JNDI trong bài kiểm tra đơn vị SpringBoot

Ở trên, chúng tôi đã nói rằng để JNDI tương tác với dịch vụ đặt tên và thư mục, cần phải có SPI (Giao diện nhà cung cấp dịch vụ), với sự trợ giúp của việc tích hợp giữa Java và dịch vụ đặt tên sẽ được thực hiện. JDK tiêu chuẩn đi kèm với một số SPI khác nhau (chúng tôi đã liệt kê chúng ở trên), mỗi SPI ít được quan tâm cho mục đích trình diễn. Việc tạo một ứng dụng JNDI và Java bên trong một thùng chứa có phần thú vị. Tuy nhiên, tác giả bài viết này là một người lười biếng nên để chứng minh cách thức hoạt động của JNDI, ông đã chọn con đường ít trở ngại nhất: chạy JNDI bên trong một bài kiểm tra đơn vị ứng dụng SpringBoot và truy cập vào ngữ cảnh JNDI bằng cách sử dụng một bản hack nhỏ từ Spring Framework. Vì vậy, kế hoạch của chúng tôi:
  • Hãy viết một dự án Spring Boot trống.
  • Hãy tạo một bài kiểm tra đơn vị bên trong dự án này.
  • Trong phần thử nghiệm, chúng tôi sẽ chứng minh cách làm việc với JNDI:
    • được tiếp cận với bối cảnh;
    • liên kết (bind) một đối tượng nào đó dưới một tên nào đó trong JNDI;
    • lấy đối tượng theo tên của nó (tra cứu);
    • Hãy kiểm tra xem đối tượng có phải là null không.
Hãy bắt đầu theo thứ tự. File->New->Project... Sử dụng JNDI trong Java - 3 Tiếp theo, chọn mục Spring Khởi tạo : Sử dụng JNDI trong Java - 4Điền siêu dữ liệu về dự án: Sử dụng JNDI trong Java - 5Sau đó chọn các thành phần Spring Framework cần thiết. Chúng ta sẽ liên kết một số đối tượng DataSource, vì vậy chúng ta cần các thành phần để làm việc với cơ sở dữ liệu:
  • API JDBC;
  • Cơ sở dữ liệu H2 D.
Sử dụng JNDI trong Java - 6Hãy xác định vị trí trong hệ thống tệp: Sử dụng JNDI trong Java - 7Và dự án được tạo. Trên thực tế, một bài kiểm tra đơn vị đã được tạo tự động cho chúng tôi và chúng tôi sẽ sử dụng bài kiểm tra này cho mục đích trình diễn. Dưới đây là cấu trúc dự án và bài kiểm tra chúng ta cần: Sử dụng JNDI trong Java - 8Hãy bắt đầu viết mã bên trong bài kiểm tra contextLoads. Một mẹo nhỏ từ Spring, đã thảo luận ở trên, là lớp SimpleNamingContextBuilder. Lớp này được thiết kế để dễ dàng nâng cao JNDI trong các bài kiểm tra đơn vị hoặc các ứng dụng độc lập. Hãy viết mã để có được bối cảnh:
final SimpleNamingContextBuilder simpleNamingContextBuilder
       = new SimpleNamingContextBuilder();
simpleNamingContextBuilder.activate();

final InitialContext context = new InitialContext();
Hai dòng mã đầu tiên sẽ cho phép chúng ta dễ dàng khởi tạo bối cảnh JNDI sau này. Nếu không có chúng, InitialContextmột ngoại lệ sẽ được đưa ra khi tạo một phiên bản: javax.naming.NoInitialContextException. Tuyên bố từ chối trách nhiệm. Lớp này SimpleNamingContextBuilderlà một lớp không được dùng nữa. Và ví dụ này nhằm mục đích chỉ ra cách bạn có thể làm việc với JNDI. Đây không phải là cách thực hành tốt nhất để sử dụng JNDI trong các bài kiểm tra đơn vị. Đây có thể nói là một chiếc nạng cho việc xây dựng bối cảnh và thể hiện việc liên kết, truy xuất các đối tượng từ JNDI. Sau khi nhận được một ngữ cảnh, chúng ta có thể trích xuất các đối tượng từ nó hoặc tìm kiếm các đối tượng trong ngữ cảnh. Chưa có đối tượng nào trong JNDI, vì vậy sẽ hợp lý nếu đặt một cái gì đó vào đó. Ví dụ, DriverManagerDataSource:
context.bind("java:comp/env/jdbc/datasource", new DriverManagerDataSource("jdbc:h2:mem:mydb"));
Trong dòng này, chúng ta đã ràng buộc đối tượng lớp DriverManagerDataSourcevới tên java:comp/env/jdbc/datasource. Tiếp theo, chúng ta có thể lấy đối tượng từ ngữ cảnh theo tên. Chúng ta không có lựa chọn nào khác ngoài việc lấy đối tượng mà chúng ta vừa đặt, vì không có đối tượng nào khác trong ngữ cảnh =(
final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");
Bây giờ hãy kiểm tra xem DataSource của chúng ta có kết nối hay không (kết nối, kết nối hoặc kết nối là một lớp Java được thiết kế để hoạt động với cơ sở dữ liệu):
assert ds.getConnection() != null;
System.out.println(ds.getConnection());
Nếu chúng ta làm mọi thứ một cách chính xác thì kết quả sẽ như thế này:

conn1: url=jdbc:h2:mem:mydb user=
Điều đáng nói là một số dòng mã có thể đưa ra ngoại lệ. Các dòng sau đây được ném javax.naming.NamingException:
  • simpleNamingContextBuilder.activate()
  • new InitialContext()
  • context.bind(...)
  • context.lookup(...)
Và khi làm việc với một lớp, DataSourcenó có thể bị ném đi java.sql.SQLException. Về vấn đề này, cần phải thực thi mã bên trong một khối try-catchhoặc cho biết trong chữ ký của đơn vị kiểm tra rằng nó có thể đưa ra các ngoại lệ. Đây là mã hoàn chỉnh của lớp kiểm tra:
@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();
        }
    }
}
Sau khi chạy thử nghiệm, bạn có thể thấy các nhật ký sau:

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=

Phần kết luận

Hôm nay chúng tôi đã xem xét JNDI. Chúng tôi đã tìm hiểu về dịch vụ đặt tên và thư mục là gì, đồng thời JNDI là một API Java cho phép bạn tương tác thống nhất với các dịch vụ khác nhau từ một chương trình Java. Cụ thể, với sự trợ giúp của JNDI, chúng ta có thể ghi lại các đối tượng trong cây JNDI dưới một tên nhất định và nhận các đối tượng tương tự theo tên. Là một nhiệm vụ bổ sung, bạn có thể chạy một ví dụ về cách hoạt động của JNDI. Liên kết một số đối tượng khác vào ngữ cảnh và sau đó đọc đối tượng này theo tên.
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION