Implementierung der Spring Framework-API von Grund auf. Komplettlösung für Anfänger. Teil 1

  • Tutorial


Das Spring Framework ist eines der komplexesten Frameworks zum Verstehen und Lernen. Die meisten Entwickler lernen es langsam, durch praktische Aufgaben und Google. Dieser Ansatz ist nicht effektiv, da er kein vollständiges Bild liefert und gleichzeitig teuer ist.

Ich möchte Ihnen einen grundlegend neuen Ansatz für das Studium des Frühlings anbieten. Es besteht darin, dass eine Person eine Reihe von speziell vorbereiteten Tutorials durchläuft und selbständig die Federfunktion ausführt. Die Besonderheit dieses Ansatzes besteht darin, dass es neben einem 100% igen Verständnis der untersuchten Aspekte von Spring auch eine erhebliche Steigerung von Java Core (Annotations, Reflection, Files, Generics) gibt.

Der Artikel wird Ihnen ein unvergessliches Erlebnis bereiten und Sie wie ein Pivotal-Entwickler fühlen lassen. Schritt für Schritt machen Sie Ihre Klassen zu etwas Besonderem und organisieren ihren Lebenszyklus (wie in einem echten Frühling). Die Klassen, die Sie implementieren, sind BeanFactory , Component , Service , BeanPostProcessor , BeanNameAware , BeanFactoryAware , InitializingBean , PostConstruct , PreDestroy , DisposableBean , ApplicationContext , ApplicationListener , ContextClosedEvent .

Ein bisschen über mich


Mein Name ist Jaroslaw und ich bin ein Java-Entwickler mit 4 Jahren Erfahrung. Im Moment arbeite ich für EPAM Systems (SPB) und beschäftige mich intensiv mit den Technologien, die wir verwenden. Sehr oft muss ich mich mit dem Frühling auseinandersetzen und sehe darin einen Mittelweg, auf dem man wachsen kann (jeder kennt Java gut, aber zu spezifische Werkzeuge und Technologien können kommen und gehen).

Vor ein paar Monaten habe ich die Spring Professional v5.0-Zertifizierung bestanden (ohne Kurse zu belegen). Danach dachte ich darüber nach, wie ich anderen Menschen das Springen beibringen könnte. Leider gibt es derzeit keine effektive Lehrmethode. Die meisten Entwickler haben eine sehr oberflächliche Vorstellung von dem Framework und seinen Funktionen. Das Debuggen der Spring-Quellen ist zu schwierig und aus Trainingssicht absolut nicht effektiv (ich war irgendwie süchtig danach). 10 Projekte machen? Ja, irgendwo können Sie Ihr Wissen vertiefen und viel praktische Erfahrung sammeln, aber vieles, was sich „unter der Haube“ befindet, wird sich nie vor Ihnen öffnen. Frühling in Aktion lesen? Cool, aber aufwendig. Ich habe es zu 40% geschafft (während der Vorbereitung auf die Zertifizierung), aber es war nicht einfach.

Die einzige Möglichkeit, etwas bis zum Ende zu verstehen, besteht darin, es selbst zu entwickeln. Vor kurzem hatte ich die Idee, dass Sie eine Person durch ein interessantes Tutorial führen können, das die Entwicklung des DI-Frameworks überwacht. Das Hauptmerkmal wird sein, dass die API mit der zu untersuchenden API übereinstimmt. Das Besondere an diesem Ansatz ist, dass eine Person nicht nur ein tiefes Verständnis des Frühlings (ohne Leerzeichen) hat, sondern auch eine RIESIGE Erfahrung in Java Core hat. Ehrlich gesagt habe ich während der Vorbereitung des Artikels sowohl auf Spring als auch auf Java Core viel Neues gelernt. Fangen wir an!

Projekt von Grund auf neu


Das erste, was Sie tun müssen, ist, Ihre Lieblings-IDE zu öffnen und ein Projekt von Grund auf neu zu erstellen. Wir werden keine Maven oder Bibliotheken von Drittanbietern verbinden. Wir werden Spring-Abhängigkeiten nicht einmal verbinden. Unser Ziel ist es, eine API zu entwickeln, die der Spring-API so ähnlich wie möglich ist, und sie selbst zu implementieren.

Erstellen Sie in einem sauberen Projekt 2 Hauptpakete. Das erste Paket ist Ihre Anwendung ( com.kciray) und die KlasseMain.javadrinnen. Das zweite Paket ist org.springframework. Ja, wir duplizieren die Paketstruktur der ursprünglichen Quelle, den Namen ihrer Klassen und ihre Methoden. Es gibt einen so interessanten Effekt: Wenn Sie etwas Eigenes erschaffen, scheint es Ihnen einfach und verständlich. Wenn Sie dann in großen Projekten arbeiten, wird es Ihnen so erscheinen, als ob dort alles basierend auf Ihrem Werkstück erstellt wird. Dieser Ansatz kann sich sehr positiv auf das Verständnis des Systems als Ganzes auswirken, es verbessern, Fehler beheben, Probleme lösen usw.

Wenn Sie irgendwelche Probleme haben, können Sie hier ein Arbeitsprojekt machen .

Erstellen Sie einen Container


Legen Sie zunächst die Aufgabe fest. Stellen Sie sich vor, wir haben 2 Klassen - ProductFacadeund PromotionService. Stellen Sie sich nun vor, Sie möchten diese Klassen miteinander verbinden, aber die Klassen selbst kennen sich nicht aus (Muster DI). Wir benötigen eine separate Klasse, die alle diese Klassen verwaltet und die Abhängigkeiten zwischen ihnen ermittelt. Nennen wir es einen Container. Erstellen Sie eine KlasseContainer... Nein, warte! Spring hat keine einzige Containerklasse. Wir haben viele Containerimplementierungen, und alle diese Implementierungen können in zwei Typen unterteilt werden - bin factories und context. Die Bin-Factory erstellt Beans und verknüpft sie miteinander (Dependency Injection, DI). Der Kontext übernimmt die gleichen Aufgaben und fügt einige zusätzliche Funktionen hinzu (z. B. das Internationalisieren von Nachrichten). Da wir diese zusätzlichen Funktionen jetzt nicht benötigen, werden wir mit der Müllfabrik arbeiten.

Erstellen Sie eine neue Klasse BeanFactoryund fügen Sie sie in ein Paket ein org.springframework.beans.factory. Lassen Sie es in dieser Klasse aufbewahren , in der die Bean der Bean selbst zugeordnet ist. Fügen Sie eine Methode hinzu , die Beans nach Bezeichner abruft.Map singletonsidObject getBean(String beanName)

public class BeanFactory {
    private Map singletons = new HashMap();
    public Object getBean(String beanName){
        return singletons.get(beanName);
    }
}

Beachten Sie, dass BeanFactoryund FactoryBeanzwei verschiedene Dinge sind. Das erste ist die Behälterfabrik (Container), und das zweite ist die Behälterfabrik, die sich im Container befindet und auch Behälter produziert. Fabrik in der Fabrik. Wenn Sie zwischen diesen Definitionen verwechselt werden, können Sie sich daran erinnern, dass das zweite Substantiv im Englischen das führende und das erste so etwas wie ein Adjektiv ist. In der Bohnenfabrik ist das Hauptwort die Fabrik und in der Bohnenfabrik die Bohne.

Erstellen Sie jetzt Klassen ProductServiceund PromotionsService.ProductServicewird das Produkt aus der Datenbank zurückgeben, Sie müssen jedoch vorher prüfen, ob für dieses Produkt Rabatte (Werbeaktionen) gelten. Im E-Commerce wird ermäßigte Arbeit häufig einer separaten Serviceklasse (und manchmal einem Webdienst eines Drittanbieters) zugewiesen.

public class PromotionsService {
}
public class ProductService {
    private PromotionsService promotionsService;
    public PromotionsService getPromotionsService() {
        return promotionsService;
    }
    public void setPromotionsService(PromotionsService promotionsService) {
        this.promotionsService = promotionsService;
    }
}

Jetzt müssen wir unseren Container ( BeanFactory) dazu bringen, unsere Klassen zu erkennen, sie für uns zu erstellen und eine in die andere zu injizieren. Typoperationen new ProductService()sollten sich im Container befinden und für den Entwickler ausgeführt werden. Verwenden wir den modernsten Ansatz (Klassenscannen und Anmerkungen). Dazu müssen wir eine Annotation @Component( пакет org.springframework.beans.factory.stereotype) mit Stiften erstellen .

@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}

Standardmäßig werden Anmerkungen nicht in den Speicher geladen, während das Programm ausgeführt wird ( RetentionPolicy.CLASS). Wir haben dieses Verhalten durch die neue Aufbewahrungsrichtlinie ( RetentionPolicy.RUNTIME) geändert .

Fügen Sie jetzt @Componentvor Klassen ProductServiceund vorher hinzu PromotionService.

@Component
public class ProductService {
    //...
}
@Component
public class PromotionService {
    //...
}


Wir müssen BeanFactoryunser Paket ( com.kciray) scannen und Klassen darin finden, die mit Anmerkungen versehen sind @Component. Diese Aufgabe ist alles andere als trivial. In Java Core gibt es keine fertige Lösung , und wir müssen selbst eine Krücke bauen. Tausende von Federanwendungen verwenden die Bauteilabtastung über diese Krücke. Du hast die schreckliche Wahrheit gelernt. Sie müssen aus ClassLoaderdem Dateinamen extrahieren und prüfen, ob sie mit ".class" enden oder nicht, und dann ihren vollständigen Namen erstellen und Klassenobjekte daraus ziehen!

Ich möchte Sie sofort warnen, dass es viele überprüfte Ausnahmen geben wird, seien Sie also bereit, sie zu verpacken. Aber zuerst entscheiden wir, was wir wollen. Wir möchten eine spezielle Methode hinzufügen BeanFactoryund diese aufrufen Main:

//BeanFactory.java
public class BeanFactory{
    public void instantiate(String basePackage) {
    }
}
//Main.java
BeanFactory beanFactory = new BeanFactory();
beanFactory.instantiate("com.kciray");

Als nächstes müssen wir bekommen ClassLoader. Es ist für das Laden der Klassen verantwortlich und es wird ziemlich einfach:

ClassLoader classLoader = ClassLoader.getSystemClassLoader();

Sie haben wahrscheinlich bereits bemerkt, dass die Pakete durch einen Punkt und die Dateien durch einen Schrägstrich voneinander getrennt sind. Wir müssen den Stapelpfad in den Ordnerpfad konvertieren und so etwas wie (Pfade in Ihrem Dateisystem, mit denen Sie nach Klassendateien suchen können) abrufen.List

String path = basePackage.replace('.', '/'); //"com.kciray" -> "com/kciray"
Enumeration resources = classLoader.getResources(path);

Also warte einen Moment! es ist nicht . Worum geht es hier? Oh, Horror, dies ist ein alter Vorläufer , der seit Java 1.0 verfügbar ist. Mit diesem Erbe müssen wir uns befassen. Wenn Sie mit for durchgehen können (alle Sammlungen implementieren es), müssen Sie für den Fall , dass Sie einen Handle-Bypass durchführen, mit und durch . Es gibt jedoch keine Möglichkeit, Elemente aus der Sammlung zu entfernen. Nur 1996, nur Hardcore. Oh ja, Java 9 hat eine Methode hinzugefügt , damit Sie sie durcharbeiten können.EnumerationListIteratorIterable Enumeration while(resources.hasMoreElements())nextElement()Enumeration.asIterator()

Lass uns weiter gehen. Wir müssen die Ordner extrahieren und den Inhalt der einzelnen Ordner durcharbeiten. Konvertieren Sie die URL in eine Datei und rufen Sie den Namen ab. Hierbei ist zu beachten, dass verschachtelte Pakete nicht gescannt werden, um den Code nicht zu komplizieren. Sie können Ihre Aufgabe komplizieren und eine Rekursion durchführen, wenn Sie dies wünschen.

while (resources.hasMoreElements()) {
    URL resource = resources.nextElement();
    File file = new File(resource.toURI());
    for(File classFile : file.listFiles()){
        String fileName = classFile.getName();//ProductService.class
    }
}

Als nächstes müssen wir den Dateinamen ohne die Erweiterung erhalten. In der Werft im Jahr 2018 hat Java File I / O (NIO 2) für viele Jahre entwickelt, kann jedoch die Erweiterung nicht vom Dateinamen trennen. Ich muss mein eigenes Fahrrad bauen, weil Wir haben uns entschieden, keine Bibliotheken von Drittanbietern wie Apache Commons zu verwenden. Lass uns den alten Großvater-Weg benutzen lastIndexOf("."):

if(fileName.endsWith(".class")){
    String className = fileName.substring(0, fileName.lastIndexOf("."));
}

Als nächstes können wir das Klassenobjekt unter Verwendung des vollständigen Namens der Klasse erhalten (hierfür nennen wir die Klasse der Klasse Class):

Class classObject = Class.forName(basePackage + "." + className);

Okay, jetzt sind unsere Klassen in unseren Händen. Darüber hinaus bleiben nur diejenigen hervorzuheben, die die Anmerkung haben @Component:

if(classObject.isAnnotationPresent(Component.class)){
    System.out.println("Component: " + classObject);
}

Führen Sie und überprüfen Sie. Die Konsole sollte ungefähr so ​​aussehen:

Component: class com.kciray.ProductService
Component: class com.kciray.PromotionsService

Jetzt müssen wir unsere Bohne erstellen. Es ist notwendig, so etwas zu tun new ProductService(), aber für jeden Behälter haben wir eine eigene Klasse. Reflection in Java bietet uns eine universelle Lösung (der Standardkonstruktor heißt):

Object instance = classObject.newInstance();//=new CustomClass()

Als nächstes müssen wir diese Bohne hineinlegen . Wählen Sie dazu den Bean-Namen (seine ID). In Java rufen wir Variablen wie Klassen auf (nur der erste Buchstabe ist klein geschrieben). Dieser Ansatz kann auch auf Beans angewendet werden, da Spring ein Java-Framework ist! Konvertieren Sie den Bin-Namen so, dass der erste Buchstabe klein ist, und fügen Sie ihn der Karte hinzu:Map singletons

String beanName = className.substring(0, 1).toLowerCase() + className.substring(1);
singletons.put(beanName, instance);

Stellen Sie jetzt sicher, dass alles funktioniert. Der Container muss Beans erstellen und diese müssen namentlich abgerufen werden. Bitte beachten Sie, dass der Name Ihrer Methode instantiate()und der Name der Methode classObject.newInstance();eine gemeinsame Wurzel haben. Darüber hinaus ist instantiate()es Teil des Bin-Lebenszyklus. In Java ist alles miteinander verbunden!

//Main.java
BeanFactory beanFactory = new BeanFactory();
beanFactory.instantiate("com.kciray");
ProductService productService = (ProductService) beanFactory.getBean("productService");
System.out.println(productService);//ProductService@612


Versuchen Sie auch, Annotation zu implementieren org.springframework.beans.factory.stereotype.Service. Es hat genau die gleiche Funktion wie @Component, wird aber anders aufgerufen. Der springende Punkt ist der Name - Sie demonstrieren, dass die Klasse ein Service und nicht nur eine Komponente ist. Das ist so etwas wie konzeptionelles Tippen. In der Frühjahrszertifizierung gab es eine Frage: "Welche Anmerkungen sind stereotyp?" (von den gelisteten). ” Stereotype Anmerkungen sind also diejenigen, die im Paket enthalten sind stereotype.

Füllen Sie die Eigenschaften


Schauen Sie sich das folgende Diagramm an, es zeigt den Beginn des Lebenszyklus der Bohne. Was wir vorher gemacht haben, ist Instantiate (Bohnen durchschaffen newInstance()). Die nächste Stufe ist die Kreuzinjektion von Bohnen (Abhängigkeitsinjektion, es ist auch die Inversion der Kontrolle (IoC)). Sie müssen die Eigenschaften der Bohnen durchgehen und verstehen, welche Eigenschaften Sie injizieren müssen. Wenn Sie jetzt anrufen productService.getPromotionsService(), erhalten Sie null, weil Abhängigkeit noch nicht hinzugefügt.



Erstellen Sie zunächst ein Paket org.springframework.beans.factory.annotationund fügen Sie eine Anmerkung hinzu @Autowired. Die Idee ist, Felder, die Abhängigkeiten sind, mit dieser Annotation zu kennzeichnen.

@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}

Fügen Sie es als Nächstes der Eigenschaft hinzu:

@Component
public class ProductService {
    @Autowired
    PromotionsService promotionsService;
    //...
}

Jetzt müssen wir uns beibringen BeanFactory, diese Annotationen zu finden und Abhängigkeiten von ihnen zu erzeugen. Fügen Sie dazu eine eigene Methode hinzu und rufen Sie diese auf von Main:

public class BeanFactory {
    //...
    public void populateProperties(){
        System.out.println("==populateProperties==");
    }
}

Als nächstes müssen wir nur alle unsere Fächer in der Karte singletonsdurchgehen und für jedes Fach alle seine Felder durchgehen (die Methode object.getClass().getDeclaredFields()gibt alle Felder zurück, einschließlich der privaten). Und überprüfen Sie, ob das Feld eine Anmerkung enthält @Autowired:

for (Object object : singletons.values()) {
    for (Field field : object.getClass().getDeclaredFields()) {
        if (field.isAnnotationPresent(Autowired.class)) {
        }
    }
}

Als nächstes müssen wir noch einmal alle Mülleimer durchgehen und ihren Typ sehen - plötzlich ist dies der Typ, den unser Mülleimer für sich selbst nehmen möchte. Ja, wir bekommen einen dreidimensionalen Zyklus!

for (Object dependency : singletons.values()) {
    if (dependency.getClass().equals(field.getType())) {
    }
}

Wenn wir die Sucht gefunden haben, müssen wir sie injizieren. Das erste, woran Sie vielleicht denken, ist, das Feld promotionsServicedirekt mit Reflexion aufzunehmen. Aber so funktioniert der Frühling nicht. Wenn das Feld einen Modifizierer hat private, müssen wir ihn zuerst als setzen public, dann unseren Wert schreiben und dann erneut setzen private(um die Integrität aufrechtzuerhalten). Klingt nach einer großen Krücke. Anstelle einer großen Krücke machen wir eine kleine Krücke (bilden wir den Namen des Setters und nennen ihn):

String setterName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);//setPromotionsService
System.out.println("Setter name = " + setterName);
Method setter = object.getClass().getMethod(setterName, dependency.getClass());
setter.invoke(object, dependency);

Führen Sie nun Ihr Projekt aus und stellen Sie sicher, dass beim Aufruf productService.getPromotionsService()stattdessen null unsere Bean zurückgegeben wird .

Was wir implementiert haben, ist Injektion nach Typ. Es gibt auch eine Spritze mit Namen (Zusammenfassung javax.annotation.Resource). Es unterscheidet sich darin, dass anstelle des Feldtyps der Name extrahiert wird und dementsprechend die Abhängigkeit von der Karte. Hier ist alles ähnlich, auch in etwas Einfacherem. Ich empfehle, dass Sie experimentieren und eine eigene Bohne erstellen, diese dann injizieren @Resourceund die Methode erweitern populateProperties().

Wir unterstützen Bohnen, die ihren Namen kennen




Es gibt Zeiten, in denen Sie seinen Namen in den Müll werfen müssen. Ein solches Bedürfnis entsteht nicht oft, weil Mülleimer sollten im Wesentlichen nichts voneinander wissen und wissen, dass es sich um Mülleimer handelt. In den ersten Versionen des Frühlings wurde angenommen, dass das Bean ein POJO (Plain Old Java Objec, das gute alte Java-Objekt) ist und die gesamte Konfiguration in XML-Dateien gerendert und von der Implementierung getrennt wird. Wir implementieren diese Funktionalität jedoch, da die Namensinjektion Teil des Lebenszyklus des Behälters ist.

Woher wissen wir, welche Bohne wissen will, wie er heißt und was er nicht will? Das erste, was mir in den Sinn kommt, ist eine neue Anmerkung wie@InjectNameund formen Sie es in Felder vom Typ String. Diese Lösung ist jedoch zu allgemein und ermöglicht es Ihnen, sich viele Male in den Fuß zu schießen (platzieren Sie diese Anmerkung auf Feldern ungeeigneter Typen (nicht auf Zeichenfolgen) oder versuchen Sie, einen Namen in mehrere Felder derselben Klasse einzufügen). Es gibt eine andere, genauere Lösung - die Erstellung einer speziellen Schnittstelle mit einer Setter-Methode. Alle Bins, die es implementieren, erhalten ihren Namen. Erstellen Sie eine Klasse BeanNameAwareim Paket org.springframework.beans.factory:

public interface BeanNameAware {
    void setBeanName(String name);
}

Lassen Sie es uns als Nächstes PromotionsServiceimplementieren:

@Component
public class PromotionsService implements BeanNameAware {
    private String beanName;
    @Override
    public void setBeanName(String name) {
        beanName = name;
    }
    public String getBeanName() {
        return beanName;
    }
}

Und schließlich fügen Sie der Bohnenfabrik eine neue Methode hinzu. Hier ist alles einfach - wir gehen unser bin-singleton durch, prüfen, ob das bin unsere Schnittstelle implementiert, und rufen den Setter auf:

public void injectBeanNames(){
    for (String name : singletons.keySet()) {
        Object bean = singletons.get(name);
        if(bean instanceof BeanNameAware){
            ((BeanNameAware) bean).setBeanName(name);
        }
    }
}

Führen Sie aus und stellen Sie sicher, dass alles funktioniert:

BeanFactory beanFactory = new BeanFactory();
beanFactory.instantiate("com.kciray");
beanFactory.populateProperties();
beanFactory.injectBeanNames();
//...
System.out.println("Bean name = " + promotionsService.getBeanName());

Es ist zu beachten, dass es im Frühjahr noch andere ähnliche Schnittstellen gibt. Ich empfehle, dass Sie die BeanFactoryAware- Schnittstelle unabhängig implementieren , damit Beans einen Link zur Bean Factory erhalten. Es wird auf ähnliche Weise implementiert.

Beans initialisieren




Stellen Sie sich vor, Sie müssen Code ausführen, nachdem die Abhängigkeiten eingefügt wurden (die Bin-Eigenschaften sind festgelegt). In einfachen Worten müssen wir dem Bin die Möglichkeit geben, sich selbst zu initialisieren. Alternativ können wir eine Schnittstelle erstellen InitializingBeanund die Methodensignatur darin einfügen void afterPropertiesSet(). Die Implementierung dieses Mechanismus entspricht genau der für die Benutzeroberfläche vorgestellten BeanNameAware, sodass sich die Lösung unter dem Spoiler befindet. Übe und mache es selbst in einer Minute:

Bean-Initialisierungslösung
//InitializingBean.java
package org.springframework.beans.factory;
public interface InitializingBean {
    void afterPropertiesSet();
}
//BeanFactory.java
public void initializeBeans(){
    for (Object bean : singletons.values()) {
        if(bean instanceof InitializingBean){
            ((InitializingBean) bean).afterPropertiesSet();
        }
    }
}
//Main.java
beanFactory.initializeBeans();



Fügen Sie Postprozessoren hinzu


Stellen Sie sich vor, Sie hätten die ersten Frühlingsentwickler. Ihr Framework wächst und ist bei Entwicklern sehr beliebt. Täglich werden Briefe an die E-Mail gesendet, in denen Sie aufgefordert werden, die eine oder andere nützliche Funktion hinzuzufügen. Wenn Sie für jede dieser Funktionen eine eigene Schnittstelle hinzufügen und diese im Lebenszyklus der Bean prüfen, wird sie (der Lebenszyklus) mit unnötigen Informationen überfüllt. Stattdessen können wir eine universelle Schnittstelle erstellen, mit der Sie eine Logik hinzufügen können (absolut jede, unabhängig davon, ob auf Annotation geprüft wird, der Bin durch einen anderen Bin ersetzt wird, einige spezielle Eigenschaften festgelegt werden usw.).

Überlegen wir uns, wozu diese Schnittstelle dient. Es muss eine Nachbearbeitung der Beans durchführen, daher kann es BeanPostProcessor genannt werden. Aber wir stehen vor einer schwierigen Frage: Wann sollte Logik befolgt werden? Immerhin können wir es vor der Initialisierung ausführen, aber wir können es danach ausführen. Für einige Aufgaben ist die erste Option besser, für andere - die zweite ... Was soll ich tun?

Wir können beide Optionen gleichzeitig aktivieren. Lassen Sie einen Postprozessor zwei Logiken und zwei Methoden ausführen. Eine wird vor der Initialisierung (vor der Methode afterPropertiesSet()) und die andere danach ausgeführt. Überlegen wir uns nun die Methoden selbst - welche Parameter sollten sie haben? Offensichtlich die Bohne selbst (Object bean) Der Einfachheit halber können Sie zusätzlich zu der Bean den Namen dieser Bean übergeben. Sie erinnern sich, dass der Behälter selbst seinen Namen nicht kennt. Und wir möchten nicht alle Beans zwingen, die BeanNameAware-Schnittstelle zu implementieren. Auf der Postprozessor-Ebene kann der Bean-Name jedoch sehr nützlich sein. Deshalb fügen wir es als zweiten Parameter hinzu.

Und was soll die Methode zurückgeben, wenn die Bean nachbearbeitet wird? Lassen Sie es uns den Behälter selbst zurückgeben. Dies gibt uns Superflexibilität, da Sie anstelle eines Bins ein Proxy-Objekt verschieben können, das seine Aufrufe umschließt (und die Sicherheit erhöht). Oder Sie können ein anderes Objekt vollständig zurückgeben, indem Sie die Ablage erneut erstellen. Entwicklern wird eine sehr große Handlungsfreiheit eingeräumt. Unten finden Sie die endgültige Version der gestalteten Benutzeroberfläche:

package org.springframework.beans.factory.config;
public interface BeanPostProcessor {
    Object postProcessBeforeInitialization(Object bean, String beanName);
    Object postProcessAfterInitialization(Object bean, String beanName);
}

Als nächstes müssen wir unserer Bohnenfabrik eine Liste einfacher Prozessoren hinzufügen und neue hinzufügen. Ja, dies ist eine reguläre ArrayList.

//BeanFactory.java
private List postProcessors = new ArrayList<>();
public void addPostProcessor(BeanPostProcessor postProcessor){
    postProcessors.add(postProcessor);
}

Jetzt ändern wir die Methode initializeBeans so, dass Postprozessoren berücksichtigt werden:

public void initializeBeans() {
    for (String name : singletons.keySet()) {
        Object bean = singletons.get(name);
        for (BeanPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessBeforeInitialization(bean, name);
        }
        if (bean instanceof InitializingBean) {
            ((InitializingBean) bean).afterPropertiesSet();
        }
        for (BeanPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessAfterInitialization(bean, name);
        }
    }
}

Erstellen wir einen kleinen Postprozessor, der die Aufrufe der Konsole einfach nachverfolgt und unserer Bean-Factory hinzufügt:

public class CustomPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        System.out.println("---CustomPostProcessor Before " + beanName);
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("---CustomPostProcessor After " + beanName);
        return bean;
    }
}

//Main.java
BeanFactory beanFactory = new BeanFactory();
beanFactory.addPostProcessor(new CustomPostProcessor());


Jetzt laufen und sicherstellen, dass alles funktioniert. Erstellen Sie als Schulungsaufgabe einen Postprozessor, der Anmerkungen bereitstellt @PostConstruct (javax.annotation.PostConstruct). Es bietet eine alternative Möglichkeit zur Initialisierung (verwurzelt in Java, nicht im Frühjahr). Im Wesentlichen platzieren Sie die Annotation auf einer Methode. Diese Methode wird VOR der Standardfrühlingsinitialisierung (InitializingBean) aufgerufen.

Achten Sie darauf, alle Annotationen und Pakete (auch javax.annotation) manuell zu erstellen, verbinden Sie die Abhängigkeiten nicht! Auf diese Weise können Sie den Unterschied zwischen dem Federkern und seinen Verlängerungen erkennen (Javax-Unterstützung) und sich daran erinnern. Dies wird auch in Zukunft einen Stil beibehalten.

Sie werden sich dafür interessieren, dass in einem echten Frühjahr die Annotation erfolgt@PostConstructDies ist genau das, was durch den Postprozessor CommonAnnotationBeanPostProcessor implementiert wird. Aber schauen Sie nicht dort, schreiben Sie Ihre Implementierung.

Zuletzt empfehle ich Ihnen void close(), der Klasse eine Methode hinzuzufügen BeanFactoryund zwei weitere Mechanismen zu erarbeiten. Die erste ist eine Anmerkung @PreDestroy (javax.annotation.PreDestroy), die für Methoden gedacht ist, die aufgerufen werden sollen, wenn der Container geschlossen wird. Die zweite ist die Schnittstelle org.springframework.beans.factory.DisposableBean, die die Methode enthält void destroy(). Alle Beans, die diese Schnittstelle ausführen, können sich selbst zerstören (z. B. Ressourcen freigeben).

@PreDestroy + DisposableBean
//DisposableBean.java
package org.springframework.beans.factory;
public interface DisposableBean {
    void destroy();
}
//PreDestroy.java
package javax.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface PreDestroy {
}
//DisposableBean.java
public void close() {
    for (Object bean : singletons.values()) {
        for (Method method : bean.getClass().getMethods()) {
            if (method.isAnnotationPresent(PreDestroy.class)) {
                try {
                    method.invoke(bean);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
        if (bean instanceof DisposableBean) {
            ((DisposableBean) bean).destroy();
        }
    }
}



Voller Bohnen-Lebenszyklus


Wir haben also den gesamten Lebenszyklus des Behälters in seiner modernen Form implementiert. Ich hoffe, dieser Ansatz hilft Ihnen dabei, sich daran zu erinnern.

Unser Lieblingskontext


Programmierer verwenden sehr oft den Begriff Kontext, aber nicht jeder versteht, was er wirklich bedeutet. Jetzt werden wir alles in Ordnung bringen. Wie ich am Anfang des Artikels bemerkt habe, ist Kontext genau wie die Implementierung des Containers BeanFactory. Zusätzlich zu den Grundfunktionen (DI) werden jedoch noch einige interessante Funktionen hinzugefügt. Eine dieser Funktionen ist das Senden und Verarbeiten von Ereignissen zwischen Bins.

Der Artikel erwies sich als zu groß und der Inhalt wurde abgeschnitten, sodass ich die Kontextinformationen unter den Spoiler stellte.

Wir erkennen den Kontext
Beginnen wir mit der Vorbereitung des Kontexts. Erstellen Sie ein Paket org.springframework.contextund eine Klasse ApplicationContextdarin. Lassen Sie es eine Instanz der Klasse enthalten BeanFactory. Alle Initialisierungsschritte werden im Konstruktor platziert, und wir fügen auch die Methodenumleitung hinzu close().

public class ApplicationContext {
    private BeanFactory beanFactory = new BeanFactory();
    public ApplicationContext(String basePackage) throws ReflectiveOperationException{
        System.out.println("******Context is under construction******");
        beanFactory.instantiate(basePackage);
        beanFactory.populateProperties();
        beanFactory.injectBeanNames();
        beanFactory.initializeBeans();
    }
    public void close(){
        beanFactory.close();
    }
}


Добавьте его в класс Main, запустите и убедитесь, что он работает:

ApplicationContext applicationContext = new ApplicationContext("com.kciray");
applicationContext.close();

Теперь давайте подумаем, как организовать события. Поскольку у нас уже есть метод close(), мы можем создать событие «Закрытие контекста» и перехватить его внутри какого-нибудь бина. Создайте простой класс, представляющий данное событие:

package org.springframework.context.event;
public class ContextClosedEvent {
} 

Теперь нам надо создать интерфейс ApplicationListener, который позволит бинам слушать наши события. Поскольку мы решили представлять события в виде классов, то имеет смысл типизировать этот интерфейс по классу события (ApplicationListener). Да, мы будем использовать Java-дженерики, и вы получите немножко опыта по работе с ними. Далее, вам нужно придумать название для метода, который будет обрабатывать событие:

package org.springframework.context;
public interface ApplicationListener{
    void onApplicationEvent(E event);
}

Nun zurück zum Unterricht ApplicationContext. In der Methode müssen wir close()alle unsere Behälter durchgehen und herausfinden, welche von ihnen Zuhörer von Ereignissen sind. Wenn der Bin instantiierbar ist , müssen Sie ihn aufrufen . Es scheint einfach und logisch, oder?ApplicationListeneronApplicationEvent(ContextClosedEvent)

public void close(){
    beanFactory.close();
    for(Object bean : beanFactory.getSingletons().values()) {
        if (bean instanceof ApplicationListener) {
        }
    }
}

Aber nein Hier ergibt sich die Schwierigkeit. Wir können keine Typprüfung durchführen . Dies liegt an der Besonderheit der Java-Implementierung. Beim Kompilieren kommt es zu einer sogenannten Typlöschung , bei der alles gelöscht wirdbean instanceof ApplicationListener ersetzt durch . Как же быть, что же делать? Как нам выловить бины, которые имплементят именно ApplicationListener, а не другие типы событий?

На самом деле, информация о типах сохраняется в метаданных класса, и очень даже успешно извлекается через рефлексию. Для начала, нам нужно получить список из интерфейсов, которые данный бин реализует, и отфильтровать среди них те, которые имеют параметры:

for (Type type: bean.getClass().getGenericInterfaces()){
    if(type instanceof ParameterizedType){
        ParameterizedType parameterizedType = (ParameterizedType) type;
    }
}

Далее, мы всего лишь извлекаем тип первого параметра, и убеждаемся, что он — наш класс события. Если это так, мы получаем наш метод через рефлексию и вызываем его:

Type firstParameter = parameterizedType.getActualTypeArguments()[0];
if(firstParameter.equals(ContextClosedEvent.class)){
    Method method = bean.getClass().getMethod("onApplicationEvent", ContextClosedEvent.class);
    method.invoke(bean, new ContextClosedEvent());
}

Пускай один из ваших классов реализует интерфейс ApplicationListener:

@Service
public class PromotionsService implements BeanNameAware, ApplicationListener {
    //...
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        System.out.println(">> ContextClosed EVENT");
    }
}

Далее, тестируете ваш контекст в Main и убеждаетесь, что он также работает, и событие отправляется:

//Main.java
void testContext() throws ReflectiveOperationException{
    ApplicationContext applicationContext = new ApplicationContext("com.kciray");
    applicationContext.close();
}


Заключение


Изначально я планировал данную статью для Baeldung на английском, но потом подумал, что аудитория хабры может положительно оценить данный подход к обучению. Если вам понравились мои идеи, обязательно поддержите статью. Если она наберёт рейтинг более 30, то обещаю продолжение. При написании статьи, я старался показать именно те знания Spring Core, которе используются наиболее часто, а также с опорой на Core Spring 5.0 Certification Study Guide. В будущем, с помощью таких туториалов можно покрыть всю сертификацию и сделать спринг более доступным для Java-разработчиков.

Update 10/05/2018


Мне постоянно приходят письма с вопросами «а когда продолжение, мы его ждём». Но вот времени совсем нету, и другие личные проекты в приоритете. Однако, если кому-то из вас действительно понравилась идея, вы можете изучить узкий раздел спринга и написать статью-продолжение. Если у вас нету аккаунта хабры, то я могу опубликовать статью от моего акка или помочь вам получить инвайт.

Распределение тем:
Spring Container — [имя пользователя]
Spring AOP — [имя пользователя]
Spring Web — [имя пользователя]
Spring Cloud — [имя пользователя]

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Тема для следующей статьи

  • 49%Spring Container, углубляемся ещё больше (@Bean, @Configuration, Context, Prototype, XML)81
  • 13.3%Spring AOP, пишем прокси своими руками (@Aspect, @Pointcut)22
  • 29%Spring Web, веб-сервер с нуля (@Contoller, @RequestMapping)48
  • 3%Spring WebFlux5
  • 5.4%Spring Cloud9

Jetzt auch beliebt: