JavaRush /Blog Java /Random-PL /API refleksji. Odbicie. Ciemna strona Javy
Oleksandr Klymenko
Poziom 13
Харків

API refleksji. Odbicie. Ciemna strona Javy

Opublikowano w grupie Random-PL
Pozdrawiam, młody Padawan. W tym artykule opowiem Wam o Mocy, z której mocy programiści Java korzystają jedynie w pozornie beznadziejnej sytuacji. Zatem ciemna strona Javy to...Reflection API
API refleksji.  Odbicie.  Ciemna strona Javy – 1
Odbicie w Javie odbywa się przy użyciu interfejsu API Java Reflection. Jaka jest ta refleksja? Istnieje krótka i precyzyjna definicja, która jest również popularna w Internecie. Refleksja (od późnego łacińskiego reflexio – cofanie się) to mechanizm badania danych o programie w trakcie jego wykonywania. Refleksja umożliwia sprawdzenie informacji o polach, metodach i konstruktorach klas. Sam mechanizm refleksji pozwala przetwarzać typy, których brakuje podczas kompilacji, ale pojawiają się podczas wykonywania programu. Refleksja i istnienie logicznie spójnego modelu raportowania błędów pozwala na stworzenie prawidłowego kodu dynamicznego. Innymi słowy, zrozumienie, jak działa refleksja w Javie, otwiera przed Tobą wiele niesamowitych możliwości. Można dosłownie żonglować klasami i ich komponentami.
API refleksji.  Odbicie.  Ciemna strona Javy – 2
Oto podstawowa lista tego, na co pozwala refleksja:
  • Znaleźć/określić klasę obiektu;
  • Uzyskaj informacje o modyfikatorach klas, polach, metodach, stałych, konstruktorach i nadklasach;
  • Dowiedz się, które metody należą do zaimplementowanego interfejsu/interfejsów;
  • Utwórz instancję klasy, a nazwa klasy będzie nieznana do czasu wykonania programu;
  • Pobierz i ustaw wartość pola obiektu według nazwy;
  • Wywołaj metodę obiektu po nazwie.
Reflection jest stosowany w prawie wszystkich nowoczesnych technologiach Java. Trudno sobie wyobrazić, czy Java jako platforma mogłaby osiągnąć tak szerokie zastosowanie bez refleksji. Najprawdopodobniej nie mogłem. Zapoznałeś się z ogólną teoretyczną koncepcją refleksji, teraz przejdźmy do jej praktycznego zastosowania! Nie będziemy badać wszystkich metod Reflection API, a jedynie to, co faktycznie spotyka się w praktyce. Ponieważ mechanizm refleksji wymaga pracy z klasami, będziemy mieli prostą klasę - 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 widzimy, jest to najczęstsza klasa. Konstruktor z parametrami został nie bez powodu zakomentowany, wrócimy do tego później. Jeśli przyjrzałeś się bliżej zawartości klasy, prawdopodobnie zauważyłeś brak getter„a” dla name. Samo pole namejest oznaczone modyfikatorem dostępu private, nie będziemy mogli uzyskać do niego dostępu poza samą klasą, =>nie możemy uzyskać jego wartości. "Więc w czym problem? - mówisz. „Dodaj getterlub zmień modyfikator dostępu.” I będziesz miał rację, ale co jeśli MyClassjest to w skompilowanej bibliotece aar lub w innym zamkniętym module bez uprawnień do edycji, a w praktyce zdarza się to niezwykle często. A jakiś nieuważny programista po prostu zapomniał napisać getter. Czas przypomnieć sobie o refleksji! Spróbujmy dostać się do privatepola nameklasy 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
}
Zastanówmy się teraz, co się tutaj wydarzyło. W Javie jest cudowna klasa Class. Reprezentuje klasy i interfejsy w wykonywalnej aplikacji Java. Nie będziemy dotykać połączenia pomiędzy Classi ClassLoader. nie jest to tematem artykułu. Następnie aby otrzymać pola tej klasy należy wywołać metodę getFields(), ta metoda zwróci nam wszystkie dostępne pola tej klasy. To nie jest dla nas odpowiednie, ponieważ nasze pole to private, więc używamy metody getDeclaredFields().Ta metoda również zwraca tablicę pól klas, ale teraz oba privatei protected. W naszej sytuacji znamy nazwę interesującego nas pola i możemy skorzystać z metody getDeclaredField(String), gdzie Stringjest nazwa pożądanego pola. Notatka: getFields()i getDeclaredFields()nie zwracaj pól klasy nadrzędnej! Świetnie, otrzymaliśmy obiekt Field z linkiem do naszego name. Ponieważ pole nie było публичным(publiczne), należy dać dostęp do pracy z nim. Metoda setAccessible(true)pozwala nam kontynuować pracę. Teraz pole namejest całkowicie pod naszą kontrolą! Możesz uzyskać jego wartość wywołując get(Object)obiekt Field, w którym Objectznajduje się instancja naszej klasy MyClass. Rzutujemy go Stringi przypisujemy do naszej zmiennej name. W przypadku, gdy nagle nie mamy setter„a”, możemy użyć tej metody, aby ustawić nową wartość dla pola nazwy set:
field.set(myClass, (String) "new value");
Gratulacje! Właśnie opanowałeś podstawowy mechanizm odbicia i udało Ci się uzyskać dostęp do privatepola! Zwróć uwagę na blok try/catchi typy obsługiwanych wyjątków. Samo IDE wskaże ich obowiązkową obecność, ale ich nazwa wyjaśnia, dlaczego tu są. Zacząć robić! Jak zapewne zauważyłeś, nasz MyClassma już metodę wyświetlania informacji o danych klasy:
private void printData(){
       System.out.println(number + name);
   }
Ale ten programista również pozostawił po sobie dziedzictwo. Metoda znajduje się pod modyfikatorem dostępu privatei za każdym razem musieliśmy sami pisać kod wyjściowy. To nie tak, gdzie jest nasze odbicie?... Napiszmy 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();
   }
}
Tutaj procedura jest mniej więcej taka sama, jak przy pobieraniu pola - pobieramy żądaną metodę po nazwie i dajemy do niej dostęp. I wywołać obiekt, Methodktórego używamy invoke(Оbject, Args), gdzie Оbjectjest także instancja klasy MyClass. Args- argumenty metody - u nas nie ma żadnych. Teraz używamy funkcji do wyświetlania informacji 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
}
Hurra, teraz mamy dostęp do prywatnej metody klasy. Ale co, jeśli metoda nadal ma argumenty i dlaczego istnieje konstruktor z komentarzem? Wszystko ma swój czas. Z definicji zawartej na początku jasno wynika, że ​​refleksja pozwala na tworzenie instancji klasy w trybie runtime(podczas działania programu)! Możemy utworzyć obiekt klasy według pełnej nazwy tej klasy. W pełni kwalifikowana nazwa klasy to nazwa klasy podana do niej ścieżka w pliku package.
API refleksji.  Odbicie.  Ciemna strona Javy – 3
W mojej hierarchii packagepełna nazwa MyClassbędzie brzmieć „ reflection.MyClass”. Nazwę klasy możesz także znaleźć w prosty sposób (nazwa klasy zostanie zwrócona w postaci ciągu znaków):
MyClass.class.getName()
Stwórzmy instancję klasy za pomocą refleksji:
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
}
W momencie uruchomienia aplikacji Java nie wszystkie klasy są ładowane do maszyny JVM. Jeśli Twój kod nie odnosi się do klasy MyClass, to osoba odpowiedzialna za ładowanie klas do JVM, czyli ClassLoadernigdy jej tam nie załaduje. Musimy zatem wymusić ClassLoaderna nim załadowanie i otrzymanie opisu naszej klasy w postaci zmiennej typu Class. Do tego zadania służy metoda forName(String), gdzie Stringznajduje się nazwa klasy, której opisu potrzebujemy. Po otrzymaniu Сlasswywołanie metody newInstance()zwróci Object, które zostanie utworzone według tego samego opisu. Pozostaje jeszcze przenieść ten obiekt na naszą klasę MyClass. Fajny! Było to trudne, ale mam nadzieję, że jest zrozumiałe. Teraz możemy stworzyć instancję klasy dosłownie z jednej linii! Niestety opisana metoda będzie działać tylko z konstruktorem domyślnym (bez parametrów). Jak wywoływać metody z argumentami i konstruktory z parametrami? Czas odkomentować naszego konstruktora. Zgodnie z oczekiwaniami newInstance()nie znajduje domyślnego konstruktora i już nie działa. Przepiszmy proces tworzenia instancji 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
}
Aby uzyskać konstruktory klas wywołaj metodę z opisu klasy getConstructors(), a parametry konstruktora wywołaj 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();
}
W ten sposób uzyskujemy wszystkie konstruktory i wszystkie parametry do nich. W moim przykładzie następuje wywołanie konkretnego konstruktora o określonych, znanych już parametrach. A żeby wywołać tego konstruktora korzystamy z metody newInstance, w której określamy wartości tych parametrów. To samo stanie się invokez wywoływaniem metod. Powstaje pytanie: gdzie refleksyjne wywoływanie konstruktorów może się przydać? Nowoczesne technologie Java, jak wspomniano na początku, nie mogą obejść się bez Reflection API. Na przykład DI (Dependency Injection), gdzie adnotacje połączone z odzwierciedleniem metod i konstruktorów tworzą popularną w programowaniu Androida bibliotekę Dagger. Po przeczytaniu tego artykułu możesz śmiało uważać się za oświeconego w zakresie mechanizmów API Reflection. Nie bez powodu refleksję nazywa się ciemną stroną Javy. Całkowicie łamie paradygmat OOP. W Javie enkapsulacja służy do ukrywania i ograniczania dostępu niektórych komponentów programu do innych. Używając modyfikatora private mamy na myśli, że dostęp do tego pola będzie miał tylko w obrębie klasy, w której to pole istnieje, na tej podstawie budujemy dalszą architekturę programu. W tym artykule widzieliśmy, jak możesz wykorzystać odbicie, aby dotrzeć do dowolnego miejsca. Dobrym przykładem w postaci rozwiązania architektonicznego jest generatywny wzorzec projektowy – Singleton. Jego główną ideą jest to, że przez całe działanie programu klasa implementująca ten szablon powinna mieć tylko jedną kopię. Odbywa się to poprzez ustawienie domyślnego modyfikatora dostępu na prywatny dla konstruktora. A będzie bardzo źle, jeśli jakiś programista z własną refleksją stworzy takie klasy. Swoją drogą, ostatnio usłyszałem od mojego pracownika bardzo ciekawe pytanie: czy klasa implementująca szablon może mieć Singletonspadkobierców? Czy to możliwe, że nawet refleksja jest w tym przypadku bezsilna? Napisz swoją opinię na temat artykułu i odpowiedź w komentarzach, a także zadaj pytania! Prawdziwa moc interfejsu API Reflection pojawia się w połączeniu z adnotacjami środowiska wykonawczego, o których prawdopodobnie porozmawiamy w przyszłym artykule o ciemnej stronie Javy. Dziękuję za uwagę!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION