CodeGym /Blog Java /Random-PL /Odbicie API: Odbicie. Ciemna strona Jawy
Autor
Pavlo Plynko
Java Developer at CodeGym

Odbicie API: Odbicie. Ciemna strona Jawy

Opublikowano w grupie Random-PL
Witaj, młody Padawan. W tym artykule opowiem o Mocy, mocy, której programiści Javy używają tylko w pozornie niemożliwych sytuacjach. Ciemną stroną Javy jest Reflection API. W Javie odbicie jest realizowane przy użyciu API Java Reflection.

Co to jest odbicie Java?

W Internecie istnieje krótka, dokładna i popularna definicja. Refleksja ( z późnej łac. reflexio - zawrócić ) to mechanizm eksploracji danych o programie podczas jego działania. Refleksja pozwala eksplorować informacje o polach, metodach i konstruktorach klas. Odbicie umożliwia pracę z typami, które nie były obecne w czasie kompilacji, ale stały się dostępne w czasie wykonywania. Refleksja i logicznie spójny model wydawania informacji o błędach umożliwiają stworzenie poprawnego kodu dynamicznego. Innymi słowy, zrozumienie, jak refleksja działa w Javie, otworzy przed Tobą wiele niesamowitych możliwości. Możesz dosłownie żonglować klasami i ich komponentami. Oto podstawowa lista tego, na co pozwala refleksja:
  • Poznaj/określ klasę obiektu;
  • Uzyskaj informacje o modyfikatorach klasy, polach, metodach, stałych, konstruktorach i klasach nadrzędnych;
  • Dowiedz się, jakie metody należą do zaimplementowanych interfejsów;
  • Utwórz instancję klasy, której nazwa klasy jest nieznana do czasu wykonania;
  • Pobieraj i ustawiaj wartości pól obiektu według nazwy;
  • Wywołaj metodę obiektu według nazwy.
Odbicie jest używane w prawie wszystkich nowoczesnych technologiach Java. Trudno sobie wyobrazić, że Java, jako platforma, mogła bez zastanowienia osiągnąć tak szerokie przyjęcie. Najprawdopodobniej nie miałby. Teraz, kiedy już ogólnie zaznajomiłeś się z refleksją jako koncepcją teoretyczną, przejdźmy do jej praktycznego zastosowania! Nie poznamy wszystkich metod Reflection API — tylko te, z którymi faktycznie spotkasz się w praktyce. Ponieważ refleksja obejmuje pracę z klasami, zaczniemy od prostej klasy o nazwie 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);
   }
}
Jak widać, jest to bardzo podstawowa klasa. Konstruktor z parametrami jest celowo komentowany. Wrócimy do tego później. Jeśli uważnie przyjrzałeś się zawartości klasy, prawdopodobnie zauważyłeś brak gettera dla pola name . Samo pole name jest oznaczone modyfikatorem private access: nie możemy uzyskać do niego dostępu poza samą klasą, co oznacza, że ​​nie możemy odzyskać jego wartości . Więc w czym problem ?” mówisz. „Dodaj moduł pobierający lub zmień modyfikator dostępu”. I miałbyś rację, chyba żeMyClassznajdował się w skompilowanej bibliotece AAR lub w innym prywatnym module bez możliwości wprowadzania zmian. W praktyce dzieje się tak cały czas. A jakiś nieostrożny programista po prostu zapomniał napisać gettera . To jest właśnie czas, aby przypomnieć sobie refleksję! Spróbujmy dostać się do pola nazwy prywatnej klasy 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
}
Przeanalizujmy, co się właśnie wydarzyło. W Javie istnieje wspaniała klasa o nazwie Class. Reprezentuje klasy i interfejsy w wykonywalnej aplikacji Java. Nie będziemy omawiać relacji między Classi ClassLoader, ponieważ nie jest to tematem tego artykułu. Następnie, aby pobrać pola tej klasy, musisz wywołać getFields()metodę. Ta metoda zwróci wszystkie dostępne pola tej klasy. To nie działa dla nas, ponieważ nasze pole jest prywatne , więc używamy tej getDeclaredFields()metody. Ta metoda zwraca również tablicę pól klasy, ale teraz zawiera pola prywatne i chronione . W tym przypadku znamy nazwę interesującego nas pola, więc możemy skorzystać z getDeclaredField(String)metody gdzieStringjest nazwą żądanego pola. Notatka: getFields()i getDeclaredFields()nie zwracaj pól klasy nadrzędnej! Świetnie. Mamy Fieldobiekt odwołujący się do naszej nazwy . Ponieważ pole nie było publiczne , musimy udzielić dostępu do pracy z nim. Metoda setAccessible(true)pozwala przejść dalej. Teraz pole nazwy jest pod naszą pełną kontrolą! Możesz pobrać jego wartość, wywołując metodę Fieldobiektu get(Object), gdzie Objectjest instancją naszej MyClassklasy. Konwertujemy typ na Stringi przypisujemy wartość do naszej zmiennej name . Jeśli nie możemy znaleźć setera , który ustawi nową wartość w polu nazwy, możesz użyć metody set :

field.set(myClass, (String) "new value");
Gratulacje! Właśnie opanowałeś podstawy refleksji i uzyskałeś dostęp do prywatnego pola! Zwróć uwagę na try/catchblok i typy obsługiwanych wyjątków. IDE powie ci, że ich obecność jest wymagana sama, ale możesz wyraźnie powiedzieć po ich imionach, dlaczego tu są. Iść dalej! Jak zapewne zauważyłeś, nasza MyClassklasa ma już metodę wyświetlania informacji o danych klasy:

private void printData(){
       System.out.println(number + name);
   }
Ale ten programista też zostawił tu swoje odciski palców. Metoda ma prywatny modyfikator dostępu i musimy napisać własny kod, aby za każdym razem wyświetlać dane. Co za bałagan. Gdzie podziało się nasze odbicie? Napisz następującą funkcję:

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();
   }
}
Procedura tutaj jest mniej więcej taka sama, jak ta używana do pobierania pola. Uzyskujemy dostęp do żądanej metody po nazwie i udzielamy do niej dostępu. A na Methodobiekcie wywołujemy invoke(Object, Args)metodę, gdzie Objectjest też instancja klasy MyClass. Argsto argumenty metody, chociaż nasza nie ma żadnych. Teraz używamy printDatafunkcji do wyświetlenia informacji:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // Output: 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
}
Hurra! Teraz mamy dostęp do prywatnej metody klasy. Ale co, jeśli metoda ma argumenty i dlaczego konstruktor jest komentowany? Wszystko w swoim czasie. Z definicji na początku jasno wynika, że ​​refleksja pozwala tworzyć instancje klasy w czasie wykonywania (podczas działania programu)! Możemy utworzyć obiekt używając pełnej nazwy klasy. Pełna nazwa klasy to nazwa klasy, w tym ścieżka do jej pakietu .
Odbicie API: Odbicie.  Ciemna strona Javy - 2
W mojej hierarchii pakietów pełna nazwa MyClass to „reflection.MyClass”. Istnieje również prosty sposób na poznanie nazwy klasy (zwróć nazwę klasy jako ciąg znaków):

MyClass.class.getName()
Użyjmy refleksji Java, aby utworzyć instancję klasy:

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
}
Podczas uruchamiania aplikacji Java nie wszystkie klasy są ładowane do JVM. Jeśli twój kod nie odnosi się do MyClassklasy, to ClassLoader, który jest odpowiedzialny za ładowanie klas do JVM, nigdy nie załaduje klasy. Oznacza to, że musisz wymusić ClassLoaderzaładowanie go i uzyskanie opisu klasy w postaci Classzmiennej. Dlatego mamy forName(String)metodę, gdzie Stringjest nazwa klasy, której opisu potrzebujemy. Po otrzymaniu Сlassobiektu wywołanie metody newInstance()zwróci Objectobiekt utworzony przy użyciu tego opisu. Pozostało tylko dostarczyć ten obiekt do naszegoMyClassklasa. Fajny! To było trudne, ale mam nadzieję, że zrozumiałe. Teraz możemy stworzyć instancję klasy dosłownie w jednej linii! Niestety opisane podejście zadziała tylko z domyślnym konstruktorem (bez parametrów). Jak wywoływać metody i konstruktory z parametrami? Czas odkomentować naszego konstruktora. Zgodnie z oczekiwaniami newInstance()nie można znaleźć domyślnego konstruktora i już nie działa. Przepiszmy instancję klasy:

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
}
Metoda getConstructors()powinna zostać wywołana na definicji klasy w celu uzyskania konstruktorów klas, a następnie getParameterTypes()powinna zostać wywołana w celu uzyskania parametrów konstruktora:

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
To daje nam wszystkie konstruktory i ich parametry. W moim przykładzie odwołuję się do konkretnego konstruktora o określonych, znanych wcześniej parametrach. A do wywołania tego konstruktora używamy newInstancemetody, do której przekazujemy wartości tych parametrów. Tak samo będzie w przypadku używania invokemetod do wywoływania. Nasuwa się pytanie: kiedy przydaje się wywoływanie konstruktorów poprzez refleksję? Jak już wspomniano na początku, nowoczesne technologie Java nie mogą obejść się bez Java Reflection API. Na przykład Dependency Injection (DI), który łączy adnotacje z odbiciem metod i konstruktorów, tworząc popularny Darerbiblioteka do programowania na Androida. Po przeczytaniu tego artykułu możesz śmiało uważać się za wykształconego w zakresie API Java Reflection. Nie bez powodu nazywają refleksję ciemną stroną Javy. To całkowicie łamie paradygmat OOP. W Javie enkapsulacja ukrywa i ogranicza dostęp innych do niektórych komponentów programu. Kiedy używamy modyfikatora private, chcemy, aby to pole było dostępne tylko z poziomu klasy, w której istnieje. I na tej zasadzie budujemy późniejszą architekturę programu. W tym artykule zobaczyliśmy, jak możesz użyć refleksji, aby przebić się gdziekolwiek. Kreacyjny wzorzec projektowy Singletonjest dobrym przykładem takiego rozwiązania architektonicznego. Podstawową ideą jest to, że klasa implementująca ten wzorzec będzie miała tylko jedną instancję podczas wykonywania całego programu. Osiąga się to przez dodanie prywatnego modyfikatora dostępu do domyślnego konstruktora. I byłoby bardzo źle, gdyby programista użył refleksji do stworzenia większej liczby instancji takich klas. Nawiasem mówiąc, ostatnio słyszałem, jak współpracownik zadał bardzo interesujące pytanie: czy klasa, która implementuje wzorzec Singleton, może być dziedziczona? Czy to możliwe, że w tym przypadku nawet refleksja byłaby bezsilna? Zostaw swoją opinię na temat artykułu i odpowiedź w komentarzach poniżej i zadaj tam własne pytania!

Więcej czytania:

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION