Động lực học lập trình Java, Phần 1: Các lớp Java và việc nạp các lớp

Quan sát các lớp và những gì xảy ra khi chúng được một JVM nạp

Hãy xem xét những gì xảy ra ở hậu trường về việc thực hiện ứng dụng Java của bạn trong loạt bài viết mới về các khía cạnh động của lập trình Java. Chuyên gia Java doanh nghiệp Dennis Sosnoski đưa ra tin số dẻo về định dạng lớp nhị phân Java và những gì xảy ra với các lớp bên trong JVM. Trong bài này, ông còn trình bày các vấn đề nạp lớp nằm trong phạm vi từ số lượng các lớp cần thiết để chạy một ứng dụng Java đơn giản đến các xung đột trình nạp lớp, mà chúng có thể gây ra các vấn đề trong J2EE và các kiến trúc phức tạp tương tự.

Dennis Sosnoski, Nhà tư vấn, Sosnoski Software Solutions, Inc.

Dennis Sosnoski là một nhà tư vấn và nhà trợ giúp đào tạo chuyên về các dịch vụ Web và SOA dựa trên-Java. Kinh nghiệm phát triển phần mềm chuyên nghiệp của ông trải suốt hơn 30 năm qua, với một thập kỉ cuối tập trung vào các công nghệ XML và Java phía máy chủ. Dennis là nhà phát triển hàng đầu về dụng cụ liên kết dữ liệu XML JiBX mã nguồn mở, cũng là một người có duyên nợ với khung công tác của các dịch vụ Web Apache Axis2. Ông cũng là một trong những thành viên của nhóm chuyên gia đặc tả kỹ thuật của Jax-WS 2.0 và JAXB 2.0. Xem trang web của ông để có thông tin về các dịch vụ đào tạo và tư vấn của ông.



04 12 2009

Bài viết này mở đầu một loạt bài viết mới trình bày một họ các chủ đề mà tôi gọi là động lực học lập trình Java. Các chủ đề gồm từ cấu trúc cơ sở của định dạng tệp lớp nhị phân Java, thông qua truy cập siêu dữ liệu trong thời gian chạy bằng cách sử dụng sự phản chiếu, tất cả các cách để sửa đổi và xây dựng các lớp mới trong thời gian chạy. Các chủ đề chung chạy xuyên suốt tất cả tài liệu này là ý tưởng trong đó việc lập trình nền tảng Java là năng động nhiều hơn làm việc với các ngôn ngữ biên dịch thẳng với mã gốc. Nếu bạn hiểu những khía cạnh năng động này, bạn có thể làm nhiều thứ với lập trình Java mà không thể khớp với bất kì ngôn ngữ lập trình chủ đạo nào khác.

Trong bài viết này, tôi trình bày một số các khái niệm cơ bản làm nền tảng cho các tính năng động này của nền tảng Java. Các khái niệm này xoay quanh định dạng nhị phân được sử dụng để biểu diễn các lớp Java, gồm cả những gì sẽ xảy ra khi những lớp này được nạp vào trong JVM. Tài liệu này không chỉ cung cấp một nền móng cho phần còn lại của các bài viết trong loạt này, mà nó còn giải thích một số các mối quan tâm rất thiết thực cho các nhà phát triển đang làm việc trên nền tảng Java.

Một lớp dưới dạng mã nhị phân

Các nhà phát triển đang làm việc trong ngôn ngữ Java thường không phải bận tâm đến các chi tiết về những gì xảy ra với mã nguồn của họ khi nó được chạy qua trình biên dịch. Trong loạt bài này, tôi sắp trình bày rất nhiều các chi tiết ở hậu trường liên quan đến việc sẽ xảy ra từ mã nguồn để thực hiện chương trình, tuy nhiên, tôi sẽ bắt đầu xem xét tại các lớp nhị phân do một trình biên dịch tạo ra.

Định dạng lớp nhị phân trên thực tế được đặc tả JVM xác định. Bình thường, từ mã nguồn của ngôn ngữ Java, một trình biên dịch tạo ra các cách biểu diễn các lớp này và chúng thường được lưu trữ trong các tệp có phần mở rộng .class. Tuy vậy, cả hai tính năng này không cần thiết. Các ngôn ngữ lập trình khác đã được phát triển có sử dụng định dạng lớp nhị phân Java và với một số mục đích, các cách biểu diễn lớp mới được xây dựng và ngay lập tức được nạp trong lúc thực hiện JVM. Đối với JVM , phần quan trọng không phải là mã nguồn hoặc cách nó được lưu trữ, mà là chính định dạng của nó.

Vì vậy, trên thực tế lớp này định dạng trông như thế nào? Liệt kê 1 đưa ra các mã nguồn cho một lớp (rất) ngắn, cùng với một phần hiển thị hệ đếm mười sáu của kết quả tệp lớp của trình biên dịch:

Liệt kê 1. Mã nguồn và (một phần) mã nhị phân cho tệp Hello.java
public class Hello
{
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

0000: cafe babe 0000 002e 001a 0a00 0600 0c09  ................
0010: 000d 000e 0800 0f0a 0010 0011 0700 1207  ................
0020: 0013 0100 063c 696e 6974 3e01 0003 2829  .....<init>...()
0030: 5601 0004 436f 6465 0100 046d 6169 6e01  V...Code...main.
0040: 0016 285b 4c6a 6176 612f 6c61 6e67 2f53  ..([Ljava/lang/S
0050: 7472 696e 673b 2956 0c00 0700 0807 0014  tring;)V........
0060: 0c00 1500 1601 000d 4865 6c6c 6f2c 2057  ........Hello, W
0070: 6f72 6c64 2107 0017 0c00 1800 1901 0005  orld!...........
0080: 4865 6c6c 6f01 0010 6a61 7661 2f6c 616e  Hello...java/lan
0090: 672f 4f62 6a65 6374 0100 106a 6176 612f  g/Object...java/
00a0: 6c61 6e67 2f53 7973 7465 6d01 0003 6f75  lang/System...ou
...

Bên trong mã nhị phân

Điều đầu tiên trong việc biểu diễn lớp nhị phân được thể hiện trong Liệt kê 1 là chữ kí "quán cafe babe" để xác nhận định dạng lớp nhị phân Java (và ngẫu nhiên dùng như là một -- nhưng phần lớn không được thừa nhận -- chứng cứ bền vững cho những người pha cà phê (baristas) làm việc chăm chỉ, những người giữ tinh thần cho các nhà phát triển đang xây dựng nền tảng Java). Chữ kí này là một cách dễ dàng để xác minh rằng một khối dữ liệu thực sự đưa ra yêu cầu là một cá thể của định dạng lớp Java. Mỗi lớp nhị phân Java, thậm chí một lớp không có trên hệ thống tệp tin đó, cần bắt đầu bằng bốn byte này.

Đừng bỏ lỡ phần còn lại của loạt bài này

Phần 2, "Giới thiệu sự phản chiếu" (June 2003)

Phần 3, "Ứng dụng sự phản chiếu" (07.2003)

Phần 4, "Chuyển đổi lớp bằng Javassist" (09.2003)

Phần 5, "Việc chuyển các lớp đang hoạt động" (02.2004)

Phần 6, "Các thay đổi hướng-khía cạnh với Javassist" (03.2004)

Phần 7, "Kỹ thuật bytecode với BCEL" (04.2004)

Phần 8, "Thay thế sự phản chiếu bằng việc tạo mã" (06.2004)

Phần còn lại của dữ liệu ít thú vị. Sau chữ kí là một cặp các số phiên bản định dạng lớp (trong trường hợp này, phiên bản phụ 0 và phiên bản chính 46 -- hệ đếm mười sáu 0x2e -- như được tạo ra bởi các javac 1.4.1), sau đó một số đếm các lối vào trong nhóm hằng số. Số đếm lối vào (trong trường hợp 26 này hoặc 0x001a) tiếp theo là dữ liệu của nhóm hằng số thực sự. Đây là nơi lưu trữ tất cả các hằng số được sử dụng bởi các định nghĩa lớp. Nó gồm lớp và các tên phương thức, các chữ kí và các chuỗi (mà bạn có thể nhận ra trong phần giải thích bằng văn bản ở bên phải của kết quả theo hệ đếm mười sáu), cùng với các giá trị nhị phân khác nhau.

Các mục trong nhóm hằng số có chiều dài thay đổi, với byte đầu tiên của mỗi mục xác định kiểu mục và cách giải mã nó. Ta sẽ không đi vào chi tiết của tất cả thứ có ở đây -- có rất nhiều tài liệu tham khảo có sẵn nếu bạn quan tâm, bắt đầu với đặc tả JVM thực sự. Điểm mấu chốt là nhóm hằng số có tất cả các tham chiếu đến các lớp khác và các phương thức được lớp này sử dụng, cùng với những định nghĩa thực sự cho lớp này và các phương thức của nó. Nhóm hằng số có thể dễ dàng chiếm một nửa hoặc phần lớn hơn của kích thước lớp nhị phân, mặc dù tỷ lệ trung bình có lẽ là thấp hơn.

Tiếp theo nhóm hằng số là một vài mục tham chiếu các lối vào nhóm hằng số cho chính lớp đó, siêu lớp và các giao diện của nó. Các mục này được kế tiếp bởi các thông tin về các trường và các phương thức, chính chúng được biểu diễn như là các cấu trúc phức tạp. Các mã thực hiện cho các phương thức được trình bày dưới dạng các thuộc tính mã được chứa trong các định nghĩa phương thức. Mã này có trong dạng hướng dẫn cho JVM, thường được gọi là mã byte (bytecode), đây là một trong những chủ đề cho phần tiếp theo.

Hỏi chuyên gia: Dennis Sosnoski về các vấn đề JVM và bytecode

Đối với các ý kiến hay các câu hỏi về tài liệu được trình bày trong loạt bài này, cũng như bất cứ điều gì khác có liên quan đến Java bytecode, định dạng lớp nhị phân Java hoặc các vấn đề JVM chung, hãy truy cập vào diễn đàn thảo luận JVM và Bytecode, do Dennis Sosnoski kiểm soát.

Các thuộc tính được sử dụng cho một số các mục đích đã xác định trong định dạng lớp Java, bao gồm cả bytecode đã nói trên, giá trị hằng số cho các trường, xử lý ngoại lệ và thông tin gỡ rối. Dẫu sao các mục đích này không phải chỉ có khả năng sử dụng cho các thuộc tính. Từ đầu, đặc tả JVM đã yêu cầu các JVM bỏ qua các thuộc tính của các kiểu không rõ. Yêu cầu này đưa ra tính linh hoạt để cho việc mở rộng sử dụng các thuộc tính để phục vụ cho các mục đích khác trong tương lai, như là việc cung cấp siêu-thông tin được các khung công tác cần để làm việc với các lớp của người sử dụng -- một cách tiếp cận mà các ngôn ngữ C# có nguồn gốc Java đã sử dụng rộng rãi. Thật không may, vẫn chưa có các kết nối nào được cung cấp để tiến hành sử dụng tính linh hoạt này ở mức người dùng.


Bytecode và các ngăn xếp

Bytecode chiếm phần thực thi của tệp lớp trên thực tế là mã máy cho một loại máy tính đặc biệt -- JVM. Máy này được gọi là một máy ảo vì nó được thiết kế để thực hiện trong phần mềm hơn là phần cứng. Mỗi JVM được sử dụng để chạy các ứng dụng nền tảng Java được xây dựng xung quanh một việc triển khai thực hiện của máy này.

Máy ảo này thực sự khá đơn giản. Nó sử dụng một kiến trúc ngăn xếp, có nghĩa là các toán hạng lệnh được nạp vào một ngăn xếp bên trong trước khi chúng được sử dụng. Tập lệnh gồm tất cả các phép toán số học và logic bình thường, cùng với các nhánh có điều kiện và không điều kiện, nạp/lưu trữ, gọi/trả về, thao tác ngăn xếp và một số các kiểu lệnh đặc biệt. Một số lệnh gồm các giá trị toán hạng cụ thể được mã hóa trực tiếp vào trong các lệnh. Những cái khác trực tiếp tham chiếu các giá trị từ nhóm hằng số.

Mặc dù máy ảo đơn giản, những việc thực hiện không nhất thiết phải như vậy. Các JVM ban đầu (thế hệ đầu tiên) về cơ bản đã là các trình dịch cho bytecode của máy ảo. Trên thực tế chúng tương đối đơn giản, nhưng bị mắc phải vấn đề về hiệu năng nghiêm trọng -- ; việc thông dịch mã luôn luôn sẽ mất nhiều thời gian hơn so với thực hiện mã gốc. Để giảm các vấn đề hiệu năng này, các JVM thế hệ thứ hai được bổ sung việc dịch ngay (JIT). Kỹ thuật JIT biên dịch bytecode Java thành mã gốc trước khi thực hiện nó cho lần đầu tiên, cho phép hiệu năng tốt hơn với nhiều lần thực hiện lặp lại. Các JVM thế hệ hiện nay thậm chí còn đi xa hơn nữa, bằng cách sử dụng các kỹ thuật thích nghi để theo dõi việc thực hiện chương trình và chọn lựa tối ưu hóa các mã sử dụng nhiều.


Nạp các lớp

Các ngôn ngữ như C và C++ biên dịch thành mã gốc thường đòi hỏi một bước liên kết sau khi mã nguồn được biên dịch. Quá trình liên kết này kết hợp mã từ các tệp mã nguồn đã biên dịch tách biệt nhau, cùng với mã thư viện dùng chung, để tạo thành một chương trình thực hiện. Ngôn ngữ Java có khác. Với ngôn ngữ Java, các lớp được trình biên dịch tạo ra nói chung vẫn giữ nguyên như chúng có cho đến khi chúng đang nạp vào một JVM. Ngay cả việc xây dựng một tệp JAR từ các tệp lớp không thay đổi điều này -- JAR chỉ là một thùng chứa cho các tệp lớp đó.

Thay vì là một bước riêng rẽ, việc liên kết các lớp là một phần của một công việc được JVM thực hiện khi nó nạp chúng vào trong bộ nhớ. Điều này bổ sung thêm chi phí hoạt động khi các lớp được nạp lúc đầu, nhưng cũng tạo ra một mức linh hoạt cao hơn cho các ứng dụng Java. Ví dụ, các ứng dụng có thể được viết để sử dụng các giao diện với các việc thực hiện thực sự mà chúng đã để lại không xác định cho đến khi đang chạy. Cách tiếp cận liên kết cuối này để lắp ráp một ứng dụng được sử dụng rộng rãi trong nền tảng Java, với các servlet đang là một ví dụ phổ biến.

Các quy tắc để nạp các lớp được giải thích một cách chi tiết trong đặc tả JVM. Nguyên tắc cơ bản là các lớp chỉ được nạp khi cần thiết (hoặc ít nhất xuất hiện để được nạp theo cách này -- JVM có một số tính linh hoạt trong việc nạp thực tế, nhưng phải duy trì trình tự khởi tạo lớp cố định). Mỗi lớp đã nạp có thể có các lớp khác mà nó phụ thuộc vào, do đó, quá trình nạp là quá trình đệ quy. Các lớp trong Liệt kê 2 cho thấy việc nạp đệ quy này hoạt động như thế nào. Lớp Demo gồm một phương thức main (chính) đơn giản để tạo ra một cá thể Greeter Người đón khách) và gọi phương thức greet. Hàm tạo Greeter tạo ra một cá thể Message (Thông báo), nó sau đó sử dụng trong cuộc gọi phương thức greet.

Liệt kê 2. Mã nguồn với chứng thực việc nạp lớp
public class Demo
{
    public static void main(String[] args) {
        System.out.println("**beginning execution**");
        Greeter greeter = new Greeter();
        System.out.println("**created Greeter**");
        greeter.greet();
    }
}

public class Greeter
{
    private static Message s_message = new Message("Hello, World!");
    
    public void greet() {
        s_message.print(System.out);
    }
}

public class Message
{
    private String m_text;
    
    public Message(String text) {
        m_text = text;
    }
    
    public void print(java.io.PrintStream ps) {
        ps.println(m_text);
    }
}

Thiết lập tham số -verbose:class trên dòng lệnh java in ra dấu vết của quá trình nạp lớp. Liệt kê 3 cho thấy một phần kết quả từ lúc chạy chương trình của Liệt kê 2 với tham số này:

Liệt kê 3. Một phần kết quả của -verbose:class
[Opened /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/sunrsasign.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/jsse.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/jce.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/charsets.jar]
[Loaded java.lang.Object from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.io.Serializable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.String from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
...
[Loaded java.security.Principal from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.security.cert.Certificate 
  from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded Demo]
**beginning execution**
[Loaded Greeter]
[Loaded Message]
**created Greeter**
Hello, World!
[Loaded java.util.HashMap$KeySet 
  from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.util.HashMap$KeyIterator 
  from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]

Đây chỉ là một phần danh sách của các phần quan trọng nhất -- dấu vết đầy đủ gồm 294 dòng, mà với danh sách này tôi đã xóa hầu hết. Tập đầu tiên của việc nạp lớp (trong trường hợp này là 279) tất cả được kích hoạt bằng nỗ lực để nạp lớp Demo. Đây là các lớp cốt lõi được tất cả các chương trình Java sử dụng, không kể nhỏ bao nhiêu. Ngay cả việc loại bỏ tất cả các mã khỏi phương thức Demo main không ảnh hưởng đến trình tự nạp ban đầu này. Tuy vậy, số lượng và tên của các lớp được bao hàm sẽ khác giữa một phiên bản của thư viện lớp này với một phiên bản của thư viện lớp khác.

Phần liệt kê sau khi lớp Demo được nạp sẽ thú vị hơn. Trình tự ở đây cho thấy lớp Greeter được nạp sẽ thú vị hơn. Trình tự ở đây cho thấy lớp Greeter sử dụng một cá thể tĩnh của lớp Message, nên trước khi một cá thể của lớp trước có thể được tạo ra, các lớp sau này cũng cần phải được nạp.

Có nhiều việc xảy ra bên trong JVM khi một lớp được nạp và được khởi tạo, bao gồm việc giải mã định dạng lớp nhị phân, kiểm tra tính tương thích với các lớp khác, xác minh chuỗi các phép toán bytecode và cuối cùng là xây dựng một cá thể java.lang.Class để biểu diễn lớp mới. Đối tượng Class này trở thành cơ sở cho tất cả các cá thể của lớp mới được JVM tạo ra. Đó cũng là trình định danh cho lớp đã tự nạp -- bạn có thể có nhiều bản sao của cùng một lớp nhị phân đã nạp trong một JVM, mỗi bản sao có cá thể Class riêng của nó. Mặc dù tất cả các bản sao này chia sẻ cùng một tên lớp, nhưng chúng sẽ là các lớp riêng biệt với JVM.

Tắt đường dẫn (lớp) theo lối mòn (beaten)

Các trình nạp lớp (class loaders) điều khiển lớp đang nạp vào một JVM. Có một trình nạp lớp tự mồi (bootstrap) được xây dựng bên trong JVM, nó có trách nhiệm nạp các lớp thư viện của lớp Java cơ bản. Trình nạp lớp đặc biệt này có một số tính năng đặc biệt. Với một điều là, nó chỉ nạp các lớp được tìm thấy trên đường dẫn lớp khởi động. Bởi vì đó là những lớp hệ thống tin cậy, trình nạp bootstrap bỏ qua phần lớn việc xác nhận hợp lệ được thực hiện cho các lớp bình thường (không tin cậy).

Bootstrap không phải là trình nạp lớp duy nhất. Đối với người mới bắt đầu, một JVM định nghĩa một trình nạp lớp mở rộng (extension) để nạp các lớp từ các API mở rộng Java tiêu chuẩn và lớp hệ thống (system) để nạp các lớp từ đường dẫn lớp chung (gồm cả các lớp ứng dụng của bạn). Các ứng dụng cũng có thể định nghĩa các trình nạp lớp riêng của chúng cho các mục đích đặc biệt (chẳng hạn như nạp lại các lớp trong thời gian chạy). Các trình nạp lớp được thêm vào như vậy có nguồn gốc từ lớp java.lang.ClassLoader (có thể gián tiếp), lớp này cung cấp sự hỗ trợ cốt lõi để xây dựng một sự biểu diễn lớp trong một cá thể java.lang.Class từ một mảng các byte. Mỗi lớp được xây dựng theo một số ý nghĩa "có sở hữu" bằng trình nạp lớp đã nạp nó. Các trình nạp lớp thường giữ bản đồ các lớp mà chúng đã nạp, để có khả năng tìm một lớp theo tên nếu nó được yêu cầu lại.

Mỗi trình nạp lớp cũng giữ một tham chiếu đến một trình nạp lớp cha mẹ, khi xác định một cây của các trình nạp lớp với trình nạp bootstrap tại gốc. Khi cần một cá thể của một lớp đặc biệt (được xác định theo tên), bất cứ trình nạp lớp nào lúc đầu xử lý yêu cầu thường kiểm tra trình nạp lớp cha mẹ của nó đầu tiên trước khi cố gắng nạp lớp trực tiếp. Điều này áp dụng theo cách đệ quy nếu có nhiều tầng của các trình nạp lớp, sao cho nó có nghĩa là một lớp thường sẽ nhìn thấy không chỉ trong trình nạp lớp đã nạp nó, mà còn cho tất cả các trình nạp lớp con cháu. Nó cũng có nghĩa là nếu một lớp có thể được nạp bởi nhiều hơn một trình nạp lớp theo một chuỗi, một lớp xa nhất lên đến cây đó sẽ là một lớp nạp nó thực sự.

Có rất nhiều trường hợp ở đó các trình nạp lớp (classloaders) của nhiều ứng dụng được các chương trình Java sử dụng. Một ví dụ là trong khung công tác J2EE. Mỗi ứng dụng J2EE được nạp bởi khung công tác cần phải có một trình nạp lớp riêng để ngăn chặn các lớp trong một ứng dụng khỏi cản trở các ứng dụng khác. Mã của khung công tác tự nó sẽ sử dụng một hoặc nhiều trình nạp lớp khác, cũng để ngăn chặn sự cản trở đến hoặc từ các ứng dụng. Tập các trình nạp lớp hoàn chỉnh tạo nên một hệ thống phân cấp có cấu trúc cây với các kiểu khác nhau của các lớp ở mỗi cấp.

Các cây của các trình nạp

Như ví dụ của một hệ thống phân cấp của một trình nạp lớp trong lúc hành động, Hình 1 cho thấy hệ thống phân cấp của một trình nạp lớp được máy servlet Tomcat xác định. Ở đây trình nạp lớp Chung (Common) nạp từ các tệp JAR trong một thư mục cụ thể của quá trình cài đặt Tomcat dành cho những mã dùng chung giữa máy chủ và tất cả các ứng dụng Web. Trình nạp Catalina dành cho các lớp riêng của Tomcat và trình nạp dùng chung (Shared) cho các lớp được chia sẻ giữa các ứng dụng Web. Cuối cùng, mỗi ứng dụng Web nhận trình nạp riêng cho các lớp riêng của nó.

Hình 1. Các trình nạp lớp Tomcat
Các trình nạp lớp Tomcat

Trong kiểu môi trường này, việc theo dõi trình nạp thích hợp để sử dụng cho yêu cầu một lớp mới có thể lộn xộn. Do điều này, các phương thức setContextClassLoadergetContextClassLoader đã được thêm vào lớp java.lang.Thread trong nền tảng Java 2. Các phương thức này cho phép khung công tác thiết lập trình nạp lớp sẽ được sử dụng cho mỗi ứng dụng trong lúc chạy mã từ ứng dụng đó.

Tính linh hoạt về khả năng nạp các tập độc lập của các lớp là một tính năng quan trọng của nền tảng Java. Tuy nhiên, khi tính năng này có ích, nó thể tạo ra sự lẫn lộn trong một số trường hợp. Một khía cạnh lẫn lộn là vấn đề tiếp tục đối phó với các đường dẫn lớp (classpaths) của JVM. Trong phân cấp Tomcat của các trình nạp lớp đã chỉ ra trong Hình 1, ví dụ, các lớp được nạp bởi trình nạp lớp Common sẽ không bao giờ có khả năng trực tiếp truy cập (theo tên) các lớp được ứng dụng Web nạp. Cách duy nhất để liên kết các lớp này cùng nhau là thông qua việc sử dụng các giao diện trực quan cho cả hai tập các lớp. Trong trường hợp này, điều đó bao gồm javax.servlet.Servlet được servlet Java triển khai thực hiện.

Các vấn đề có thể nảy sinh khi mã được di chuyển giữa các trình nạp lớp vì bất cứ lý do nào. Ví dụ, khi J2SE 1.4 đã di chuyển JAXP API để xử lý XML trong sự phân phối chuẩn, nó đã tạo ra các vấn đề cho nhiều môi trường, mà ở đó các ứng dụng trước đó đã dựa vào việc nạp các việc thực hiện đã chọn theo các XML API riêng của chúng. Với J2SE 1.3, điều này có thể được thực hiện chỉ bằng cách đặt tệp JAR thích hợp trong đường dẫn lớp của người sử dụng. Trong J2SE 1.4, các phiên bản tiêu chuẩn của các API này bây giờ đang ở trong đường dẫn lớp của các phần mở rộng (extensions), để cho những đường dẫn này thường sẽ ghi đè lên bất kỳ sự hiện diện của việc thực hiện trong đường dẫn lớp của người sử dụng.

Các kiểu lẫn lộn khác cũng có thể xảy ra khi sử dụng các trình nạp nhiều lớp. Hình 2 cho thấy một ví dụ về biến động nhận dạng lớp xảy ra khi mỗi một giao diện và việc thực hiện có liên quan được nạp bởi hai trình nạp lớp riêng biệt. Mặc dù các tên và việc triển khai thực hiện mã nhị phân của các giao diện và các lớp đều giống nhau, một cá thể của một lớp từ một trình nạp có thể không được thừa nhận khi triển khai thực hiện giao diện từ trình nạp khác. Sự lẫn lộn này có thể được giải quyết trong Hình 2 bằng cách di chuyển lớp giao diện I vào vùng của trình nạp lớp System. Vẫn là hai cá thể riêng biệt của lớp A, nhưng cả hai sẽ triển khai thực hiện cùng giao diện I.

Hình 2. Biến động nhận dạng lớp
Biến động nhận dạng lớp

Kết luận

Định nghĩa lớp Java và đặc tả JVM cùng xác định một khung công tác vô cùng mạnh mẽ để lắp ráp mã trong thời gian chạy. Thông qua việc sử dụng các trình nạp lớp, các ứng dụng Java có khả năng làm việc với nhiều phiên bản của các lớp, nếu khác đi các lớp này sẽ gây ra các xung đột. Tính linh hoạt của các trình nạp lớp thậm chí cho phép nạp động lại mã đã sửa đổi trong khi một ứng dụng tiếp tục thực hiện.

Chi phí cho tính linh hoạt của nền tảng Java trong lĩnh vực này là chi phí hoạt động cao hơn một chút khi bắt đầu một ứng dụng. Hàng trăm các lớp riêng biệt cần phải được JVM nạp trước khi nó có thể bắt đầu thực hiện ngay cả những mã ứng dụng đơn giản nhất. Chi phí khởi động này thường làm cho nền Java phù hợp với các ứng dụng kiểu máy chủ, chạy lâu dài tốt hơn để sử dụng thường xuyên hơn cho các chương trình nhỏ. Các ứng dụng máy chủ cũng được hưởng lợi nhiều nhất từ sự linh hoạt lắp ráp mã trong thời gian chạy, vì vậy không có gì ngạc nhiên rằng nền tảng Java đã trở nên ngày càng thuận lợi cho kiểu phát triển này.

Trong phần 2 của loạt bài này, tôi sẽ trình bày một giới thiệu về cách sử dụng một khía cạnh khác của các nền móng năng động của nền tảng Java: API phản chiếu (Reflection API). Sự phản chiếu giúp cho việc thực thi mã của bạn truy cập tới các lớp thông tin bên trong. Đây có thể là một công cụ quan trọng để xây dựng mã linh hoạt, có thể được nối với nhau trong thời gian chạy mà không cần bất kỳ các liên kết mã nguồn nào giữa các lớp. Tuy nhiên, như với hầu hết các công cụ, bạn cần phải biết sử dụng nó khi nào và như thế nào để có lợi ích tốt nhất. Kiểm tra lại để tìm hiểu các thủ thuật và các sự thỏa hiệp về sự phản chiếu hiệu quả trong Phần 2 của Các động lực học lập trình Java.

Tài nguyên

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java
ArticleID=452537
ArticleTitle=Động lực học lập trình Java, Phần 1: Các lớp Java và việc nạp các lớp
publish-date=12042009