CodeGym /Java Blog /Willekeurig /Reflectie-API: Reflectie. De donkere kant van Java
John Squirrels
Niveau 41
San Francisco

Reflectie-API: Reflectie. De donkere kant van Java

Gepubliceerd in de groep Willekeurig
Groeten, jonge Padawan. In dit artikel vertel ik je over de Force, een kracht die Java-programmeurs alleen gebruiken in schijnbaar onmogelijke situaties. De duistere kant van Java is de Reflection API. In Java wordt reflectie geïmplementeerd met behulp van de Java Reflection API.

Wat is Java-reflectie?

Er is een korte, nauwkeurige en populaire definitie op internet. Reflectie ( van het laat-Latijnse reflexio - terugdraaien ) is een mechanisme om gegevens over een programma te onderzoeken terwijl het draait. Met Reflectie kunt u informatie verkennen over velden, methoden en klassenbouwers. Met Reflection kunt u werken met typen die niet aanwezig waren tijdens het compileren, maar die beschikbaar kwamen tijdens runtime. Reflectie en een logisch consistent model voor het afgeven van foutinformatie maken het mogelijk om correcte dynamische code te creëren. Met andere woorden, als u begrijpt hoe reflectie op Java werkt, krijgt u een aantal geweldige kansen. Je kunt letterlijk jongleren met klassen en hun onderdelen. Hier is een basislijst van wat reflectie mogelijk maakt:
  • Leer / bepaal de klasse van een object;
  • Krijg informatie over de modifiers, velden, methoden, constanten, constructors en superklassen van een klasse;
  • Ontdek welke methodes bij de geïmplementeerde interface(s) horen;
  • Maak een instantie van een klasse waarvan de klassenaam tot de uitvoeringstijd onbekend is;
  • Waarden van de velden van een object ophalen en instellen op naam;
  • Roep de methode van een object bij naam aan.
Reflectie wordt gebruikt in bijna alle moderne Java-technologieën. Het is moeilijk voor te stellen dat Java als platform zonder nadenken zo'n wijdverbreide acceptatie had kunnen bereiken. Waarschijnlijk niet. Nu u over het algemeen bekend bent met reflectie als een theoretisch concept, gaan we over tot de praktische toepassing ervan! We zullen niet alle methoden van de Reflection API leren, alleen degene die u in de praktijk zult tegenkomen. Aangezien reflectie het werken met klassen inhoudt, beginnen we met een eenvoudige klasse genaamd 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);
   }
}
Zoals je kunt zien, is dit een zeer basisklasse. De constructor met parameters is bewust uitgeschreven. We komen daar later op terug. Als je goed naar de inhoud van de klasse hebt gekeken, is het je waarschijnlijk opgevallen dat er geen getter voor het naamveld is . Het naamveld zelf is gemarkeerd met de private access modifier: we hebben er geen toegang toe buiten de klasse zelf, wat betekent dat we de waarde ervan niet kunnen ophalen. " Dus wat is het probleem ?" jij zegt. "Voeg een getter toe of wijzig de toegangsmodifier". En je zou gelijk hebben, tenzijMyClasswas in een gecompileerde AAR-bibliotheek of in een andere privémodule zonder de mogelijkheid om wijzigingen aan te brengen. In de praktijk gebeurt dit de hele tijd. En een onzorgvuldige programmeur vergat gewoon een getter te schrijven . Dit is hét moment om reflectie te onthouden! Laten we proberen naar het privénaamveld van de MyClassklas te gaan:

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
}
Laten we analyseren wat er net is gebeurd. In Java is er een prachtige klasse genaamd Class. Het vertegenwoordigt klassen en interfaces in een uitvoerbare Java-toepassing. We zullen de relatie tussen Classen niet behandelen ClassLoader, aangezien dat niet het onderwerp van dit artikel is. Vervolgens moet u de methode aanroepen om de velden van deze klasse op te halen getFields(). Deze methode retourneert alle toegankelijke velden van deze klasse. Dit werkt niet voor ons, omdat ons veld privé is , dus gebruiken we de getDeclaredFields()methode. Deze methode retourneert ook een reeks klassenvelden, maar bevat nu privé- en beveiligde velden. In dit geval kennen we de naam van het veld waarin we geïnteresseerd zijn, zodat we de getDeclaredField(String)methode waar kunnen gebruikenStringis de naam van het gewenste veld. Opmerking: getFields()en getDeclaredFields()retourneer niet de velden van een bovenliggende klasse! Geweldig. We hebben een Fieldobject dat naar onze naam verwijst . Aangezien het veld niet openbaar was , moeten we toegang verlenen om ermee te werken. De setAccessible(true)methode laat ons verder gaan. Nu staat het naamveld onder onze volledige controle! U kunt de waarde ervan ophalen door de methode Fieldvan het object aan te roepen, waarbij een instantie van onze klasse is. We converteren het type naar en wijzen de waarde toe aan onze naamvariabele . Als we geen setter kunnen vinden om een ​​nieuwe waarde in het naamveld in te stellen, kunt u de set- methode gebruiken: get(Object)ObjectMyClassString

field.set(myClass, (String) "new value");
Gefeliciteerd! Je hebt zojuist de basis van reflectie onder de knie en hebt toegang gekregen tot een privéveld ! Besteed aandacht aan het try/catchblok en de soorten uitzonderingen die worden afgehandeld. De IDE zal je vertellen dat hun aanwezigheid op zichzelf vereist is, maar je kunt aan hun naam duidelijk zien waarom ze hier zijn. Verder gaan! Zoals je misschien hebt gemerkt, MyClassheeft onze klas al een methode om informatie over klasgegevens weer te geven:

