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.
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ư:- Giao thức truy cập thư mục nhẹ (LDAP) ;
- Dịch vụ đặt tên CORBA ;
- Dịch vụ thông tin mạng (NIS) ;
- Và những người khác.
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: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:- Cuối cùng chúng ta cần có một đối tượng Java.
- Chúng tôi sẽ lấy đối tượng này từ một số sổ đăng ký.
- Có một loạt các đối tượng trong sổ đăng ký này.
- Mỗi đối tượng trong sổ đăng ký này có một tên duy nhất.
- Để 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.”
- 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 ở đó).
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).
- javax.naming;
- javax.naming.directory;
- javax.naming.ldap;
- javax.naming.event;
- javax.naming.spi.
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)
bind
:
void bind(Name name, Object obj)
void bind(String name, Object obj)
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)
Bối cảnh ban đầu
InitialContext
là 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.
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 InitialContext
nhậ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.
- API JDBC;
- Cơ sở dữ liệu H2 D.
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, InitialContext
mộ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 SimpleNamingContextBuilder
là 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 DriverManagerDataSource
vớ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(...)
DataSource
nó 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-catch
hoặ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=
GO TO FULL VERSION