Analysieren Sie die Prinzipien der Tomcat-Architektur für den Architekturentwurf

Analysieren Sie die Prinzipien der Tomcat-Architektur für den Architekturentwurf

1. Lernziele

1.1. Beherrschen Sie das Design und die Prinzipien der Tomcat-Architektur, um Ihre internen Fähigkeiten zu verbessern

Makroansicht

Als „ Http -Server + Servlet Container“ schirmt uns Tomcat vom Protokoll der Anwendungsschicht und den Netzwerkkommunikationsdetails ab und stellt uns standardmäßige Request und Response zur Verfügung. Die spezifische Geschäftslogik wird als Variationspunkt verwendet und die Implementierung bleibt uns überlassen. Wir verwenden Frameworks wie SpringMVC , müssen uns aber nie mit TCP Verbindungen, der Datenverarbeitung und Antworten Http Protokolls befassen. Da Tomcat dies alles für uns erledigt hat, müssen wir uns nur auf die spezifische Geschäftslogik jeder Anfrage konzentrieren.

Mikroskopische Ansicht

Tomcat isoliert außerdem intern die Änderungspunkte und die unveränderten Punkte und verwendet ein komponentenbasiertes Design, um einen hohen Grad an Anpassung (Kombinationsmuster) im „Matroschka-Stil“ zu erreichen. Das Lebenszyklusmanagement jeder Komponente weist einige gemeinsame Merkmale auf, die in Schnittstellen und abstrakte Klassen extrahiert werden, damit bestimmte Unterklassen die Änderungspunkte implementieren können. Dies ist das Entwurfsmuster der Vorlagenmethode.

Auch die heute beliebten Microservices folgen dieser Idee und teilen die monolithische Anwendung entsprechend ihrer Funktionen in „Microservices“ auf. Beim Aufteilen werden die Gemeinsamkeiten extrahiert, und diese Gemeinsamkeiten werden zu den zentralen Basisdiensten oder allgemeinen Bibliotheken. Dasselbe gilt für die Idee einer „mittleren Plattform“.

Entwurfsmuster sind oft ein leistungsstarkes Werkzeug zum Einkapseln von Änderungen. Der sinnvolle Einsatz von Entwurfsmustern kann unseren Code und unser Systemdesign elegant und übersichtlich gestalten.

Dies ist die „innere Stärke“, die durch das Erlernen hervorragender Open-Source-Software gewonnen werden kann. Sie wird niemals veraltet sein und die darin enthaltenen Designideen und -philosophien sind der grundlegende Weg. Lernen Sie aus ihren Designerfahrungen, verwenden Sie Designmuster sinnvoll, um Änderungen und Konstanten zu kapseln, und nutzen Sie die Erfahrungen aus ihrem Quellcode, um Ihre eigenen Systemdesignfähigkeiten zu verbessern.

1.2. Makroverständnis der Verbindung einer Anfrage mit Spring

Im Laufe unserer Arbeit sind wir bereits sehr gut mit der Java-Syntax vertraut, haben uns sogar einige Entwurfsmuster „gemerkt“ und viele Web-Frameworks verwendet, aber wir haben selten die Gelegenheit, sie in tatsächlichen Projekten zu verwenden. Das eigenständige Entwerfen eines Systems scheint nur die Implementierung eines Dienstes nach Bedarf zu sein. Ich habe anscheinend keinen umfassenden Überblick über die Java-Webentwicklung. Ich weiß beispielsweise nicht, wie die Browseranforderung mit dem Code in Spring verknüpft ist.

Um diesen Engpass zu überwinden, könnten Sie sich auf die Schultern von Riesen stellen, hervorragende Open-Source-Systeme kennenlernen und sehen, wie die Großen über diese Probleme denken.

Nachdem ich die Prinzipien von Tomcat studiert hatte, stellte ich fest, dass die Servlet -Technologie der Ursprung der Webentwicklung ist. Fast alle Java-Web-Frameworks (wie Spring) basieren auf Servlet -Kapselung. Die Spring-Anwendung selbst ist ein Servlet ( DispatchSevlet ), und Web-Container wie Tomcat und Jetty sind für das Laden und Ausführen Servlet verantwortlich. Wie in der Abbildung gezeigt:

1.3. Verbessern Sie Ihre Systemdesignfähigkeiten

Beim Erlernen von Tomcat habe ich auch festgestellt, dass ich viele fortgeschrittene Java-Technologien verwendet habe, wie etwa parallele Multithread-Programmierung in Java, Socket-Netzwerkprogrammierung und Reflexion. Früher kannte ich diese Technologien nur und hatte einige Fragen für Vorstellungsgespräche auswendig gelernt. Aber ich habe immer das Gefühl, dass es eine Lücke zwischen „Wissen“ und der Fähigkeit gibt, es zu nutzen. Durch das Studium des Tomcat-Quellcodes habe ich gelernt, in welchen Szenarien diese Technologien eingesetzt werden können.

Es gibt auch Systemdesignfunktionen wie schnittstellenorientierte Programmierung, komponentenbasierter Kombinationsmodus, Skelett-abstrakte Klasse, Starten und Stoppen mit einem Klick, Objektpooltechnologie und verschiedene Designmuster wie Vorlagenmethode, Beobachtermodus, Verantwortungskettenmodus usw. Später begann ich, sie nachzuahmen und diese Designideen in der tatsächlichen Arbeit anzuwenden.

2. Gesamtarchitekturentwurf

Heute werden wir die Designideen von Tomcat Schritt für Schritt analysieren. Einerseits können wir die Gesamtarchitektur von Tomcat kennenlernen, lernen, wie man ein komplexes System aus einer Makroperspektive entwirft, wie man Module der obersten Ebene entwirft und die Beziehung zwischen Modulen. Andererseits legt es auch die Grundlage für unser eingehendes Studium der Arbeitsprinzipien von Tomcat.

Tomcat-Startvorgang:

startup.sh -> catalina.sh start -> java -jar org.apache.catalina.startup.Bootstrap.main()

Tomcat implementiert zwei Kernfunktionen:

  • Verarbeitet Socket -Verbindungen und ist für die Konvertierung von Netzwerk-Byteströmen in Request und Response Objekte verantwortlich.
  • Laden und verwalten Sie Servlet und verarbeiten Sie spezifische Request -Anfragen.

Daher ist Tomcat mit zwei Kernkomponenten konzipiert: Connector und Container. Der Connector ist für die externe Kommunikation verantwortlich und der Container für die interne Verarbeitung

Um mehrere I/O -Modelle und Protokolle der Anwendungsschicht zu unterstützen, kann ein Tomcat Container an mehrere Konnektoren angeschlossen werden, so wie ein Raum mehrere Türen hat.

  • Server entspricht einer Tomcat-Instanz.
  • Standardmäßig gibt es nur einen Dienst, d. h. es gibt nur einen Dienst für eine Tomcat-Instanz.
  • Konnektor: Ein Dienst kann mehrere Konnektoren haben, die unterschiedliche Verbindungsprotokolle akzeptieren.
  • Container: Mehrere Konnektoren entsprechen einem Container, und der Container der obersten Ebene ist eigentlich die Engine.

Jede Komponente hat einen entsprechenden Lebenszyklus und muss gestartet werden, und ihre internen Unterkomponenten müssen ebenfalls gestartet werden. Beispielsweise enthält eine Tomcat-Instanz einen Dienst, und ein Dienst enthält mehrere Konnektoren und einen Container. Ein Container enthält mehrere Hosts, und innerhalb des Hosts können sich mehrere Contex-Container befinden, und ein Kontext kann auch mehrere Servlets enthalten. Daher verwendet Tomcat den Composite-Modus zum Verwalten der einzelnen Komponenten und behandelt jede Komponente als eine einzelne Gruppe. Insgesamt gleicht das Design der einzelnen Komponenten einer „russischen Puppe“.

2.1 Anschlüsse

Bevor ich über Konnektoren spreche, möchte ich zunächst die Grundlagen für die verschiedenen I/O Modelle und Anwendungsschichtprotokolle erläutern, die von Tomcat unterstützt werden.

Die von Tomcat unterstützten I/O -Modelle sind:

  • NIO : Nicht blockierende I/O , implementiert mithilfe der Java NIO Klassenbibliothek.
  • NIO2 : Asynchrone I/O , implementiert mit der neuesten NIO2 -Klassenbibliothek JDK 7 .
  • APR : Es wird mithilfe der Apache Portable Runtime implementiert und ist eine native Bibliothek, die in C/C++ geschrieben ist.

Die von Tomcat unterstützten Anwendungsschichtprotokolle sind:

  • HTTP/1.1 : Dies ist das Zugriffsprotokoll, das von den meisten Webanwendungen verwendet wird.
  • AJP : Wird für die Integration mit Webservern (wie Apache) verwendet.
  • HTTP/2 : HTTP 2.0 verbessert die Web-Leistung erheblich.

Somit kann ein Container an mehreren Konnektoren andocken. Der Connector schützt den Servlet Container vor den Unterschieden in Netzwerkprotokollen und I/O Modellen. Unabhängig davon, ob es sich um Http oder AJP handelt, wird im Container ein standardmäßiges ServletRequest Objekt abgerufen.

Die funktionalen Anforderungen an den verfeinerten Steckverbinder sind:

  • Abhörender Netzwerkport.
  • Akzeptieren Sie die Netzwerkverbindungsanfrage.
  • Lesen Sie den angeforderten Netzwerk-Bytestream.
  • Analysieren Sie den Bytestrom entsprechend dem spezifischen Anwendungsschichtprotokoll ( HTTP/AJP ) und generieren Sie ein einheitliches Tomcat Request Anforderungsobjekt.
  • Konvertieren Sie das Tomcat Request Anforderungsobjekt in eine standardmäßige ServletRequest .
  • Rufen Sie den Servlet -Container auf und erhalten Sie ServletResponse .
  • Konvertieren Sie ServletResponse in ein Tomcat Response Objekt.
  • Konvertiert Tomcat Response in einen Netzwerk-Bytestream. Schreibt den Antwort-Bytestream zurück an den Browser.

Nachdem die Anforderungen klar aufgelistet sind, müssen wir uns als nächstes die Frage stellen, welche Untermodule der Connector haben soll. Ein ausgezeichnetes modulares Design sollte eine hohe Kohäsion und geringe Kopplung berücksichtigen.

  • Hohe Kohäsion bedeutet, dass Funktionen mit hoher Relevanz möglichst konzentriert und nicht verstreut werden sollten.
  • Geringe Kopplung bedeutet, dass zwei verwandte Module die Abhängigkeiten und den Grad der Abhängigkeiten so weit wie möglich reduzieren und starke Abhängigkeiten zwischen den beiden Modulen vermeiden sollten.

Wir haben festgestellt, dass Konnektoren drei eng miteinander verknüpfte Funktionen erfüllen müssen:

  • Netzwerkkommunikation.
  • Protokollanalyse der Anwendungsschicht.
  • Konvertierung zwischen Tomcat Request/Response und ServletRequest/ServletResponse .

Daher haben die Entwickler von Tomcat drei Komponenten entwickelt, um diese drei Funktionen zu implementieren, nämlich EndPoint、Processor 和Adapter .

Das E/A-Modell der Netzwerkkommunikation ändert sich, und auch das Protokoll der Anwendungsschicht ändert sich, aber die allgemeine Verarbeitungslogik bleibt unverändert. EndPoint ist dafür verantwortlich, Processor Byte-Streams bereitzustellen, Processor ist dafür verantwortlich, Adapter Tomcat Request Anforderungsobjekte bereitzustellen, und Adapter ist dafür verantwortlich, dem Container ServletRequest Objekte bereitzustellen.

2.2 Kapselungsänderungen und Invarianz

Aus diesem Grund hat Tomcat eine Reihe abstrakter Basisklassen entwickelt, um diese stabilen Teile zu kapseln. Die abstrakte Basisklasse AbstractProtocol implementiert die Schnittstelle ProtocolHandler . Jedes Protokoll der Anwendungsschicht verfügt über seine eigene abstrakte Basisklasse, beispielsweise AbstractAjpProtocol und AbstractHttp11Protocol , und die Implementierungsklasse des spezifischen Protokolls erweitert die abstrakte Basisklasse der Protokollschicht.

Dies ist die Anwendung des Template-Method-Entwurfsmusters.

Zusammenfassend lässt sich sagen, dass die drei Kernkomponenten des Connectors, Endpoint , Processor und Adapter , jeweils drei Dinge tun. Endpoint und Processor werden zusammen in die Komponente ProtocolHandler abstrahiert. Ihre Beziehung wird in der folgenden Abbildung dargestellt.

ProtocolHandler-Komponente:

Es verwaltet hauptsächlich Netzwerkverbindungen und Protokolle der Anwendungsschicht. Es umfasst zwei wichtige Komponenten: EndPoint und Processor. Die beiden Komponenten werden kombiniert, um ProtocoHandler zu bilden. Lassen Sie mich ihre Arbeitsprinzipien im Detail vorstellen.

Endpunkt:

EndPoint ist der Kommunikationsendpunkt, also die Schnittstelle zur Kommunikationsüberwachung. Es handelt sich um einen speziellen Socket-Empfangs- und Sendeprozessor und eine Abstraktion der Transportschicht. Daher wird EndPoint zum Implementieren des Lesens und Schreibens TCP/IP -Protokolldaten verwendet und ruft im Wesentlichen die Socket-Schnittstelle des Betriebssystems auf.

EndPoint ist eine Schnittstelle und die entsprechende abstrakte Implementierungsklasse ist AbstractEndpoint . Die konkreten Unterklassen von AbstractEndpoint , wie NioEndpoint und Nio2Endpoint , haben zwei wichtige Unterkomponenten: Acceptor und SocketProcessor .

Der Acceptor wird verwendet, um die Socket-Verbindungsanforderung zu überwachen. SocketProcessor wird verwendet, um die Acceptor empfangene Socket -Anforderung zu verarbeiten. Es implementiert die Runnable Schnittstelle und ruft zur Verarbeitung die Protokollverarbeitungskomponente Processor der Anwendungsschicht in Run Methode auf. Um die Verarbeitungsleistung zu verbessern, wird SocketProcessor zur Ausführung an den Thread-Pool übermittelt.

Wir wissen, dass die Verwendung von Java-Multiplexern nichts weiter als zwei Schritte umfasst:

  • Erstellen Sie einen Seletor, registrieren Sie verschiedene für ihn interessante Ereignisse und rufen Sie dann die Auswahlmethode auf, um auf interessante Ereignisse zu warten.
  • Wenn etwas Interessantes passiert, z. B. Daten zum Lesen verfügbar sind, wird ein neuer Thread erstellt, um Daten aus dem Kanal zu lesen.

In Tomcat ist NioEndpoint die spezifische Implementierung von AbstractEndpoint . Obwohl es viele Komponenten darin gibt, besteht die Verarbeitungslogik immer noch aus den ersten beiden Schritten. Es enthält fünf Komponenten: LimitLatch , Acceptor , Poller , SocketProcessor und Executor , die zusammenarbeiten, um die Verarbeitung des gesamten TCP/IP-Protokolls zu implementieren.

LimitLatch ist ein Verbindungscontroller, der die maximale Anzahl von Verbindungen steuert. Der Standardwert im NIO-Modus beträgt 10.000. Wenn dieser Schwellenwert erreicht ist, wird die Verbindungsanforderung abgelehnt.

Acceptor läuft in einem separaten Thread. Er ruft die Methode accept in einer Endlosschleife auf, um neue Verbindungen zu empfangen. Sobald eine neue Verbindungsanforderung eintrifft, gibt accept ein Channel Objekt zurück, das dann zur Verarbeitung an Channel Poller übergeben wird.

Die Essenz von Poller ist ein Selector , der ebenfalls in einem separaten Thread ausgeführt wird. Poller verwaltet intern ein Channel Array. Es erkennt kontinuierlich den Datenbereitschaftsstatus Channel in einer Endlosschleife. Sobald Channel lesbar ist, generiert es ein SocketProcessor Taskobjekt und sendet es zur Verarbeitung an Executor .

SocketProcessor implementiert die Runnable-Schnittstelle, in getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL); der Code in der Run-Methode ruft den Handler ab und führt die Verarbeitung von SocketWrapper aus und ruft schließlich über den Socket den entsprechenden Protokollprozessor der Anwendungsschicht ab, d. h. er ruft die Komponente Http11Processor auf, um die Anforderung zu verarbeiten. Http11Processor liest die Daten des Kanals, um ein ServletRequest-Objekt zu generieren. Http11Processor liest den Kanal nicht direkt. Dies liegt daran, dass Tomcat ein synchrones, nicht blockierendes E/A-Modell und ein asynchrones E/A-Modell unterstützt. In der Java-API sind auch die entsprechenden Kanalklassen unterschiedlich, z. B. AsynchronousSocketChannel und SocketChannel. Um diese Unterschiede von Http11Processor abzuschirmen, hat Tomcat eine Wrapper-Klasse namens SocketWrapper entwickelt. Http11Processor ruft nur SocketWrapper-Methoden auf, um Daten zu lesen und zu schreiben.

Executor ist ein Threadpool, der für die Ausführung SocketProcessor Taskklasse verantwortlich ist. Die run Methode von SocketProcessor ruft Http11Processor auf, um die Anforderungsdaten zu lesen und zu analysieren. Wir wissen, dass Http11Processor eine Kapselung des Anwendungsschichtprotokolls ist. Es ruft den Container auf, um die Antwort zu erhalten, und schreibt die Antwort dann über Channel .

Der Arbeitsablauf ist wie folgt:

Prozessor:

Der Prozessor wird zur Implementierung des HTTP-Protokolls verwendet. Der Prozessor empfängt den Socket vom Endpunkt, liest den Byte-Stream, analysiert ihn in Tomcat-Anforderungs- und Antwortobjekte und übermittelt ihn zur Verarbeitung über den Adapter an den Container. Der Prozessor ist eine Abstraktion des Anwendungsschichtprotokolls.

Aus der Abbildung können wir ersehen, dass EndPoint, nachdem es die Socket-Verbindung empfangen hat, eine SocketProcessor-Aufgabe generiert und diese zur Verarbeitung an den Thread-Pool übermittelt. Die Run-Methode von SocketProcessor ruft die HttpProcessor-Komponente auf, um das Protokoll der Anwendungsschicht zu analysieren. Nachdem der Prozessor durch Analyse das Request-Objekt generiert hat, ruft er die Service-Methode des Adapters auf. Die Methode übergibt die Anforderung über den folgenden Code an den Container.

// Aufruf des Containers
connector.getService().getContainer().getPipeline().getFirst().invoke(Anfrage, Antwort);

Adapterkomponente:

Aufgrund unterschiedlicher Protokolle definiert Tomcat seine eigene Request -Klasse zum Speichern von Anforderungsinformationen, was tatsächlich das objektorientierte Denken widerspiegelt. Allerdings handelt es sich bei dieser Anfrage nicht um eine standardmäßige ServletRequest . Daher können Sie Tomcat nicht direkt verwenden, um die Anfrage als Parameter direkt im Container zu definieren.

Die Lösung der Tomcat-Designer besteht in der Einführung CoyoteAdapter , einer klassischen Anwendung des Adaptermusters. Der Connector ruft die Sevice Methode von CoyoteAdapter auf und übergibt das Tomcat Request Objekt. CoyoteAdapter ist dafür verantwortlich, Tomcat Request in ServletRequest umzuwandeln und dann die Service Methode des Containers aufzurufen.

2.3 Behälter

Der Connector ist für die externe Kommunikation und der Container für die interne Verarbeitung verantwortlich. Insbesondere übernimmt der Connector die Analyse der Socket-Kommunikation und des Anwendungsschichtprotokolls, um Servlet Anforderung zu erhalten, während der Container für die Verarbeitung Servlet Anforderung verantwortlich ist.

Container: Wie der Name schon sagt, wird er zum Aufbewahren von Dingen verwendet, daher wird der Tomcat-Container zum Laden Servlet verwendet.

Tomcat hat vier Container entwickelt: Engine , Host , Context und Wrapper . Server stellt die Tomcat-Instanz dar.

Es ist zu beachten, dass diese vier Container nicht in einer parallelen Beziehung stehen, sondern in einer Eltern-Kind-Beziehung, wie in der folgenden Abbildung dargestellt:

Sie fragen sich vielleicht, warum wir so viele Containerebenen entwerfen müssen? Erhöht das nicht die Komplexität? Der Grund hierfür liegt in der Tatsache, dass Tomcat eine Schichtenarchitektur verwendet, um den Servlet-Container sehr flexibel zu machen. Denn hier kommt es vor, dass ein Host mehrere Kontexte hat und ein Kontext auch mehrere Servlets enthält und jede Komponente ein einheitliches Lebenszyklusmanagement erfordert, sodass der kombinierte Modus diese Container entwirft

Wrapper stellt ein Servlet dar, Context stellt eine Webanwendung dar und eine Webanwendung kann mehrere Servlet haben; Host stellt einen virtuellen Host oder eine Site dar, ein Tomcat kann mit mehreren Sites (Host) konfiguriert werden; eine Site (Host) kann mehrere Webanwendungen bereitstellen; Engine stellt die Engine dar, die zum Verwalten mehrerer Sites (Host) verwendet wird und ein Service kann nur eine Engine haben.

Sie können die Tomcat-Konfigurationsdatei verwenden, um ein tieferes Verständnis der hierarchischen Beziehungen zu erlangen.

<Server port="8005" shutdown="SHUTDOWN"> // Komponente der obersten Ebene, kann mehrere Dienste enthalten, stellt eine Tomcat-Instanz dar<Service name="Catalina"> // Komponente der obersten Ebene, enthält eine Engine, mehrere Konnektoren<Connector port="8080" protocol="HTTP/1.1"
               VerbindungsTimeout="20000"
               UmleitungsPort="8443" />

    <!-- Definieren Sie einen AJP 1.3-Connector auf Port 8009 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> // Connector // Containerkomponente: Eine Engine verarbeitet alle Serviceanfragen, einschließlich mehrerer Hosts
    <Engine-Name="Catalina" Standardhost="localhost">
	  //Containerkomponente: verarbeitet Clientanforderungen unter dem angegebenen Host, der mehrere Kontexte enthalten kann
      <Hostname="localhost" appBase="webapps"
            unpackWARs="true" autoDeploy="true">
			//Containerkomponente: verarbeitet alle Clientanforderungen für eine bestimmte Context-Web-Anwendung <Context></Context>
      </Host>
    </Engine>
  </Dienst>
</Server>

Wie verwalte ich diese Container? Wir haben festgestellt, dass zwischen Containern eine Eltern-Kind-Beziehung besteht, die eine Baumstruktur bildet. Ist es möglich, sich das Kombinationsmuster im Entwurfsmuster vorzustellen?

Tomcat verwendet den kombinierten Modus, um diese Container zu verwalten. Die spezifische Implementierungsmethode besteht darin, dass alle Containerkomponenten die Container implementieren, sodass der zusammengesetzte Modus es Benutzern ermöglicht, einzelne Containerobjekte und zusammengesetzte Containerobjekte konsistent zu verwenden. Hier bezieht sich das einzelne Containerobjekt auf den Wrapper der untersten Ebene und das zusammengesetzte Containerobjekt auf den darüber liegenden Context , Host oder Engine . Container Schnittstelle ist wie folgt definiert:

öffentliche Schnittstelle Container erweitert Lebenszyklus {
    öffentliche void setName(String name);
    öffentlicher Container getParent();
    öffentliche void setParent(Container Container);
    öffentliche void addChild(Container-Unterelement);
    öffentliche void removeChild(Container-Unterelement);
    öffentlicher Container findChild(Stringname);
}

Wir haben Methoden wie getParent , SetParent , addChild und removeChild gesehen, die lediglich das erwähnte Kombinationsmuster überprüfen. Wir sehen auch, dass Container -Schnittstelle Lifecycle erweitert. Tomcat verwaltet den Lebenszyklus aller Containerkomponenten auf einheitliche Weise über Lifecycle . Alle Container werden über den kombinierten Modus verwaltet und Lifecycle wird erweitert, um die Lebenszyklusverwaltung jeder Komponente zu implementieren. Lifecycle umfasst hauptsächlich die Methoden init()、start()、stop() 和destroy() .

2.4. Der Prozess der Lokalisierung des Servlets

Wie gelangt eine Anfrage zu welchem Wrapper Servlet um sie zu verarbeiten? Die Antwort ist, dass Tomcat die Mapper-Komponente verwendet, um diese Aufgabe zu erfüllen.

Die Funktion Mapper Komponente besteht darin, URL zu einem Servlet zu lokalisieren. Ihr Funktionsprinzip ist: Mapper Komponente speichert die Konfigurationsinformationen der Webanwendung, die eigentlich die Zuordnungsbeziehung zwischen der Containerkomponente und dem Zugriffspfad darstellen, z. B. den im Host Container konfigurierten Domänennamen, Web im Context Container und den Servlet -Zuordnungspfad im Wrapper -Container. Sie können sich vorstellen, dass diese Konfigurationsinformationen eine mehrstufige Map sind.

Wenn eine Anforderung eingeht, kann Mapper Komponente ein Servlet lokalisieren, indem sie den Domänennamen und den Pfad in der Anforderungs-URL analysiert und dann in der gespeicherten Map sucht. Bitte beachten Sie, dass eine Anforderungs-URL letztendlich nur einen Wrapper -Container, also ein Servlet , findet.

Wenn ein Benutzer eine URL wie beispielsweise http://user.shopping.com:8080/order/buy in der Abbildung besucht, wie findet Tomcat diese URL zu einem Servlet?

1. Bestimmen Sie zunächst den Dienst und die Engine anhand des Protokolls und der Portnummer. Der Standard-HTTP-Connector von Tomcat lauscht auf Port 8080 und der Standard-AJP-Connector lauscht auf Port 8009. Die URL im obigen Beispiel greift auf Port 8080 zu, sodass die Anforderung vom HTTP-Connector empfangen wird. Da ein Connector zu einer Servicekomponente gehört, wird die Servicekomponente bestimmt. Wir wissen auch, dass eine Servicekomponente zusätzlich zu mehreren Konnektoren auch eine Containerkomponente hat, und zwar einen Engine-Container. Sobald also der Service bestimmt ist, ist auch die Engine bestimmt.

2. Wählen Sie den Host basierend auf dem Domänennamen aus. Nachdem Service und Engine ermittelt wurden, sucht die Mapper-Komponente über den Domänennamen in der URL nach dem entsprechenden Host-Container. Der Domänenname, auf den im Beispiel über die URL zugegriffen wird, lautet beispielsweise user.shopping.com , sodass der Mapper den Host2-Container findet.

3. Suchen Sie die Kontextkomponente anhand des URL-Pfads. Nachdem der Host ermittelt wurde, gleicht der Mapper den Pfad der entsprechenden Webanwendung entsprechend dem URL-Pfad ab. In diesem Beispiel lautet der aufgerufene Pfad beispielsweise /order, sodass der Kontextcontainer Context4 gefunden wird.

4. Suchen Sie den Wrapper (Servlet) basierend auf dem URL-Pfad. Nachdem der Kontext bestimmt wurde, findet der Mapper den spezifischen Wrapper und das Servlet gemäß dem in web.xml konfigurierten Servlet-Mapping-Pfad.

Der Adapter im Connector ruft die Servicemethode des Containers auf, um das Servlet auszuführen. Der erste Container, der die Anforderung empfängt, ist der Engine-Container. Nachdem der Engine-Container die Anforderung verarbeitet hat, übergibt er sie zur weiteren Verarbeitung an seinen untergeordneten Container Host und so weiter. Schließlich wird die Anforderung an den Wrapper-Container übergeben, und der Wrapper ruft das endgültige Servlet zur Verarbeitung auf. Wie wird dieser Aufrufvorgang implementiert? Die Antwort ist die Verwendung der Pipeline-Valve-Pipeline.

Pipeline-Valve ist ein Verantwortungskettenmodell. Das Verantwortungskettenmodell bedeutet, dass es im Prozess der Anforderungsverarbeitung viele Prozessoren gibt, die die Anforderung nacheinander verarbeiten. Jeder Prozessor ist für seine eigene entsprechende Verarbeitung verantwortlich. Nach der Verarbeitung wird der nächste Prozessor aufgerufen, um die Verarbeitung fortzusetzen. Valve stellt einen Verarbeitungspunkt dar (d. h. ein Verarbeitungsventil), sodass die invoke zum Verarbeiten der Anforderung verwendet wird.

öffentliche Schnittstelle Valve {
  öffentliches Valve getNext();
  öffentliche void setNext(Ventil Ventil);
  public void invoke (Anfrage Anfrage, Antwort Antwort)
}

Schauen Sie sich weiterhin die Pipeline-Schnittstelle an

öffentliche Schnittstelle Pipeline {
  öffentliche void addValve(Ventil Ventil);
  öffentliches Valve getBasic();
  öffentliche void setBasic(Ventil Ventil);
  öffentliches Valve getFirst();
}

Es gibt addValve -Methode in Pipeline . In der Pipeline wird eine Valve -Liste verwaltet. Valve können in Pipeline eingefügt werden, um bestimmte Verarbeitungsvorgänge für die Anforderung auszuführen. Wir haben auch festgestellt, dass es in Pipeline keine Aufrufmethode gibt, da das Auslösen der gesamten Aufrufkette von Valve abgeschlossen wird. Nachdem Valve seine eigene Verarbeitung abgeschlossen hat, ruft es getNext.invoke() auf, um den nächsten Valve-Aufruf auszulösen.

Tatsächlich hat jeder Container ein Pipeline-Objekt. Solange das erste Ventil dieser Pipeline ausgelöst wird, werden alle Ventile in Pipeline dieses Containers aufgerufen. Doch wie werden die Pipelines verschiedener Container in einer Kette ausgelöst? Beispielsweise muss die Pipeline in der Engine die Pipeline im untergeordneten Container-Host aufrufen.

Dies liegt daran, dass es in Pipeline auch eine getBasic -Methode gibt. Dieses BasicValve befindet sich am Ende der verknüpften Valve -Liste. Es ist ein wesentliches Valve in Pipeline und dafür verantwortlich, das erste Valve in der Pipeline des unteren Containers aufzurufen.

Der gesamte Vorgang wird durch CoyoteAdapter im Connector ausgelöst, der das erste Ventil der Engine aufruft:

@Überschreiben
öffentlicher void-Dienst(org.apache.coyote.Request req, org.apache.coyote.Response res) {
    // Anderen Code weglassen // Aufruf des Containers
    Connector.getService().getContainer().getPipeline().getFirst().aufrufen(
        Anfrage, Antwort);
    ...
}

Das letzte Ventil des Wrapper-Containers erstellt eine Filterkette und ruft doFilter() auf, die schließlich an Servlet service des Servlets übertragen wird.

Haben wir nicht vorher über Filter gesprochen? Es scheint ähnliche Funktionen zu haben. Was ist also der Unterschied zwischen Valve und Filter ? Die Unterschiede zwischen ihnen sind:

  • Valve ist ein privater Mechanismus von Tomcat und eng mit der Infrastruktur API von Tomcat gekoppelt. Servlet API ist ein öffentlicher Standard und alle Webcontainer, einschließlich Jetty, unterstützen den Filtermechanismus.
  • Ein weiterer wichtiger Unterschied besteht darin, dass Valve auf der Ebene des Web-Containers arbeitet und Anfragen aller Anwendungen abfängt, während Servlet Filter auf der Anwendungsebene arbeitet und nur alle Anfragen einer bestimmten Web Anwendung abfangen kann. Wenn Sie ein Interceptor für den gesamten Web Container sein möchten, müssen Sie dies über Valve tun.

Lebenszyklus

Zuvor haben wir gesehen, dass Container Container Lifecycle erbt. Wenn wir möchten, dass ein System Dienste für die Außenwelt bereitstellt, müssen wir diese Komponenten erstellen, zusammenstellen und starten. Wenn der Dienst stoppt, müssen wir auch Ressourcen freigeben und diese Komponenten zerstören. Es handelt sich also um einen dynamischen Prozess. Das heißt, Tomcat muss den Lebenszyklus dieser Komponenten dynamisch verwalten.

Wie kann die Erstellung, Initialisierung, der Start, Stopp und die Zerstörung von Komponenten einheitlich verwaltet werden? Wie kann man die Code-Logik deutlich machen? Wie können Komponenten einfach hinzugefügt oder entfernt werden? Wie kann sichergestellt werden, dass Komponenten ohne Auslassungen oder Duplikate gestartet und gestoppt werden?

Starten und Stoppen mit nur einem Tastendruck: LifeCycle-Schnittstelle

Beim Design geht es darum, die veränderlichen und unveränderlichen Punkte des Systems zu finden. Der unveränderliche Punkt hierbei ist, dass jede Komponente die Prozesse der Erstellung, Initialisierung und des Starts durchlaufen muss und diese Zustände und Zustandstransformationen unverändert bleiben. Die Änderung besteht darin, dass die Initialisierungsmethode jeder spezifischen Komponente, d. h. die Startmethode, unterschiedlich ist.

Daher abstrahiert Tomcat die invarianten Punkte in eine Schnittstelle, die sich auf den Lebenszyklus bezieht und LifeCycle genannt wird. Die LifeCycle-Schnittstelle definiert mehrere Methoden: init()、start()、stop() 和destroy() , und jede spezifische Komponente (d. h. Container) implementiert diese Methoden.

In init() Methode der übergeordneten Komponente müssen Sie die untergeordnete Komponente erstellen und init() Methode der untergeordneten Komponente aufrufen. In ähnlicher Weise muss start() -Methode der untergeordneten Komponente auch in start() -Methode der übergeordneten Komponente aufgerufen werden, sodass der Anrufer init() -Methode und start() -Methode jeder Komponente wahllos aufrufen kann. Dies ist die Verwendung des zusammengesetzten Modus. Solange init() und start() -Methoden der Komponente der obersten Ebene, d. H. Der Serverkomponente, aufgerufen werden, wird der gesamte Tomcat gestartet. Daher verwendet Tomcat einen kombinierten Modus zur Verwaltung von Containern. Die Container erben die LifeCycle-Schnittstelle, sodass der Lebenszyklus jedes Containers wie ein einzelnes Objekt mit einem Klick verwaltet werden kann und der gesamte Tomcat gestartet wird.

Skalierbarkeit: Lebenszyklus-Ereignisse

Betrachten wir ein weiteres Problem, nämlich die Skalierbarkeit des Systems. Weil die spezifische Implementierung der init() und start() Methoden jeder Komponente komplex und veränderbar ist. Beispielsweise ist es in der Startmethode des Host-Containers erforderlich, die Webanwendungen im Webapps-Verzeichnis zu scannen und den entsprechenden Kontextcontainer zu erstellen. Wenn in Zukunft neue Logik hinzugefügt werden muss, kann start() Methode dann direkt geändert werden? Dies würde das Offen-Geschlossen-Prinzip verletzen. Wie lässt sich dieses Problem also lösen? Das Open-Closed-Prinzip besagt, dass zur Erweiterung der Funktionalität eines Systems nicht die unmittelbare Veränderung vorhandener Klassen im System, sondern die Definition neuer Klassen möglich ist.

init() und start() Aufrufe einer Komponente werden durch den Statuswechsel ihrer übergeordneten Komponente ausgelöst. Die Initialisierung der Komponente der oberen Ebene löst die Initialisierung der untergeordneten Komponente aus, und der Start der Komponente der oberen Ebene löst den Start der untergeordneten Komponente aus. Daher definieren wir den Lebenszyklus einer Komponente als Status und betrachten den Statusübergang als Ereignis. Ereignisse haben Listener, in denen eine gewisse Logik implementiert werden kann, und Listener können auch einfach hinzugefügt und gelöscht werden. Dies ist ein typisches Beobachtermuster.

Nachfolgend sehen Sie die Definition der Lyfecycle -Schnittstelle:

Wiederverwendbarkeit: LifeCycleBase abstrakte Basisklasse

Sehen Sie sich das Entwurfsmuster „Abstrakte Vorlage“ noch einmal an.

Bei der Schnittstelle müssen wir Klassen verwenden, um die Schnittstelle zu implementieren. Im Allgemeinen gibt es mehr als eine Implementierungsklasse, und verschiedene Klassen verwenden bei der Implementierung der Schnittstelle häufig die gleiche Logik. Wenn jede Unterklasse sie implementieren muss, entsteht doppelter Code. Wie können Unterklassen diese Logik wiederverwenden? Tatsächlich geht es darum, eine Basisklasse zu definieren, um eine gemeinsame Logik zu implementieren, und diese dann jeder Unterklasse erben zu lassen, um den Zweck der Wiederverwendung zu erreichen.

Tomcat definiert eine Basisklasse LifeCycleBase zur Implementierung der LifeCycle-Schnittstelle und fügt der Basisklasse einige allgemeine Logik hinzu, wie etwa den Übergang und die Aufrechterhaltung von Lebenszuständen, das Auslösen von Lebensereignissen und das Hinzufügen und Löschen von Listenern usw., während die Unterklasse für die Implementierung ihrer eigenen Initialisierungs-, Start- und Stoppmethoden verantwortlich ist.

öffentliche abstrakte Klasse LifecycleBase implementiert Lifecycle{
    //Alle Beobachter privat halten final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();
    /**
     * Veranstaltung veröffentlichen *
     * @param Typ Ereignistyp
     * @param data Mit dem Ereignis verknüpfte Daten.
     */
    geschützt void fireLifecycleEvent(String-Typ, Objektdaten) {
        LifecycleEvent-Ereignis = neues LifecycleEvent(dieses, Typ, Daten);
        für (LifecycleListener-Listener: LifecycleListeners) {
            listener.lifecycleEvent(Ereignis);
        }
    }
    // Die Template-Methode definiert den gesamten Startvorgang und startet alle Container @Override
    öffentliches final synchronisiertes void init() wirft LifecycleException {
        //1. Statusprüfung if (!state.equals(LifecycleState.NEW)) {
            ungültiger Übergang (Lifecycle.BEFORE_INIT_EVENT);
        }

        versuchen {
            //2. Lösen Sie den Listener für das INITIALIZING-Ereignis aus: setStateInternal(LifecycleState.INITIALIZING, null, false);
            // 3. Rufen Sie die Initialisierungsmethode initInternal() der spezifischen Unterklasse auf;
            // 4. Lösen Sie den Listener für das INITIALIZED-Ereignis aus: setStateInternal(LifecycleState.INITIALIZED, null, false);
        } fangen (Wurfbares t) {
            ExceptionUtils.handleThrowable(t);
            setStateInternal(LifecycleState.FAILED, null, false);
            neue LifecycleException werfen(
                    sm.getString("lifecycleBase.initFail",toString()), t);
        }
    }
}

Um ein Klick zu erreichen, berücksichtigt Tomcat die Skalierbarkeit Containaer Wiederverwendbarkeit des Lifecycle und enthält das Objekt-orientierte Denk- und Entwurfsmuster. Das zusammengesetzte Muster, das Beobachtermuster, das Skelett -abstrakte Klassen- und Vorlagenmethode wurden jeweils verwendet.

Wenn Sie eine Reihe von Entitäten mit Eltern-Kind-Beziehungen beibehalten müssen, sollten Sie das zusammengesetzte Muster verwenden.

Das Observer-Muster klingt "High-End", aber tatsächlich bedeutet dies, dass bei einem Ereignis eine Reihe von Aktualisierungsvorgängen durchgeführt werden muss. Ein Mechanismus mit niedriger Kopplung, nicht störender Benachrichtigung und Aktualisierung wird implementiert.

Container erbt Lebenszyklus. .

3. Warum Tomcat den übergeordneten Delegationsmechanismus bricht

3.1

Wir wissen, dass JVM -Klasse auf der Grundlage Bootstrap übergeordneten Delegationsmechanismus das Laden an den eigenen übergeordneten Lader übernimmt. JDK bietet einen abstrakten ClassLoader , der drei Schlüsselmethoden definiert. Die externe Verwendung von loadClass(String name) 用于子類重寫打破雙親委派:loadClass(String name, boolean resolve)

öffentliche Klasse<?> loadClass(String name) löst ClassNotFoundException { aus
    gibt loadClass(Name, false) zurück;
}
geschützte Klasse<?> loadClass(String-Name, Boolesche Auflösung)
    löst ClassNotFoundException aus
{
    synchronisiert (getClassLoadingLock(name)) {
        // Herausfinden, ob die Klasse geladen wurde Class<?> c = findLoadedClass(name);
        // Wenn nicht geladen if (c == null) {
            // Delegieren Sie den übergeordneten Loader zum Laden und rufen Sie rekursiv auf, wenn (übergeordnet! = Null) {
                c = übergeordnet.loadClass (Name, false);
            } anders {
                // Wenn der übergeordnete Loader leer ist, finden Sie heraus, ob Bootstrap geladen wurde.
            }
            // Wenn es noch nicht geladen werden kann, rufen Sie Ihre eigene FindClass an, um zu laden, wenn (c == null) {
                c = Klasse finden(Name);
            }
        }
        if (Resolve) {
            Resolveclass (c);
        }
        Rückkehr c;
    }
}
geschützte Klasse<?> findClass(Stringname){
    //1. Suchen Sie gemäß dem übergebenen Klassennamen nach der Klassendatei in einem bestimmten Verzeichnis und lesen Sie die .class-Datei in den Speicher ein ...

        //2. Rufen Sie defineClass auf, um das Byte-Array in ein Class-Objekt umzuwandeln return defineClass(buf, off, len);
}

// Analysieren Sie das Bytecode-Array in ein Class-Objekt und implementieren Sie es mit nativen Methoden protected final Class<?> defineClass(byte[] b, int off, int len){
    ...
}

Es gibt 3 Klassenlader in JDK, und Sie können auch Klassenlader anpassen.

  • BootstrapClassLoader rt.jar ein in C JVM implementiertes Start -Klassen -Lader resources.jar
  • ExtClassLoader ist ein verlängerter Klassenlader, mit dem JAR -Pakete im Verzeichnis \jre\lib\ext geladen werden.
  • AppClassLoader ist der Systemklassenlader, der zum Laden von Klassen unter classpath verwendet wird.
  • Benutzerdefinierte Klassenlader, verwendet, um Klassen unter benutzerdefinierte Pfade zu laden.

Das Funktionsprinzip dieser Klassenlader ist dasselbe. Der Unterschied besteht darin, dass ihre Ladepfade unterschiedlich sind, d. h. die von der Methode findClass durchsuchten Pfade sind unterschiedlich. Der übergeordnete Delegationsmechanismus soll sicherstellen, dass eine Java-Klasse in der JVM eindeutig ist. Wenn Sie versehentlich eine Klasse mit demselben Namen wie eine JRE-Kernklasse schreiben, z. B. Object Klasse, kann der übergeordnete Delegationsmechanismus sicherstellen, dass die Object -Klasse in JRE geladen wird und nicht Object Klasse. Dies liegt daran Object Object AppClassLoader BootstrapClassLoader Objektklasse lädt, und delegieren Sie sie an ExtClassLoader , und ExtClassLoader wird an BootstrapClassLoader delegiert. Wir können höchstens ExtClassLoader bekommen, bitte beachten Sie hier.

3.2

Tomcat führt im Wesentlichen regelmäßige Aufgaben über einen Hintergrund -Thread aus, wodurch regelmäßig Änderungen in Klassendateien erfasst und Klassen neu geladen werden, wenn Änderungen festgestellt werden. Schauen wir uns an, wie ContainerBackgroundProcessor implementiert wird.

geschützte KlassencontainerbackgroundProcessor implementiert Runnable {

    @Überschreiben
    öffentliche Leere ausführen() {
        // Bitte beachten Sie, dass der hier übergebene Parameter die Instanz der "Host Class" -Prozessschilds (Containerbase.This) ist;
    }

    geschützte void ProcessChildren (Containerbehälter) {
        versuchen {
            // 1.
            Container.BackgroundProcess ();

            // 2.
            // Auf diese Weise werden alle Nachkommen des aktuellen Containers verarbeitet Container [] Kinder = Container.FindChildren ();
            für (int i = 0; i <children.length; i ++) {
            // Bitte beachten Sie hier, dass die Containerbasisklasse eine Variable namens Hintergrundprozessordelay hat.
                if (Kinder [i] .GetackgroundProcessordelay () <= 0) {
                    processCildren (Kinder [i]);
                }
            }
        } catch (Throwable t) {...}

Die heiße Belastung von Tomcat wird im Kontextcontainer implementiert, hauptsächlich durch Aufrufen der Reload -Methode des Kontextcontainers. Abgesehen von den Details aus makroischer Sicht sind die Hauptaufgaben wie folgt:

  • Stoppen Sie den Kontextbehälter und alle Kinderbehälter.
  • Stoppen Sie den Hörer und den Filter, der dem Kontextbehälter zugeordnet ist.
  • Stoppen Sie die Pipeline und verschiedene Ventile im Kontext.
  • Stoppen Sie und zerstören Sie den Klassenlader des Kontextes und die vom Klassenlader geladenen Klassendatei -Ressourcen.
  • Starten Sie den Kontextcontainer, in dem die in den vorherigen vier Schritten zerstörten Ressourcen nachgebaut werden.

In diesem Prozess spielen Klassenlader eine Schlüsselrolle. Ein Kontextbehälter entspricht einem Klassenlader. Während des Startprozesses erstellt der Kontext -Container einen neuen Klassenloader zum Laden neuer Klassendateien.

3.3

Tomcat's Custom Class Loader WebAppClassLoader bricht den übergeordneten Delegationsmechanismus. Die spezifische Implementierung besteht darin, zwei Methoden des ClassLoader umzuschreiben: findClass und loadClass .

FindClass -Methode

org.apache.catalina.loader.webAppclassloaderbase#findClass;

Um das Verständnis und das Lesen zu erleichtern, habe ich einige Details entfernt:

öffentliche Klasse<?> findClass(String name) löst ClassNotFoundException { aus
    ...

    Klasse<?> clazz = null;
    versuchen {
            //1. Suchen Sie zuerst im Web-Anwendungsverzeichnis nach der Klasse clazz = findClassInternal(name);
    } Fang (RuntimeException e) {
           werfen e;
       }

    wenn (clazz == null) {
    versuchen {
            //2. Wenn im lokalen Verzeichnis nicht gefunden, lassen Sie den übergeordneten Loader suchen clazz = super.findClass(name);
    } Fang (RuntimeException e) {
           werfen e;
       }

    //3. Wenn die übergeordnete Klasse nicht gefunden wird, werfen Sie ClassNotFoundException
    wenn (clazz == null) {
        wirf eine neue ClassNotFoundException(Name);
     }

    Rückgabeklazz;
}

1. Suchen Sie zuerst nach der Klasse, die im lokalen Verzeichnis der Webanwendung geladen wird.

2. Wenn nicht gefunden wird, wird es AppClassLoader die Suche an den übergeordneten Loader übergeben.

3. Wenn der übergeordnete Loader die Klasse auch nicht finden kann, wird eine ClassNotFound -Ausnahme ausgelöst.

Lastklasse -Methode

Schauen wir uns die Implementierung loadClass -Methode des Tomcat -Klassenladers an.

öffentliche Klasse<?> loadClass(Stringname, Boolesche Auflösung) wirft ClassNotFoundException {

    synchronisiert (getClassLoadingLock(name)) {

        Klasse<?> clazz = null;

        //1. Prüfen Sie zunächst im lokalen Cache, ob die Klasse geladen wurde. clazz = findLoadedClass0(name);
        if (clazz != null) {
            wenn (auflösen)
                Klasse auflösen(clazz);
            Rückgabeklazz;
        }

        // 2.
        if (clazz != null) {
            wenn (auflösen)
                Klasse auflösen(clazz);
            Rückgabeklazz;
        }

        // 3. Versuchen Sie, die Klasse mit dem Klassenlader ExtClassLoader zu laden. Warum?
        Ich habe versucht, den ClassLoader zu starten, aber ich habe ihn nicht gestartet.
        versuchen {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                wenn (auflösen)
                    Klasse auflösen(clazz);
                Rückgabeklazz;
            }
        } Fang (ClassNotFoundException e) {
            // Ignorieren
        }

        // 4. Versuche die Klasse im lokalen Verzeichnis zu suchen und zu laden try {
            clazz = Klasse finden(Name);
            if (clazz != null) {
                wenn (auflösen)
                    Klasse auflösen(clazz);
                Rückgabeklazz;
            }
        } Fang (ClassNotFoundException e) {
            // Ignorieren
        }

        // 5. Versuchen Sie, den Systemklassenlader (d. h. AppClassLoader) zum Laden zu verwenden try {
                clazz = Klasse.forName(Name, falsch, übergeordnetes Element);
                if (clazz != null) {
                    wenn (auflösen)
                        Klasse auflösen(clazz);
                    Rückgabeklazz;
                }
            } Fang (ClassNotFoundException e) {
                // Ignorieren
            }
       }

    //6. Das Laden der oben genannten Prozesse schlägt fehl und es wird eine Ausnahme ausgelöst: throw new ClassNotFoundException(name);
}

Es gibt sechs Hauptschritte:

1. Überprüfen Sie zuerst den lokalen Cache, ob die Klasse geladen wurde, dh, ob der Klassenlader von Tomcat diese Klasse geladen hat.

2. Wenn der Tomcat -Klasse -Loader diese Klasse nicht geladen hat, prüfen Sie, ob der Lader der Systemklasse sie geladen hat.

3. Wenn keiner von ihnen existiert, laden Sie es aus. Da Tomcat den übergeordneten Delegationsmechanismus brechen muss, wird die Objektklasse zuerst in der Webanwendung angepasst, wenn diese ExtClassLoader zuerst geladen wird. Auf diese Weise lädt der Klassenlader von Tomcat die Objektklasse unter BootstrapClassLoader ExtClassLoader BootstrapClassLoader , wodurch das Problem der Überschreibung der JRE -Kernklasse vermieden wird.

4. Wenn der ExtClassLoader -Loader nicht geladen wird, dh in JRE -Kernklasse gibt es keine solche Klasse, suchen und laden Sie ihn in das lokale Webanwendungsverzeichnis.

5. Wenn die Klasse im lokalen Verzeichnis nicht vorhanden ist, bedeutet dies, dass es sich nicht um eine Klasse handelt, die von der Webanwendung selbst definiert ist, und wird vom Systemklassenlader geladen. Bitte beachten Sie hierbei, dass die Übergabe der Web-Anwendung an den Systemklassenlader durch Class.forName erfolgt, da der Standardlader von Class.forName der Systemklassenlader ist.

6. Wenn alle oben genannten Ladeprozesse fehlschlagen, wird eine ClassNotFound -Ausnahme ausgelöst.

3.4.

Tomcat ist Servlet Servlet -Behälter für das Laden unserer Servlet -Klasse verantwortlich. Und Tomcat selbst ist auch ein Java -Programm, daher muss es seine eigenen Klassen und abhängigen JAR -Pakete laden. Lassen Sie uns zunächst über diese Fragen nachdenken:

Angenommen, wir führen zwei Webanwendungen in Tomcat aus, und in den beiden Servlet gibt es den gleichen Servlet , aber mit unterschiedlichen Funktionen.

2. Wenn zwei Webanwendungen von Spring Tomcat JVM Paket von Drittanbietern abhängen, wie Spring Spring

3. Wie der JVM müssen wir die Klassen von Tomcat selbst und die Klassen der Webanwendung isolieren.

1. WebAppClassloader

Die Lösung von Tomcat besteht darin, einen Klassenloader WebAppClassLoader anzupassen und eine Klassenladerinstanz für jede Webanwendung zu erstellen. Wir wissen, dass die Kontext -Containerkomponente einer Webanwendung entspricht, sodass jeder Context -Container für das Erstellen und Wartung einer WebAppClassLoader -Loader -Instanz verantwortlich ist. Die Begründung dahinter ist, dass Klassen, die von verschiedenen Laderinstanzen geladen werden, als unterschiedliche Klassen angesehen werden, auch wenn sie denselben Klassennamen haben. Dies ist gleichbedeutend mit der Erstellung eines gegenseitigen isolierten Java -Klassenräume in der java -virtuellen Maschine. Jede Webanwendung hat einen eigenen Klassenraum, und Webanwendungen werden über ihre eigenen Klassenlader voneinander isoliert.

2. SharedClassloader

Die wesentliche Anforderung ist, wie Sie Bibliotheksklassen zwischen zwei Webanwendungen freigeben und dieselbe Klasse nicht wiederholt laden. Im übergeordneten Delegationsmechanismus kann jeder untergeordnete Lader Klassen durch den übergeordneten Lader laden. Ist es also nicht ausreichend, die Klassen zu setzen, die unter den Ladeweg des übergeordneten Laders geteilt werden müssen?

Daher fügten die Designer von Tomcat einen Class Loader SharedClassLoader als übergeordneten Loader des WebAppClassLoader hinzu, der speziell zum Laden von Klassen verwendet wird, die zwischen Webanwendungen geteilt werden. Wenn WebAppClassLoader selbst keine Klasse lädt, wird der SharedClassLoader Lader SharedClassLoader zum Laden WebAppClassLoader Klasse geladen.

3. Catalinacklassloader

Wie isolieren Sie die eigenen Klassen von Tomcat aus den Klassen der Webanwendung?

Das Teilen kann durch eine Eltern-Kind-Beziehung erfolgen, während Isolation eine brüderliche Beziehung erfordert. CatalinaClassloader Geschwisterbeziehung bedeutet, dass zwei Klassenlader parallel sind und möglicherweise denselben Elternlader aufweisen.

Es gibt ein Problem mit diesem Design.

Die alte Methode besteht darin, einen weiteren CommonClassLoader als übergeordneten Loader von CatalinaClassloader und SharedClassLoader hinzuzufügen. Klassen, CatalinaClassLoader von CommonClassLoader geladen SharedClassLoader können

4. Zusammenfassung der allgemeinen Architektur -Designanalyse

Durch die vorherige Studie über die Gesamtarchitektur von Tomcat wissen wir, welche Kernkomponenten Tomcat und die Beziehung zwischen Komponenten haben. Und wie Tomcat eine HTTP -Anfrage umgeht. Lassen Sie es sich durch ein vereinfachtes Klassendiagramm aus dem Diagramm überprüfen.

4.1 Anschlüsse

Die Gesamtarchitektur von Tomcat besteht aus zwei Kernkomponenten: Stecker und Behälter. Der Anschluss ist für die externe Kommunikation verantwortlich und der Container ist für die interne Verarbeitung verantwortlich. EndPoint Anschluss Socket ProtocolHandler ProtocolHandler EndPoint , um Processor Unterschiede zwischen Kommunikationsprotokollen Proccesor I/O -Modellen zu verkapulieren. Der Stecker ruft den Container über den Adapter auf.

Durch das Studium der Gesamtarchitektur von Tomcat können wir einige grundlegende Ideen für die Gestaltung komplexer Systeme erhalten. Zunächst müssen wir die Anforderungen analysieren und die Submodule basierend auf dem Prinzip der hohen Kohäsion und der niedrigen Kopplung ermitteln.

4.2 Behälter

Der kombinierte Modus wird zum Verwalten des Containers verwendet, und die Start-Ereignisse werden über den Observer-Modus veröffentlicht, um Entkopplung und offene Prinzipien zu erreichen. Das skelett abstrakte Klassen- und Template -Methode abstrakte Änderungen und Konstanten, und die Änderungen werden den Unterklassen zur Implementierung überlassen, wodurch die Wiederverwendung von Code und eine flexible Expansion erreicht werden. Verwenden Sie den Ansatz der Verantwortungskette, um Anfragen wie Protokollierung zu behandeln.

4.3 Klassenlader

Die benutzerdefinierte Klassenlader WebAppClassLoader brechen den übergeordneten Delegationsmechanismus, um Webanwendungen zu isolieren. Um zu verhindern, dass die eigenen Klassen der Webanwendung die Kernklassen der JRE überschreiben, verwenden Sie den Extclassloader, um sie zu laden, was die übergeordnete Delegation durchbricht und sicher geladen werden kann.

5. Praktische Anwendungsszenarien

Das allgemeine Architekturdesign von Tomcat wird kurz analysiert, von [Anschlüssen] bis [Container], und die Designideen und Designmuster einiger Komponenten werden ausführlich erläutert. Der nächste Schritt ist, wie Sie das, was Sie gelernt haben, anwenden und aus dem eleganten Design lernen und auf die tatsächliche Arbeitsentwicklung anwenden. Lernen beginnt mit Nachahmung.

5.1.

Bei der Arbeit besteht die Forderung, dass Benutzer einige Informationen eingeben und die [industriellen und kommerziellen Informationen des Unternehmens], [gerichtliche Informationen], [China Registrierungsstatus] usw., eine oder mehrere Module wie unten gezeigt, überprüfen können, und es gibt einige gemeinsame Dinge zwischen Modulen, die von jedem Modul wiederverwendet werden müssen.

Dies ist wie eine Anfrage, die von mehreren Modulen verarbeitet wird. Daher können wir jedes Abfragemodul in ein Verarbeitungsventil abstrahieren und eine Liste auf diese Weise speichern.

Der spezifische Beispielcode lautet wie folgt:

Zunächst sind wir unser NetCheckDTO abstrahieren.

/**
 * Kette des Verantwortungsmusters: Behandeln Sie jedes Modulventil */
öffentliche Schnittstelle Valve {
    /**
     * Rufen Sie * @param netcheckdto an
     */
    void Invoke (netcheckdto netcheckdto);
}

Definieren Sie abstrakte Basisklassen, um Code wiederzuverwenden.

public abstract class AbstractCheckvalve implementiert Valve {
    öffentliche endgültige Analyse -Reportlogdo getLatesthistoryData (netcheckdto netcheckdto, netcheckDatatypeenum checkDatatypeenum) {
        // Historienaufzeichnungen abrufen, Codelogik weggelassen}

    // Erhalten Sie die Konfiguration der Verifizierungsdatenquelle im Publikum Final String getModulesource (String QuerySource, Moduleenum moduleenum) {
       // Code -Logik auslassen}
}

Definieren Sie die Geschäftslogik jedes Moduls, z. B. die Verarbeitung von [Baidu Negative News]

@Slf4j
@Service
öffentliche Klasse BaidunegativeValve erweitert AbstractCheckvalve {
    @Überschreiben
    public void Invoke (netcheckdto netcheckdto) {

    }
}

Der letzte Schritt besteht darin, die Module zu verwalten, die Benutzer überprüfen möchten, und wir speichern sie überlist. Wird verwendet, um das erforderliche Inspektionsmodul auszulösen

@Slf4j
@Service
öffentliche Klasse NetcheckService {
    // Injizieren Sie alle Ventile @autowired
    private Karte <String, Ventil> Valvemap;

    /**
     * Überprüfungsanforderung senden *
     * @Param netcheckdto
     */
    @ASync ("asyncexecutor")
    public void sendCheckRequest (netcheckdto netcheckdto) {
        // Modulventile zum Speichern von List für Kundenauswahl <ventils> Ventile = New ArrayList <> ();

        CheckModuleConfigDTO checkModuleConfig = netcheckdto.getCheckmoduleConfig ();
        // Fügen Sie das vom Benutzer ausgewählte Modul zur Ventilkette hinzu, wenn (checkModuleConfig.getBaidunegative ()) {
            valves.add (valvemap.get ("baidunegativeValve");
        }
        // einen Code weglassen .......
        if (collectionUtils.isempty (Ventile)) {
            log.info ("Das Netzwerkinspektionsmodul ist leer, es gibt keine Aufgabe zu prüfen");
            zurückkehren;
        }
        // Auslöser Verarbeitungsventile.foreach (Ventil -> Ventil.Invoke (netcheckdto));
    }
}

5.2 Template -Methodenmuster

Die Anforderung besteht darin, eine Analyse des Finanzberichts auf der Grundlage des vom Kunden eingegebenen Finanzberichts Excel -Daten oder Firmennamen durchzuführen.

Überprüfen Sie bei nicht gelisteten Produkten Excel ->, ob die Daten legal sind -> Berechnungen durchführen.

Listete Unternehmen: Stellen Sie fest, ob der Name vorhanden ist.

Die wichtige "Änderung" und "Unchange"

  • Was unverändert bleibt, besteht darin, dass der gesamte Prozess das Inspektionsprotokoll initialisieren, einen Bericht initialisieren, die Daten im Voraus überprüfen (wenn das börsennotierte Unternehmen die Überprüfung fehlschlägt, müssen auch E -Mail -Daten erstellt und gesendet werden), die Finanzberichtsdaten aus verschiedenen Quellen anpassen und anschließend die Berechnung auslösen.
  • Es hat sich geändert, dass die Überprüfungsregeln für gelistete und nicht gelistete Unternehmen unterschiedlich sind und die Daten zur Erlangung von Finanzberichtsdaten unterschiedlich sind.

Der gesamte Algorithmusprozess ist eine feste Vorlage, aber die spezifische Implementierung einiger Änderungen im Algorithmus muss in verschiedene Unterklassen verschoben werden.

öffentliche abstrakte Klasse Abstractanalysistemplate {
    /**
     * Senden Sie die Analyse -Analyse -Vorlagenmethode und definieren Sie den Skelettprozess * @Param ReportanalysisRequest
     * @zurückkehren
     */
    öffentliche Final FinancialanalyseResultdto Doprocess (FinancialReportanalysisRequest ReportanalyseRequest) {
        FinancialanalysisResultdto AnalysisDTO = neue FinanzanalyseResultdto ();
		// Zusammenfassung
        log.info ("prepevalidat validierungsergebnis = {}", prepevalidat);
        if (! prepevalidat) {
			// Abstract -Methode: Erstellen Sie die für Benachrichtigungs -E -Mails erforderlichen Daten BuildemailData (AnalysisDTO);
            log.info ("E -Mail -Informationen erstellen, data = {}", json.tojonstring (AnalysisDto));
            Return Analysisdto;
        }
        String reportno = finanziell_report_no_prefix + reportanalysisRequest.getUerid () + serialnumGenerator.getFixlenthSerialNumber ();
        // Analyse log initfinancialanalysislog (reportanalysisRequest, ReportNo) generieren;
		// Analyse -Aufzeichnung initanalysisreport (reportanalysisRequest, ReportNo) generieren;

        versuchen {
            // Abstract -Methode: Daten in Finanzbericht ziehen, verschiedene Unterklassen implementieren finanzialdatadto finanzialdata = pullfinancialdata (ReportanalysisRequest);
            log.info ("Finanzberichtsdaten abgeschlossen, Bereits zur Durchführung von Berechnungen");
            // Berechnungsindikatoren FinancialCalccontext.calc (ReportanalysisRequest, Financialdata, ReportNo);
			// das Analyse -Protokoll auf Success SuccessCalc (ReportNo) festlegen;
        } Fang (Ausnahme e) {
            log.Error ("Eine Ausnahme in der Berechnung der Finanzberichtsberechnung", e);
			// das Analysebotium auf failCalc (ReportNo) festlegen;
            werfen e;
        }
        Return Analysisdto;
    }
}

Erstellen Sie schließlich zwei Unterklassen, um die Vorlage zu erben und die abstrakte Methode zu implementieren. Dadurch entkoppelt sich die Verarbeitungslogik der aufgelisteten und nicht gelisteten Typen, während der Code wiederverwendet.

5.3 Strategiemuster

Die Anforderung besteht darin, eine Excel -Schnittstelle zu erstellen, die die allgemeinen Bankaussagen identifizieren kann. Jetzt analysieren wir den Index des Excel -Headers, in dem sich jedes erforderliche Feld befindet. Aber es gibt viele Situationen des Wasserflusses:

1. Eine soll alle Standardfelder einbeziehen.

2. Die Einschüsse des Einkommens und der Ausgaben sind in derselben Spalte und Einkommen und Ausgaben werden durch positive und negative Zahlen unterschieden.

3. Einkommen und Ausgaben befinden sich in derselben Spalte, wobei ein Transaktionstypfeld sie unterscheidet.

4. Sonderbehandlung für spezielle Banken.

Das heißt, wir müssen den entsprechenden Verarbeitungslogikalgorithmus basierend auf dem entsprechenden Einweis der Analyse if else . Am Ende kann die Codekomplexität "lang, hässlich und schwer zu pflegen" sein.

Zu diesem Zeitpunkt können wir den Strategiemodus verwenden, um die Pipelines verschiedener Vorlagen mit verschiedenen Prozessoren zu verarbeiten und den entsprechenden Strategiealgorithmus gemäß der Vorlage zu finden. Selbst wenn wir in Zukunft einen anderen Typ hinzufügen, müssen wir nur einen neuen Prozessor hinzufügen, der einen hohen Zusammenhalt, eine niedrige Kopplung aufweist und skalierbar ist.

Definieren Sie die Prozessorschnittstelle und verwenden Sie verschiedene Prozessoren, um die Verarbeitungslogik zu implementieren. Geben Sie alle Prozessoren in data_processor_map von BankFlowDataHandler ein und nehmen Sie den Verarbeitungsfluss der vorhandenen Prozessoren gemäß verschiedenen Szenarien heraus.

öffentliche Schnittstelle DataProzessor {
    /**
     * Verarbeitungsflussdaten * @param bankflowtemPlatedo Flow Indexdaten * @param Zeile
     * @zurückkehren
     */
    BanktransactionFlowdo doprocess (bankflowtemplatedo bankflowtemplatedo, list <string> row);

    /**
     * Ob die Vorlage verarbeitet werden kann
     */
    boolean issupport (bankflowtemplatedo bankflowtemplatedo);
}

// Prozessorkontext @Service
@Slf4j
öffentliche Klasse Bankflowdatacontext {
    // Injizieren Sie alle Prozessoren in die Karte @autowired
    private Liste <Dataprocessor> Prozessoren;

    // Ermitteln Sie den entsprechenden Prozessor, um die Pipeline Public void Process () {zu verarbeiten
         DataProcessor processor = getProcessor (bankflowtemplatedo);
      	 für (DataProcessor -Prozessor: Prozessoren) {
           if (processor.issupport (bankflowtemplatedo)) {
             // Zeile ist eine Reihe von Flussdatenprozessor.doprocess (Bankflowtemplatedo, Row);
             brechen;
           }
         }

    }


}

Definieren Sie den Standardprozessor, um normale Vorlagen zu DataProcessor .

/**
 *Standardprozessor: Vorlage der Standard -Pipeline -Vorlage*
 */
@Component ("defaultDataprocessor")
@Slf4j
Public Class DefaultDataprocessor implementiert Dataprocessor {

    @Überschreiben
    public banktransactionflowdo doprocess (bankflowtemplatedo bankflowtemplatedo) {
        // Die Verarbeitungslogik -Details weglassen. RECHTE BANKTRANSACTEFLOWDO;
    }

    @Überschreiben
    öffentliche String -Strategie (Bankflowtemplatedo bankflowtemplatedo) {
      // lasse das Urteil aus, ob die Pipeline boolean isdefault = true analysiert werden kann;

      ISDEFAULT zurückgeben;
    }
}

Durch das Strategiemuster weisen wir verschiedenen Verarbeitungsklassen unterschiedliche Verarbeitungslogiken zu, die vollständig entkoppelt und leicht zu erweitern sind.

Verwenden Sie die eingebettete Tomcat-Methode, um den Quellcode zu debuggen: GitHub: https://github.com/uniquedong/tomcat-embedded

Das oben genannte Inhalt der Analyse von Tomcat -Architekturprinzipien auf architektonisches Design.

Das könnte Sie auch interessieren:
  • Eine Lösung für das Problem des ungültigen Zeichenkodierungsfilters basierend auf Tomcat8
  • Detaillierte Erläuterung der Tomcat-Kernkomponenten und der Anwendungsarchitektur
  • Lösung für Tomcat zum externen Speichern von Konfigurationsdateien
  • Tomcat-Quellcodeanalyse und -Verarbeitung
  • Detaillierte Erläuterung der häufig verwendeten Filter von Tomcat

<<:  CSS zur Erzielung einer kompatiblen Textausrichtung in verschiedenen Browsern

>>:  HTML-Code für feste Titelspalte und spezifische Implementierungscode für die Titelkopftabelle

Artikel empfehlen

Vergleich von CSS-Schatteneffekten: Schlagschatten und Box-Schatten

Drop-Shadow und Box-Shadow sind beide CSS-Eigensc...

Vergleich zwischen Redis und Memcache und Auswahlmöglichkeiten

Ich verwende Redis seit Kurzem und finde es recht...

HTML-Grundlagen HTML-Struktur

Was ist eine HTML-Datei? HTML steht für Hyper Text...

Erfahren Sie schnell, wie Sie mit der Vuex-Statusverwaltung in Vue3.0 beginnen

Vuex ist ein speziell für Vue.js-Anwendungen entw...

Detaillierte Erklärung zum effizienten MySQL-Paging

Vorwort Normalerweise wird für MySQL-Abfragen mit...

So verwenden Sie React-Slots

Inhaltsverzeichnis brauchen Kernidee Zwei Möglich...

Ursachen und Lösungen für die Front-End-Ausnahme 502 Bad Gateway

Inhaltsverzeichnis 502 Bad Gateway Fehlerbildung ...

Detaillierte Erläuterung der CSS BEM-Schreibstandards

BEM ist ein komponentenbasierter Ansatz zur Weben...

MySQL-Optimierung: Cache-Optimierung (Fortsetzung)

In MySQL gibt es überall Caches. Wenn ich den Que...

MySQL-Export ganzer oder einzelner Tabellendaten

Exportieren einer einzelnen Tabelle mysqldump -u ...