JavaRush /Blog Java /Random-VI /API phản ánh. Sự phản xạ. Mặt tối của Java
Oleksandr Klymenko
Mức độ
Харків

API phản ánh. Sự phản xạ. Mặt tối của Java

Xuất bản trong nhóm
Xin chào, Padawan trẻ. Trong bài viết này, tôi sẽ kể cho bạn nghe về Thần lực, sức mạnh mà các lập trình viên java chỉ sử dụng trong những tình huống dường như vô vọng. Vì vậy, mặt tối của Java là -Reflection API
API phản ánh.  Sự phản xạ.  Mặt tối của Java - 1
Sự phản chiếu trong Java được thực hiện bằng API Java Reflection. Sự phản ánh này là gì? Có một định nghĩa ngắn gọn và chính xác cũng phổ biến trên Internet. Phản ánh (từ tiếng Latin muộn phản xạ - quay trở lại) là một cơ chế nghiên cứu dữ liệu về một chương trình trong quá trình thực hiện. Sự phản chiếu cho phép bạn kiểm tra thông tin về các trường, phương thức và hàm tạo lớp. Bản thân cơ chế phản chiếu cho phép bạn xử lý các loại không có trong quá trình biên dịch nhưng lại xuất hiện trong quá trình thực thi chương trình. Sự phản ánh và sự hiện diện của một mô hình báo cáo lỗi nhất quán về mặt logic giúp tạo mã động chính xác. Nói cách khác, hiểu cách hoạt động của sự phản chiếu trong java sẽ mở ra một số cơ hội tuyệt vời cho bạn. Theo nghĩa đen, bạn có thể sắp xếp các lớp và các thành phần của chúng.
API phản ánh.  Sự phản xạ.  Mặt tối của Java - 2
Đây là danh sách cơ bản về những gì sự phản chiếu cho phép:
  • Tìm hiểu/xác định lớp của một đối tượng;
  • Nhận thông tin về các công cụ sửa đổi lớp, trường, phương thức, hằng, hàm tạo và siêu lớp;
  • Tìm hiểu những phương thức nào thuộc về giao diện/giao diện đã triển khai;
  • Tạo một thể hiện của một lớp và không xác định được tên của lớp đó cho đến khi chương trình được thực thi;
  • Nhận và đặt giá trị của trường đối tượng theo tên;
  • Gọi phương thức của đối tượng theo tên.
Sự phản ánh được sử dụng trong hầu hết các công nghệ Java hiện đại. Thật khó để tưởng tượng liệu Java với tư cách là một nền tảng có thể đạt được sự chấp nhận rộng rãi như vậy mà không cần phản ánh hay không. Nhiều khả năng là tôi không thể. Bạn đã làm quen với ý tưởng lý thuyết chung về sự phản ánh, bây giờ chúng ta hãy bắt tay vào ứng dụng thực tế của nó! Chúng tôi sẽ không nghiên cứu tất cả các phương pháp của Reflection API mà chỉ nghiên cứu những gì thực sự gặp phải trong thực tế. Vì cơ chế phản chiếu liên quan đến việc làm việc với các lớp nên chúng ta sẽ có một lớp đơn giản - MyClass:
public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
Như chúng ta có thể thấy, đây là lớp phổ biến nhất. Hàm tạo với các tham số được nhận xét là có lý do, chúng ta sẽ quay lại vấn đề này sau. Nếu bạn nhìn kỹ vào nội dung của lớp, bạn có thể thấy sự vắng mặt của getter'a cho name. Bản thân trường này nameđược đánh dấu bằng một công cụ sửa đổi truy cập private; chúng ta sẽ không thể truy cập nó bên ngoài lớp đó; =>chúng ta không thể nhận được giá trị của nó. "Vậy vấn đề là gì? - bạn nói. “Thêm getterhoặc thay đổi công cụ sửa đổi quyền truy cập.” Và bạn sẽ đúng, nhưng điều gì sẽ xảy ra nếu MyClassnó nằm trong thư viện aar đã biên dịch hoặc trong một mô-đun đóng khác mà không có quyền truy cập chỉnh sửa và trong thực tế, điều này xảy ra cực kỳ thường xuyên. Và một số lập trình viên thiếu chú ý đã quên viết getter. Đã đến lúc nhớ về sự suy ngẫm! Hãy thử truy cập vào privatetrường namelớp MyClass:
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //no getter =(
   System.out.println(number + name);//output 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name);//output 0default
}
Bây giờ chúng ta hãy tìm hiểu những gì đã xảy ra ở đây. Có một lớp học tuyệt vời trong java Class. Nó đại diện cho các lớp và giao diện trong một ứng dụng Java có thể thực thi được. Chúng tôi sẽ không đề cập đến kết nối giữa ClassClassLoader. đây không phải là chủ đề của bài viết. Tiếp theo, để lấy các trường của lớp này, bạn cần gọi phương thức getFields(), phương thức này sẽ trả về cho chúng ta tất cả các trường có sẵn của lớp. Điều này không phù hợp với chúng ta, vì trường của chúng ta là private, vì vậy chúng ta sử dụng phương thức getDeclaredFields()này, phương thức này cũng trả về một mảng các trường lớp, nhưng bây giờ cả hai privateprotected. Trong tình huống của chúng tôi, chúng tôi biết tên của trường mà chúng tôi quan tâm và chúng tôi có thể sử dụng phương thức getDeclaredField(String), Stringtên của trường mong muốn ở đâu. Ghi chú: getFields()getDeclaredFields()không trả về các trường của lớp cha! Tuyệt vời, chúng tôi đã nhận được một đối tượng Field có liên kết tới tệp name. Bởi vì trường này không публичным(công khai), cần có quyền truy cập để làm việc với nó. Phương pháp này setAccessible(true)cho phép chúng tôi tiếp tục làm việc. Bây giờ lĩnh vực này namehoàn toàn nằm trong tầm kiểm soát của chúng tôi! Bạn có thể nhận được giá trị của nó bằng cách gọi get(Object)đối tượng Field, đâu Objectlà một thể hiện của lớp chúng ta MyClass. Chúng tôi truyền nó Stringvà gán nó cho biến của chúng tôi name. Trong trường hợp chúng ta đột nhiên không có setter'a, chúng ta có thể sử dụng phương thức này để đặt giá trị mới cho trường tên set:
field.set(myClass, (String) "new value");
Chúc mừng! Bạn vừa nắm vững cơ chế phản xạ cơ bản và có thể tiếp cận hiện privatetrường! Hãy chú ý đến khối try/catchvà các loại ngoại lệ được xử lý. Bản thân IDE sẽ cho biết sự hiện diện bắt buộc của họ, nhưng tên của họ sẽ làm rõ lý do tại sao họ có mặt ở đây. Hãy tiếp tục! Như bạn có thể nhận thấy, phương pháp của chúng tôi MyClassđã có một phương thức hiển thị thông tin về dữ liệu lớp:
private void printData(){
       System.out.println(number + name);
   }
Nhưng lập trình viên này cũng để lại một di sản ở đây. Phương thức này nằm trong access modifier privatevà chúng tôi phải tự viết mã đầu ra mỗi lần. Không theo thứ tự, hình ảnh phản chiếu của chúng ta ở đâu?... Hãy viết hàm sau:
public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
Ở đây, quy trình gần giống như khi lấy một trường - chúng tôi lấy phương thức mong muốn theo tên và cấp quyền truy cập vào nó. Và để gọi đối tượng Methodchúng ta sử dụng invoke(Оbject, Args), đâu Оbjectcũng là một thể hiện của lớp MyClass. Args- đối số phương thức - của chúng tôi không có bất kỳ đối số nào. Bây giờ chúng ta sử dụng hàm để hiển thị thông tin printData:
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // outout 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// output 0new value
}
Hoan hô, bây giờ chúng ta đã có quyền truy cập vào phương thức riêng tư của lớp. Nhưng điều gì sẽ xảy ra nếu phương thức vẫn có đối số và tại sao lại có hàm tạo nhận xét? Mọi thứ đều có thời điểm của nó. Từ định nghĩa ở đầu, rõ ràng sự phản chiếu cho phép bạn tạo các thể hiện của một lớp ở một chế độ runtime(trong khi chương trình đang chạy)! Chúng ta có thể tạo một đối tượng của một lớp bằng tên đầy đủ của lớp đó. Tên lớp đủ điều kiện là tên của lớp, được cung cấp đường dẫn đến nó ở định dạng package.
API phản ánh.  Sự phản xạ.  Mặt tối của Java - 3
Trong hệ thống phân cấp của tôi, packagetên đầy đủ MyClasssẽ là “ reflection.MyClass”. Bạn cũng có thể tìm ra tên lớp một cách đơn giản (nó sẽ trả về tên lớp dưới dạng chuỗi):
MyClass.class.getName()
Hãy tạo một thể hiện của lớp bằng cách sử dụng sự phản chiếu:
public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
Tại thời điểm ứng dụng java khởi động, không phải tất cả các lớp đều được tải vào JVM. Nếu mã của bạn không tham chiếu đến lớp MyClassthì bất kỳ ai chịu trách nhiệm tải các lớp vào JVM ClassLoadersẽ không bao giờ tải nó ở đó. Vì vậy, chúng ta cần buộc ClassLoadernó tải và nhận mô tả về lớp của chúng ta dưới dạng một biến có kiểu Class. Đối với nhiệm vụ này, có một phương thức forName(String), trong đó Stringcó tên của lớp có mô tả mà chúng tôi yêu cầu. Sau khi nhận được Сlass, lệnh gọi phương thức newInstance()sẽ trả về Object, phương thức này sẽ được tạo theo cùng một mô tả. Việc còn lại là mang đối tượng này đến lớp của chúng ta MyClass. Mát mẻ! Thật khó khăn, nhưng tôi hy vọng nó có thể hiểu được. Bây giờ chúng ta có thể tạo một thể hiện của một lớp theo đúng nghĩa đen từ một dòng! Thật không may, phương thức được mô tả sẽ chỉ hoạt động với hàm tạo mặc định (không có tham số). Làm cách nào để gọi các phương thức có đối số và hàm tạo có tham số? Đã đến lúc bỏ ghi chú hàm tạo của chúng ta. Đúng như dự đoán, newInstance()nó không tìm thấy hàm tạo mặc định và không hoạt động nữa. Hãy viết lại việc tạo một thể hiện của lớp:
public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
Để lấy các hàm tạo của lớp, hãy gọi phương thức từ mô tả lớp getConstructors()và để lấy các tham số của hàm tạo, hãy gọi getParameterTypes():
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
Bằng cách này, chúng ta có được tất cả các hàm tạo và tất cả các tham số cho chúng. Trong ví dụ của tôi, có một lệnh gọi đến một hàm tạo cụ thể với các tham số cụ thể đã biết. Và để gọi hàm tạo này, chúng ta sử dụng phương thức newInstance, trong đó chúng ta chỉ định giá trị của các tham số này. Điều tương tự sẽ xảy ra invokeđối với các phương thức gọi. Câu hỏi đặt ra: việc gọi hàm tạo phản ánh có thể hữu ích ở đâu? Các công nghệ java hiện đại, như đã đề cập ở phần đầu, không thể hoạt động nếu không có API Reflection. Ví dụ: DI (Tính năng tiêm phụ thuộc), trong đó các chú thích kết hợp với sự phản chiếu của các phương thức và hàm tạo tạo thành thư viện Dagger, phổ biến trong phát triển Android. Sau khi đọc bài viết này, bạn có thể tự tin coi mình đã hiểu biết về các cơ chế của Reflection API. Không phải vô cớ mà sự phản chiếu được gọi là mặt tối của java. Nó hoàn toàn phá vỡ mô hình OOP. Trong java, tính đóng gói dùng để ẩn và hạn chế quyền truy cập của một số thành phần chương trình đối với những thành phần khác. Bằng cách sử dụng công cụ sửa đổi riêng tư, chúng tôi muốn nói rằng quyền truy cập vào trường này sẽ chỉ nằm trong lớp nơi trường này tồn tại, dựa vào đó, chúng tôi xây dựng kiến ​​trúc sâu hơn của chương trình. Trong bài viết này, chúng tôi đã biết cách bạn có thể sử dụng sự phản chiếu để đi đến bất cứ đâu. Một ví dụ điển hình ở dạng giải pháp kiến ​​trúc là mẫu thiết kế tổng quát - Singleton. Ý tưởng chính của nó là trong toàn bộ hoạt động của chương trình, lớp triển khai mẫu này chỉ được có một bản sao. Điều này được thực hiện bằng cách đặt công cụ sửa đổi truy cập mặc định thành riêng tư cho hàm tạo. Và sẽ rất tệ nếu một lập trình viên nào đó tự suy ngẫm lại tạo ra những lớp như vậy. Nhân tiện, có một câu hỏi rất thú vị mà gần đây tôi đã nghe từ nhân viên của mình: liệu một lớp triển khai mẫu có Singletonngười thừa kế không? Có thể ngay cả sự phản ánh cũng bất lực trong trường hợp này? Viết phản hồi của bạn về bài viết và câu trả lời trong phần bình luận, đồng thời đặt câu hỏi của bạn! Sức mạnh thực sự của API Reflection đến từ sự kết hợp với Chú thích thời gian chạy, điều mà có lẽ chúng ta sẽ nói đến trong một bài viết sau về mặt tối của Java. Cám ơn vì sự quan tâm của bạn!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION