Đối với mỗi một trong ba kiểu này của các thành phần lớp -- các hàm tạo constructor, các trường và các phương thức -- java.lang.Class cung cấp bốn cuộc gọi thể hiện sự phản chiếu riêng b
Trang 1Động lực học lập trình Java, Phần 2: Giới thiệu sự phản chiếu
Sử dụng thông tin lớp trong thời gian chạy để khởi động việc lập trình của bạn
Dennis Sosnoski, Nhà tư vấn, Sosnoski Software Solutions, Inc
Tóm tắt: Sự phản chiếu cho phép truy cập mã của bạn tới thông tin bên trong đối
với các lớp được nạp vào JVM và cho phép bạn viết mã để làm việc với các lớp được lựa chọn trong quá trình thực hiện, không phải trong mã nguồn Điều này tạo cho sự phản chiếu một công cụ quan trọng để xây dựng các ứng dụng linh hoạt Nhưng xem ra nếu được sử dụng không thích hợp, sự phản chiếu có thể tốn kém Trong Phần 2 của loạt bài của mình về bản chất của nền tảng Java, nhà tư vấn phần mềm Dennis Sosnoski đưa ra một sự giới thiệu về cách sử dụng sự phản chiếu, cũng như xem xét một số các chi phí liên quan Bạn cũng sẽ tìm hiểu cách Java Reflection API (API phản chiếu Java) cho phép bạn kết nối vào các đối tượng trong thời gian chạy
Trong "Động lực học lập trình Java, Phần 1," tôi đã cung cấp cho bạn một sự giới thiệu về các lớp lập trình Java và nạp lớp Bài viết đó mô tả một số tư liệu thông tin rộng lớn theo định dạng lớp nhị phân Java Trong bài viết tháng này, tôi sẽ trình bày những điều cơ bản về việc sử dụng Java Reflection API để truy cập và sử dụng một số thông tin như vậy trong thời gian chạy Để giúp duy trì những điều này thú vị với các nhà phát triển, những người đã biết những điều cơ bản của sự phản chiếu, tôi sẽ trình bày một cái nhìn về cách so sánh hiệu năng phản chiếu với truy cập trực tiếp
Đừng bỏ lỡ phần còn lại của loạt bài này
Phần 1, "Các lớp Java và nạp lớp" (04.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)
Trang 2Sử dụng sự phản chiếu khác với lập trình Java tiêu chuẩn ở chỗ nó làm việc với
siêu dữ liệu dữ liệu mô tả dữ liệu khác Kiểu siêu dữ liệu cụ thể được truy cập
bởi sự phản chiếu của ngôn ngữ Java là sự mô tả về các lớp và các đối tượng bên trong JVM Sự phản chiếu cho phép bạn truy cập trong thời gian chạy đến một loạt các thông tin lớp Thậm chí nó còn cho phép bạn đọc và viết các trường và các phương thức gọi của một lớp được chọn trong thời gian chạy
Sự phản chiếu là một công cụ mạnh Nó cho phép bạn xây dựng mã linh hoạt, mã này có thể được lắp ráp trong thời gian chạy mà không đòi hỏi các liên kết mã nguồn giữa các thành phần Nhưng một số khía cạnh của sự phản chiếu có thể khó
hiểu Trong bài này, tôi sẽ đi vào những lý do tại sao bạn có thể không muốn sử
dụng sự phản chiếu trong các chương trình của bạn, cũng như những lý do tại sao bạn muốn Sau khi bạn biết các sự thỏa hiệp, bạn có thể quyết định cho chính mình khi những lợi ích có giá trị hơn những hạn chế
Lớp của những người mới bắt đầu
Điểm khởi đầu để sử dụng sự phản chiếu luôn luôn là một cá thể java.lang.Class Nếu bạn muốn làm việc với một lớp định sẵn, thì ngôn ngữ Java cung cấp một phím tắt dễ dàng để có được cá thể Class trực tiếp:
Class clas = MyClass.class;
Khi bạn sử dụng kỹ thuật này, tất cả các công việc liên quan đến việc nạp các lớp diễn ra ở hậu trường Tuy nhiên, nếu bạn cần phải đọc tên lớp trong thời gian chạy
từ một số nguồn bên ngoài, thì cách tiếp cận này không phải là sắp thực hiện Thay vào đó, bạn cần phải sử dụng một trình nạp lớp để tìm thông tin lớp Dưới đây là một cách để thực hiện điều đó:
// "name" is the class name to load
Class clas = null;
try {
clas = Class.forName(name);
Trang 3} catch (ClassNotFoundException ex) {
// handle exception case
}
// use the loaded class
Nếu lớp đã được nạp, bạn sẽ tìm lại các thông tin Class hiện có Nếu lớp chưa được nạp, trình nạp lớp sẽ nạp nó bây giờ và trả về cá thể lớp vừa mới được xây dựng
Sự phản chiếu trên một lớp
Đối tượng Class mang đến cho bạn tất cả các kết nối cơ bản để truy cập phản chiếu đến siêu dữ liệu lớp Siêu dữ liệu này bao gồm các thông tin về chính lớp đó, chẳng hạn như gói và siêu lớp của lớp đó, cũng như các giao diện được lớp đó triển khai thực hiện Nó cũng bao gồm các chi tiết về các hàm tạo, các trường và các phương thức được lớp đó xác định Các mục sau cùng này là những thứ hầu như thường được sử dụng trong lập trình, vì vậy tôi sẽ đưa ra một số ví dụ về làm việc với chúng sau trong phần này
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
Đối với mỗi một trong ba kiểu này của các thành phần lớp các hàm tạo
(constructor), các trường và các phương thức java.lang.Class cung cấp bốn cuộc gọi thể hiện sự phản chiếu riêng biệt để truy cập thông tin theo nhiều cách khác nhau Tất cả các cuộc gọi đi theo sau một dạng chuẩn Đây là một tập được sử dụng để tìm các hàm tạo:
Trang 4 Constructor getConstructor(Class[] params) Tìm ra hàm tạo công khai bằng cách sử dụng các kiểu tham số cụ thể
Constructor[] getConstructors() Tìm ra tất cả các hàm tạo công khai cho lớp đó
Constructor getDeclaredConstructor(Class[] params) Tìm ra hàm tạo (bất
kể mức truy cập) bằng cách sử dụng các kiểu tham số cụ thể
Constructor[] getDeclaredConstructors() Tìm ra tất cả các hàm tạo (bất
kể mức truy cập) cho lớp đó
Mỗi một trong các cuộc gọi này trả về một hoặc nhiều cá thể
java.lang.reflect.Constructor Lớp Constructor này định nghĩa một phương thức newInstance lấy một mảng các đối tượng làm đối số duy nhất của nó, sau đó trả về một cá thể vừa được xây dựng của lớp gốc Mảng các đối tượng là các giá trị tham
số sử dụng cho cuộc gọi hàm tạo Như là một ví dụ về cách làm việc này, giả sử bạn có một lớp TwoString với một hàm tạo lấy một cặp String, như thể hiện trong Liệt kê 1:
Liệt kê 1 Lớp được xây dựng từ cặp strings
public class TwoString {
private String m_s1, m_s2;
public TwoString(String s1, String s2) {
m_s1 = s1;
m_s2 = s2;
}
}
Mã được hiển thị trong Liệt kê 2 tìm ra hàm tạo và sử dụng nó để tạo một cá thể của lớp TwoString khi sử dụng Strings "a" và "b":
Trang 5Liệt kê 2 Cuộc gọi sự phản chiếu cho hàm tạo
Class[] types = new Class[] { String.class, String.class };
Constructor cons = TwoString.class.getConstructor(types);
Object[] args = new Object[] { "a", "b" };
TwoString ts = (TwoString)cons.newInstance(args);
Mã trong Liệt kê 2 bỏ qua một số kiểu có thể của các ngoại lệ đã kiểm tra được các phương thức phản chiếu khác nhau đưa ra Các ngoại lệ này được trình bày chi tiết trong các mô tả Javadoc API, vậy để cho ngắn gọn, tôi để chúng ở ngoài các ví
dụ này
Trong khi tôi đang nói chủ đề về các hàm tạo, ngôn ngữ lập trình Java cũng định nghĩa một phương thức phím tắt đặc biệt mà bạn có thể sử dụng để tạo một cá thể
của một lớp bằng một hàm tạo no-argument (hoặc mặc định) Phím tắt này được
nhúng vào trong định nghĩa Class riêng của nó như sau:
Object newInstance() Xây dựng cá thể mới khi sử dụng hàm tạo mặc định
Mặc dù cách tiếp cận này chỉ cho phép bạn sử dụng một hàm tạo cụ thể, nó tạo một phím tắt rất tiện lợi nếu đó là một thứ bạn muốn Kỹ thuật này đặc biệt có ích khi làm việc với JavaBeans, JavaBeans được dùng để xác định một hàm tạo công khai, không có đối số (no-argument)
Các trường của sự phản chiếu
Các cuộc gọi phản chiếu Class (lớp) nhằm truy cập thông tin về trường là tương tự như các cuộc gọi được dùng để truy cập các hàm tạo, với tên trường được sử dụng thay cho một mảng của các kiểu tham số:
Field getField(String name) Tìm ra trường công khai có tên
Field[] getFields() Tìm ra tất cả các trường công khai của lớp đó
Trang 6 Field getDeclaredField(String name) Tìm ra trường có tên được lớp đó khai báo
Field[] getDeclaredFields() Tìm ra tất cả các trường được lớp đó khai báo
Mặc dù có sự tương đồng với các cuộc gọi hàm tạo, cũng có một sự khác biệt quan trọng khi nói đến các trường: hai trường đầu tiên trả về các thông tin cho các trường công khai để có thể truy cập chúng thông qua lớp đó ngay cả những lớp được thừa kế từ một lớp ông bà Hai trường cuối trả về các thông tin cho các trường được lớp đó khai báo trực tiếp không phân biệt các kiểu truy cập của trường
Các cá thể java.lang.reflect.Field được các cuộc gọi trả về định nghĩa các phương thức getXXX và setXXX cho tất cả các kiểu nguyên thủy, cũng như các phương thức get và set chung làm việc với các tham chiếu đối tượng Nó cho bạn quyết định sử dụng một phương thức thích hợp dựa trên kiểu trường thực tế, mặc dù các phương thức getXXX sẽ xử lý tự động các biến đổi mở rộng (như khi sử dụng phương thức getInt để lấy ra một giá trị byte)
Liệt kê 3 cho thấy một ví dụ về việc sử dụng các phương thức phản chiếu trường, dưới dạng một phương thức để tăng một trường int của một đối tượng theo tên:
Liệt kê 3 Tăng một trường bằng sự phản chiếu
public int incrementField(String name, Object obj) throws {
Field field = obj.getClass().getDeclaredField(name);
int value = field.getInt(obj) + 1;
field.setInt(obj, value);
return value;
}
Trang 7Phương thức này bắt đầu hiển thị một số tính linh hoạt có thể với sự phản chiếu Thay vì làm việc với một lớp cụ thể, incrementField sử dụng phương thức
getClass của đối tượng được chuyển qua để tìm thông tin lớp, sau đó trực tiếp tìm trường có tên trong lớp đó
Các phương thức của sự phản chiếu
Sự phản chiếu Class gọi truy cập thông tin của phương thức rất giống với những
sự phản chiếu được sử dụng cho các hàm tạo và các trường:
Method getMethod(String name, Class[] params) Tìm ra phương thức công khai có tên bằng cách sử dụng các kiểu tham số cụ thể
Method[] getMethods() Tìm ra tất cả các phương thức công khai của lớp
Method getDeclaredMethod(String name, Class[] params) Tìm ra
phương thức công khai có tên được lớp đó khai báo bằng cách sử dụng các kiểu tham số cụ thể
Method[] getDeclaredMethods() Tìm ra tất cả các phương thức được lớp
đó khai báo
Như với các cuộc gọi trường, hai phương thức đầu tiên trả về thông tin cho các phương thức công khai có thể được truy cập thông qua lớp đó, ngay cả các lớp đó được thừa kế từ một lớp ông bà Hai phương thức cuối trả về thông tin cho các phương thức được lớp đó khai báo trực tiếp, mà không liên quan đến kiểu truy cập của phương thức này
Các cá thể java.lang.reflect.Method được các cuộc gọi trả về định nghĩa một
phương thức invoke (gọi) mà bạn có thể sử dụng để gọi phương thức đó trên một
cá thể của lớp định nghĩa Phương thức invoke này lấy hai đối số cung cấp cá thể lớp và một mảng các giá trị tham số cho cuộc gọi này
Liệt kê 4 đưa ví dụ về trường tiến thêm một bước, khi hiển thị một ví dụ về sự phản chiếu của phương thức đang hành động Phương thức này làm tăng một thuộc tính int JavaBean được xác định bằng phương thức get và set Ví dụ, nếu đối tượng đã xác định phương thức getCount và setCount cho một giá trị count (đếm)
số nguyên, thì bạn có thể vượt qua "count" như tham số name trong một cuộc gọi đến phương thức này để tăng giá trị đó
Liệt kê 4 Làm tăng một thuộc tính JavaBean bằng sự phản chiếu
Trang 8public int incrementProperty(String name, Object obj) {
String prop = Character.toUpperCase(name.charAt(0)) +
name.substring(1);
String mname = "get" + prop;
Class[] types = new Class[] {};
Method method = obj.getClass().getMethod(mname, types);
Object result = method.invoke(obj, new Object[0]);
int value = ((Integer)result).intValue() + 1;
mname = "set" + prop;
types = new Class[] { int.class };
method = obj.getClass().getMethod(mname, types);
method.invoke(obj, new Object[] { new Integer(value) });
return value;
}
Để thực hiện theo các quy ước JavaBeans, tôi biến đổi chữ cái đầu của thuộc tính tên thành chữ hoa, sau đó dựa vào get để xây dựng tên phương thức đọc và set để xây dựng tên phương thức viết Các phương thức đọc JavaBeans chỉ trả về giá trị
và viết các phương thức lấy giá trị làm tham số duy nhất, vì vậy tôi chỉ định các kiểu tham số cho các phương thức cho phù hợp Cuối cùng, quy ước đòi hỏi các phương thức là công khai, vậy tôi sử dụng dạng tra cứu thông tin để tìm các phương thức công khai có khả năng gọi được trên lớp đó
Ví dụ này là một ví dụ đầu tiên mà tôi đã chuyển qua các giá trị nguyên thủy khi
sử dụng sự phản chiếu, vậy chúng ta hãy xem xét cách làm này Nguyên tắc cơ bản rất đơn giản: bất cứ khi nào bạn cần phải chuyển qua một giá trị nguyên thủy, chỉ cần thay thế một cá thể của lớp trình bao (wrapper) tương ứng (được định
Trang 9nghĩa trong gói java.lang cho kiểu nguyên thủy đó Điều này áp dụng cho cả các cuộc gọi và cả các trả về Vì vậy, khi tôi gọi phương thức get trong ví dụ của tôi, tôi chờ đợi kết quả là một trình bao java.lang.Integer cho giá trị thuộc tính int thực
sự
Phản chiếu các mảng
Các mảng là các đối tượng trong ngôn ngữ lập trình Java Giống như tất cả các đối tượng, chúng có các lớp Nếu bạn có một mảng, bạn có thể nhận được lớp của mảng đó khi sử dụng phương thức getClass chuẩn, cũng giống như với bất kỳ đối
tượng khác Tuy nhiên, việc nhận được lớp đó mà không có một cá thể hiện có làm
việc khác với các kiểu đối tượng khác Ngay cả sau khi bạn có một lớp mảng không có nhiều lớp bạn có thể làm việc trực tiếp với nó, việc truy cập hàm tạo được sự phản chiếu cho các lớp thông thường đưa ra không làm việc với các mảng
và các mảng không có bất kỳ các trường nào dễ truy cập Các phương thức
java.lang.Object cơ bản chỉ được định nghĩa cho đối tượng mảng
Việc xử lý đặc biệt của các mảng sử dụng một tập hợp các phương thức tĩnh được lớp java.lang.reflect.Array cung cấp Các phương thức trong lớp này cho phép bạn tạo các mảng mới, nhận được chiều dài của một đối tượng mảng và đọc và viết các giá trị có chỉ mục của một đối tượng mảng
Liệt kê 5 cho thấy một phương thức hữu ích để thay đổi kích thước một mảng hiện
có một cách hiệu quả Nó sử dụng sự phản chiếu để tạo một mảng mới cùng kiểu, sau đó sao chép tất cả các dữ liệu suốt từ mảng cũ trước khi trả về mảng mới
Liệt kê 5 Phát triển một mảng bằng sự phản chiếu
public Object growArray(Object array, int size) {
Class type = array.getClass().getComponentType();
Object grown = Array.newInstance(type, size);
System.arraycopy(array, 0, grown, 0,
Math.min(Array.getLength(array), size));
return grown;
Trang 10}
An ninh và sự phản chiếu
An ninh có thể là một vấn đề phức tạp khi đối phó với sự phản chiếu Mã kiểu-khung công tác thường sử dụng sự phản chiếu và với điều này bạn có thể muốn khung công tác có truy cập đầy đủ tới mã của bạn mà không cần quan tâm về các hạn chế truy cập thông thường Tuy vậy việc truy cập không kiểm soát được có thể tạo những nguy cơ an ninh chính trong các trường hợp khác, chẳng hạn như khi
mã được thi hành trong một môi trường được chia sẻ bởi mã không đáng tin cậy
Do các nhu cầu xung đột nhau, nên ngôn ngữ lập trình Java định nghĩa một cách tiếp cận đa cấp để xử lý an ninh phản chiếu Các chế độ cơ bản là bắt tuân theo các hạn chế như nhau trên sự phản chiếu như đã áp dụng cho việc truy cập mã nguồn:
Truy cập từ bất cứ ở đâu tới các thành phần công khai của lớp
Không truy cập bên ngoài lớp riêng của nó tới các thành phần riêng
Truy cập có giới hạn tới các thành phần được bảo vệ và các thành phần gói (truy cập mặc định)
Tuy nhiên có một cách đơn giản xung quanh các hạn chế này thỉnh thoảng có Tất cả các lớp Constructor, Field, và Method mà tôi đã sử dụng trong các ví dụ trước đó mở rộng một lớp cơ sở chung lớp java.lang.reflect.AccessibleObject Lớp này định nghĩa một phương thức setAccessible cho phép bạn bật hoặc tắt kiểm tra truy cập cho một cá thể của một trong những lớp này Việc bắt giữ duy nhất (catch) là nếu có một trình quản lý an ninh, nó sẽ kiểm tra xem mã tắt kiểm tra truy cập có cho phép làm như vậy không Nếu không cho phép, trình quản lý an ninh đưa ra một lỗi ngoại lệ
Liệt kê 6 giải thích một chương trình có sử dụng sự phản chiếu trên một cá thể của lớp TwoString của Liệt kê 1 để hiển thị điều này đang hoạt động:
Liệt kê 6 An ninh phản chiếu đang hoạt động