private void printData(){
       System.out.println(number + name);
   }
Maar ook hier heeft deze programmeur zijn vingerafdrukken achtergelaten. De methode heeft een privé- toegangsmodificator en we moeten onze eigen code schrijven om elke keer gegevens weer te geven. Wat een puinhoop. Waar is onze reflectie gebleven? Schrijf de volgende functie:

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();
   }
}
De procedure is hier ongeveer dezelfde als die voor het ophalen van een veld. We benaderen de gewenste methode op naam en verlenen er toegang toe. En op het Methodobject noemen we de invoke(Object, Args)methode, waar Objectook een instantie van de MyClassklasse is. Argszijn de argumenten van de methode, hoewel die van ons er niet zijn. Nu gebruiken we de printDatafunctie om informatie weer te geven:

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
}
Hoera! Nu hebben we toegang tot de privémethode van de klas. Maar wat als de methode wel argumenten heeft, en waarom wordt de constructor becommentarieerd? Alles op zijn tijd. Het is duidelijk uit de definitie aan het begin dat reflectie je in staat stelt instanties van een klasse te creëren tijdens runtime (terwijl het programma draait)! We kunnen een object maken met de volledige naam van de klasse. De volledige naam van de klasse is de naam van de klasse, inclusief het pad van het pakket .
Reflectie-API: Reflectie.  De donkere kant van Java - 2
In mijn pakkethiërarchie zou de volledige naam van MyClass "reflection.MyClass" zijn. Er is ook een eenvoudige manier om de naam van een klas te leren (retourneer de naam van de klas als een tekenreeks):

MyClass.class.getName()
Laten we Java-reflectie gebruiken om een ​​instantie van de klasse te maken:

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
}
Wanneer een Java-toepassing wordt gestart, worden niet alle klassen in de JVM geladen. Als uw code niet naar de MyClassklasse verwijst, ClassLoaderzal , die verantwoordelijk is voor het laden van klassen in de JVM, de klasse nooit laden. Dat betekent dat je moet forceren ClassLoaderom het te laden en een klassebeschrijving in de vorm van een Classvariabele te krijgen. forName(String)Dit is waarom we de methode hebben , waar Stringis de naam van de klasse waarvan we de beschrijving nodig hebben. Nadat het Сlassobject is opgehaald, zal het aanroepen van de methode newInstance()een object retourneren Objectdat met die beschrijving is gemaakt. Het enige dat overblijft is om dit object aan ons te leverenMyClassklas. Koel! Dat was moeilijk, maar begrijpelijk, hoop ik. Nu kunnen we een instantie van een klasse maken in letterlijk één regel! Helaas werkt de beschreven aanpak alleen met de standaardconstructor (zonder parameters). Hoe noem je methodes en constructors met parameters? Het is tijd om onze constructor ongedaan te maken. Zoals verwacht, newInstance()kan de standaardconstructor niet vinden en werkt niet meer. Laten we de instantiëring van de klasse herschrijven:

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
}
De getConstructors()methode moet worden aangeroepen op de klassendefinitie om klassenconstructors te verkrijgen, en getParameterTypes()moet vervolgens worden aangeroepen om de parameters van een constructor op te halen:

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
Dat levert ons alle constructors en hun parameters op. In mijn voorbeeld verwijs ik naar een specifieke constructor met specifieke, eerder bekende parameters. En om deze constructor aan te roepen, gebruiken we de newInstancemethode, waaraan we de waarden van deze parameters doorgeven. Het zal hetzelfde zijn bij het gebruik van invokemethoden om aan te roepen. Dit roept de vraag op: wanneer is het handig om constructeurs te bellen door middel van reflectie? Zoals in het begin al vermeld, kunnen moderne Java-technologieën niet zonder de Java Reflection API. Bijvoorbeeld Dependency Injection (DI), dat annotaties combineert met reflectie van methoden en constructors om de populaire Darer te vormenbibliotheek voor Android-ontwikkeling. Na het lezen van dit artikel kun je vol vertrouwen beschouwen dat je bent opgeleid in de manieren van de Java Reflection API. Ze noemen reflectie niet voor niets de donkere kant van Java. Het breekt volledig met het OOP-paradigma. In Java verbergt en beperkt inkapseling de toegang van anderen tot bepaalde programmacomponenten. Wanneer we de private modifier gebruiken, is het de bedoeling dat dat veld alleen toegankelijk is vanuit de klasse waar het bestaat. En op basis van dit principe bouwen we de daaropvolgende architectuur van het programma. In dit artikel hebben we gezien hoe je reflectie kunt gebruiken om je overal een weg te banen. Het creatieve ontwerppatroon Singletonis hier een goed voorbeeld van als architectonische oplossing. Het basisidee is dat een klasse die dit patroon implementeert slechts één instantie heeft tijdens de uitvoering van het hele programma. Dit wordt bereikt door de private access modifier toe te voegen aan de standaard constructor. En het zou heel erg zijn als een programmeur reflectie zou gebruiken om meer instanties van dergelijke klassen te maken. Trouwens, ik hoorde onlangs een collega een zeer interessante vraag stellen: kan een klasse die het Singleton-patroon implementeert, worden geërfd? Zou het kunnen dat in dit geval zelfs reflectie machteloos zou zijn? Laat je feedback over het artikel en je antwoord achter in de reacties hieronder, en stel daar je eigen vragen!
Opmerkingen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION