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.
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):
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!
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.
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 żeMyClass
znajdował 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 Class
i 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 gdzieString
jest nazwą żądanego pola. Notatka: getFields()
i getDeclaredFields()
nie zwracaj pól klasy nadrzędnej! Świetnie. Mamy Field
obiekt 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ę Field
obiektu get(Object)
, gdzie Object
jest instancją naszej MyClass
klasy. Konwertujemy typ na String
i 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/catch
blok 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 MyClass
klasa 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 Method
obiekcie wywołujemy invoke(Object, Args)
metodę, gdzie Object
jest też instancja klasy MyClass
. Args
to argumenty metody, chociaż nasza nie ma żadnych. Teraz używamy printData
funkcji 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 .

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 MyClass
klasy, to ClassLoader
, który jest odpowiedzialny za ładowanie klas do JVM, nigdy nie załaduje klasy. Oznacza to, że musisz wymusić ClassLoader
załadowanie go i uzyskanie opisu klasy w postaci Class
zmiennej. Dlatego mamy forName(String)
metodę, gdzie String
jest nazwa klasy, której opisu potrzebujemy. Po otrzymaniu Сlass
obiektu wywołanie metody newInstance()
zwróci Object
obiekt utworzony przy użyciu tego opisu. Pozostało tylko dostarczyć ten obiekt do naszegoMyClass
klasa. 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 newInstance
metody, do której przekazujemy wartości tych parametrów. Tak samo będzie w przypadku używania invoke
metod 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.
Więcej czytania: |
---|
GO TO FULL VERSION