Neon [1] ist ein ultrakompakter, modular aufgebauter HTTP-Server auf der Grundlage des Java-Moduls jdk.httpserver
zum Einbetten in Apps und Microservices.
Übersicht
Eine Anwendung kann mit Hilfe von Neon um einen eingebetteten HTTP Server erweitert werden und so selbst auf Anfragen über HTTP reagieren. Sie wird damit über das Netz steuerbar und kann eine Bedienoberfläche liefern, die in einem Web Browser funktioniert. Dies erfordert nur wenige Schritte:
-
Einen oder mehrere Klassen des Typs Actor bauen
-
Eine Serverbeschreibungsdatei erstellen
-
Neon aus der Anwendung heraus starten
In diesem Dokument sind diese Schritte im Detail beschrieben.
Neon starten
Neon lässt sich wie folgt als Teil einer Anwendung starten.
import de.uhilger.neon.Factory;
public class App() {
public static void main(String[] args) {
App app = new App();
}
public App() {
Factory f = new Factory();
try {
NeonDescriptor d = f.readDescriptor(new File("server.json"));
f.runInstance(d);
} catch (Exception ex) {
Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
Im obigen Code-Beispiel wird ein Objekt der Klasse de.uhilger.neon.Factory
erzeugt und eine Beschreibungsdatei namens server.json
gelesen. Anschließend wird die Instanz von Neon mit dem Aufruf der Methode runInstance
der Factory gestartet.
Eine so gestartete Anwendung beinhaltet einen HTTP Server, der über die in der Serverbeschreibung konfigurierten Ports auf HTTP Anfragen lauscht und diese beantwortet. Der Server und mithin die Anwendung läuft wie ein Dienst ohne zeitliche Begrenzung und kann wie in Neon stoppen beschrieben gestoppt werden.
Serverbeschreibung
Die Datei server.json
beschreibt die Elemente, die Neon als Server bereitstellen soll. Im einfachsten Fall ist dies ein einzelner HTTP-Kontext, wie er im folgenden Beispiel beschrieben ist.
{
"contentType": "text/json",
"contentId": "neon-descriptor-0.1.0",
"contentDescription":"Neon Descriptor Version 0.1.0",
"instanceName": "Mein Server",
"instanceDescription": "Beispiel-Serverinstanz",
"actorPackages": [
"com.example.my.app",
"com.example.my.other.library"
],
"server": [
{
"name": "mein-server",
"port":7000,
"contexts": [
{
"className": "de.uhilger.neon.Handler",
"sharedHandler": "false",
"contextPath": "/meine/app",
"attributes": {
"contextName": "srv-ctx"
}
}
]
}
]
}
Nachfolgend sind die Elemente einer Beschreibungsdatei im Detail aufgeführt.
Element actorPackages
Eine Liste von Package-Namen, die nach Klassen des Typs de.uhilger.neon.Actor
durchsucht werden. Die so vorgefundenen Actor-Klassen liefern den Code, der über HTTP-Aufrufe zugänglich wird. Eine Angabe von Packages auf diesem Wege verkürzt den Vorgang, weil nicht der gesamte Classpath nach Actor-Klassen durchsucht werden muss. Innerhalb der angegebenen Packages erfolgt die Suche einschließlich aller Unterpackages (rekursiv).
Element contexts
Ein oder mehrere Elemente, die von Neon als Objekte des Typs HttpContext
angelegt werden. Jeder Kontext kann über die Kombination aus Port und contextPath aufgerufen werden und hat die folgenden Eigenschaften.
- className
-
Mit dem Attribut
className
wird die Klasse angegeben, die für diesen Kontext als HttpHandler dient. Die Klassede.uhilger.neon.Handler
kann für alle Kontexte als generischer Handler dienen. Sie erzeugt zur Laufzeit dynamisch Objekte des TypsActor
aus den Klassen, die inactorPackages
gefunden werden. - sharedHandler
-
Über
sharedHandler
kann ein einzelnes Handler-Objekt für mehrere Kontexte verwendet werden. - contextPath
-
Jeder Kontext ist an einen bestimmten Pfad gebunden, der mit dem Attribut
contextPath
angegeben wird. - contextName
-
Das Attribut
contextName
gibt den Namen an, über den die Bindung von Klassen des TypsActor
erfolgt. Weitere Attribute werden aus der Beschreibungsdatei an den Kontext übergeben und können dort der Anwendung als Parameter dienen.
Actor-Klassen
Neon macht Java-Code selbsttätig über HTTP-Aufrufe zugänglich. Voraussetzung dafür ist, dass solcher Code mit den Annotationen Actor
und Action
versehen wird. Actor-Objekte werden erst beim Aufruf erzeugt. Für jeden HTTP-Aufruf erzeugt Neon ein Actor-Objekt, das mit Ende der Ausführung der mit Action
annotierten Methode wieder freigegeben wird.
Actor
package com.example.my.app.actors;
import de.uhilger.neon.Actor;
import de.uhilger.neon.Action;
@Actor(name="beliebiger-name")
public class Example {
@Action(handler={"srv-ctx"}, route="/eine/route/zum/beispiel", type=Action.Type.GET, handlesResponse = false)
public void run() {
// hier kann beliebiger Code ausgefuehrt werden
}
}
Annotation Actor
Durch die Annotation Actor
wird eine Klasse als vom Typ Actor
erkennbar. Während der automatischen Erzeugung eines eingebetteten Servers können damit Methoden gefunden werden, die über den Server aufrufbar sein sollen. In Klassen, die als Actor
annotiert sind, werden Methoden gesucht, die mit der Annotation Action
versehen sind.
Annotation Action
Die Annotation Action
kennzeichnet Methoden einer Klasse, die über HTTP aufrufbar sein sollen. Die folgenden Attribute kommen dabei zur Anwendung.
- handler
-
Das Attribut
handler={"name-laut-serverbeschreibung"}
der AnnotationAction
verbindet die Methode einerActor
-Klasse mit der Konfiguration eines HTTP-Kontexts aus der Serverbeschreibung. Das AttributcontextPath
des betreffenden Kontexts wird zur Laufzeit zur Bildung des URL zusammengesetzt, über den die Methode aufrufbar sein soll. Im Beispiel würde sich so der URLhttp://localhost:7000/meine/app
ergeben. Das Attributhandler
erlaubt es, mehrere HTTP-Kontexte anzugeben und damit die Methode über unterschiedliche Kombinationen aus Port und Kontext-Pfad ausführen zu können. Die Angabe mehrerer Kontexte erfolgt z.B. mithandler={"ctx-1", "ctx-2", "ctx-3"}
. - route
-
Das Attribut
route
nennt die Route, an die der Aufruf der Methode geknüpft ist. Die im Beispiel genannte Route wird zur Laufzeit zur Bildung des URLhttp://localhost:7000/meine/app/eine/route/zum/beispiel
herangezogen. Über diesen URL kann der Code der Methoderun
des Beispiels ausgeführt werden. - type
-
Mit dem Attribut
type
wird angegeben, mit welcher HTTP-Methode die betreffende Methode aufrufbar sein soll,GET
,PUT
,POST
oderDELETE
. - handlesResponse
-
Mit
handlesResponse
wird dem Handler mitgeteilt, ob die Action sich selbst um das Senden einer Antwort kümmert. Ein Eintrag vonfalse
veranlasst den Handler, eine Antwort zu erzeugen.
Der Name der Methode muss nicht run
wie im Beispiel sondern kann beliebig lauten.
Mit der Einschränkung auf den Typ String
bei Parametern und Rückgabewert bleibt die Verwendung allgemein und zugleich einfach. Die Kommunikation über HTTP erfolgt via Text, so dass komplexere Typen ohnehin Serialisiert und Deserialisiert werden müssen, beispielsweise mit Hilfe von Gson
[4].
Im Folgenden eine Übersicht der Vorgaben an eine Verwendung der Annotation Action
.
Rückgabewert
Mit der Annotation Action
angebundene Methoden müssen keinen Rückgabewert liefern. Wenn aber ein Rückgabewert geliefert wird, muss dieser vom Typ String
sein.
Action
-Methode mit Rückgabewert@Actor(name="beliebiger-name")
public class Example {
@Action(handler={"srv-ctx"}, route="/eine/route/zum/beispiel", type=Action.Type.GET, handlesResponse = false)
public String run() {
try {
// hier kann beliebiger Code ausgefuehrt werden
return "Die Methode wurde erfolgreich ausgefuehrt.";
} catch(Exception ex) {
// hier den Fehler behandeln
return "Es ist ein Fehler aufgetreten: " + ex.getMessage();
}
}
}
Parameter
Parameter müssen vom Typ String
sein. Zudem müssen die Parameter der HTTP-Anfrage dieselben Namen haben wie die Parameter der Methode, an die sie gebunden sind.
Action
-Methode mit Parametern@Actor(name="beliebiger-name")
public class Example {
@Action(handler={"srv-ctx"}, route="/eine/route/zum/beispiel", type=Action.Type.GET, handlesResponse = false)
public String run(String param1, String param2) {
try {
// hier kann beliebiger Code ausgefuehrt werden
return "param1 lautet: " + param1 + ", param2 lautet: " + param2;
} catch(Exception ex) {
// hier den Fehler behandeln
return "Es ist ein Fehler aufgetreten: " + ex.getMessage();
}
}
}
In obigem Beispiel muss der URL im Falle eines HTTP GET
die Query ?param2=Inhalt 2¶m1=Inhalt 1
enthalten oder bei PUT
, POST
oder DELETE
diese Angaben im Body des HTTP-Aufrufes mitgeben. Eine weitere Möglichkeit ist es, Teile der Route als Parameter anzulegen, wie das folgende Beispiel zeigt.
Action
-Methode mit Parametern als Teil der Route@Actor(name="beliebiger-name")
public class Example {
@Action(handler={"srv-ctx"}, route="/eine/route/zum/beispiel/{param2}/{param1}", type=Action.Type.GET, handlesResponse = false)
public String run(String param1, String param2) {
try {
// hier kann beliebiger Code ausgefuehrt werden
return "param1 lautet: " + param1 + ", param2 lautet: " + param2;
} catch(Exception ex) {
// hier den Fehler behandeln
return "Es ist ein Fehler aufgetreten: " + ex.getMessage();
}
}
}
Auch hier müssen die Namen der Parameter im URL mit den Namen der Parameter der Methode übereinstimmen. Die Parameter müssen zudem am Ende der Route stehen, eine Route wie etwa /eine/route/{param2}/zum/{param1}/beispiel
wird nicht verarbeitet.
Parameter HttpExchange
Wenn einer der Parameter als HttpExchange
deklariert ist, übergibt Neon das entsprechende Objekt zur Laufzeit automatisch.
Action
-Methode, die ein Objekt der Klasse HttpExchange
benötigt@Actor(name="beliebiger-name")
public class Example {
@Action(handler={"srv-ctx"}, route="/eine/route/zum/beispiel", type=Action.Type.GET, handlesResponse = false)
public String run(HttpExchange exchange, String param1, String param2) {
try {
// hier kann beliebiger Code ausgefuehrt werden
String contextPfad = exchange.getContext().getPath();
return "pfad: " + contextPfad + ", param1 lautet: " + parameter1 + ", param2 lautet: " + parameter2;
} catch(Exception ex) {
// hier den Fehler behandeln
return "Es ist ein Fehler aufgetreten: " + ex.getMessage();
}
}
}
File Server
Mit der Klasse de.uhilger.neon.FileServer
beinhaltet Neon eine Möglichkeit zur Auslieferung der Inhalte von Dateien und damit die Funktion eines klassischen Webservers. Der FileServer
liefert Dateiinhalte auf einmal oder als Stream, abhängig davon, ob die HTTP-Anfrage einen Range-Header enthält [5].
Die Beschreibungsdatei des Servers muss hierzu einen HTTP-Kontext eröffnen, über den Dateiinhalte ausgeliefert werden. Der Klasse FileServer
wird dabei über das Attribut fileBase
der Ablageort für Dateien mitgeteilt.
{
"contentType": "text/json",
"contentId": "neon-descriptor-0.1.0",
"contentDescription":"Neon Descriptor Version 0.1.0",
"instanceName": "File-Server",
"instanceDescription": "Ein einfacher File Server",
"actorPackages": [
"de.uhilger.meine.app"
],
"server": [
{
"name": "File Server",
"port":7000,
"contexts": [
{
"className": "de.uhilger.neon.Handler",
"sharedHandler": "false",
"contextPath": "/data",
"attributes": {
"contextName": "www",
"fileBase": "/home/fred/dateien/www"
}
}
]
}
]
}
In obiger Serverbeschreibung verweist die Angabe actorPackages
auf de.uhilger.meine.app
. Dort muss ein Actor wie der folgende hinterlegt sein.
Actor
-Klasse zur Auslieferung von Dateiinhalten@Actor(name="fileServer")
public class FileActor {
@Action(handler={"www"}, route="/", type=Action.Type.GET, handlesResponse = false)
public void run(HttpExchange exchange) throws IOException {
new FileServer().serveFile(exchange);
}
}
Mit der Angabe handler={"www"}
wird die Methode run
der Klasse FileActor
an den HTTP-Kontext gebunden, der in der Serverbeschreibung angegeben ist. Dies bewirkt, dass Aufrufe wie folgt beantwortet werden.
http://localhost:7000/data/pfad/zu/hallo-welt.html
liefert den Inhalt von
/home/fred/dateien/www/pfad/zu/hallo-welt.html
Routen
Als Route wird der Teil des URL angesehen, der eine bestimmte Aktion auslöst. Ein Uniform Resource Locator (URL) lässt sich in verschiedene Teile zerlegen.
http://localhost:7000/meine/app/route/zur/aktion?param1=eins¶m2=zwei
Protokoll: http
Domain: localhost
Port: 7000
Context-Path: /meine/app
Route: /route/zur/aktion
Query: param1=eins¶m2=zwei
Die Route ergibt sich nach Wegnahme aller möglichen Kontext-Pfade, d.h., Neon probiert alle Kontext-Pfade, für die laut Serverbeschreibung ein HttpContext angelegt wurde. Wenn ein Kontext-Pfad passt, wird diesem Kontext die Anfrage weitergeleitet.
Im Kontext bestimmt die Klasse de.uhilger.neon.Handler
die Route, wie sie nach Wegnahme des Kontext-Pfades entsteht und verwendet diese Route gegen den Routen-Ausdruck, der von der Annotation de.uhilger.neon.Action
geliefert wird. Neben einer festen Angabe wie im obigen Beispiel kann in der Action
auch die Route /
verwendet werden. Damit werden alle Routen an den Actor weitergegeben.
Ein Actor, der über die Route /
mehrere Routen entgegennimmt, verwendet üblicherweise ein Objekt der Klasse HttpExchange
, um die angefragte Route zu bestimmen. Anstelle der Parameterübergabe per Query wie weiter oben gezeigt lassen sich Parameter auch als Teil der Route übergeben wie in folgendem Beispiel
http://localhost:7000/meine/app/route/zur/aktion/zwei/eins
Dies erfordert eine entsprechende Annotation im Actor.
Dynamischer Datenaustausch
Actor-Objekte werden zur Laufzeit selbsttätig aus dem HTTP-Kontext des Servers erzeugt. Zum Zeitpunkt des Entwurfs von Anwendungen ist eventuell noch nicht absehbar, welche Daten zwischen Actor-Objekten und einer Anwendung fließen oder wo sie genau herkommen werden. Aus diesem Grund stellt Neon mit den Schnittstellen DataProvider
und DataConsumer
einen dynamisch zu Laufzeit verwendbaren Weg bereit, Daten mit Actor-Objekten auszutauschen.
Diese Schnittstellen können verwendet werden, indem der Neon-Factory in einer Variante der Methode runInstance
eine Liste mit DataProvider
-Objekten übergeben wird. Für HTTP-Aufrufe übernimmt dann Neon die Bindung jener DataProvider
-Objekten an Aktoren automatisch immer dann, wenn ein Actor-Objekt von Neon erzeugt wird, das die Schnittstelle DataConsumer
implementiert. Nachfolgend ein Beispiel für einen solchen Actor.
Die Schnittstellen DataProvider
und DataConsumer
können aber auch ausserhalb des HTTP-Kontextes verwendet werden und erlauben es, Daten mit Actor-Objekten auszutauschen, ohne diese als Parameter oder Rückgabewerte von Methoden der Actor-Klasse festlegen zu müssen.
DataConsumer-Beispiel
Um einen beliebigen Actor zum DataConsumer
zu machen, lässt sich beispielsweise eine abstrakte Klasse denken, die wie folgt angelegt ist.
public abstract class ConsumerActor implements DataConsumer {
protected List<DataProvider> provider = new ArrayList();
public Object getData(String name) {
Object o = null;
Iterator<DataProvider> i = provider.iterator();
while(o == null && i.hasNext()) {
DataProvider p = i.next();
o = p.getDataObject(name);
}
return o;
}
protected void clear() {
provider.clear();
provider = null;
}
@Override
public void addDataProvider(DataProvider provider) {
this.provider.add(provider);
}
@Override
public void removeDataProvider(DataProvider provider) {
this.provider.remove(provider);
}
}
Ableitungen der Klasse ConsumerActor können so Daten einer Anwendung verwenden, ohne, dass bereits beim Entwurf der Anwendung im Code festgelegt werden muss, welche Daten ausgetauscht werden.
@Actor(name="meinAktor")
public class MeinActor extends ConsumerActor {
@Action(handler={"einKontext"}, route="/gruss", type=Action.Type.GET, handlesResponse = false)
public String run() {
Object o = getData("user.name");
if(o instanceof String) {
gruss((String) o);
}
clear();
return antwort;
}
private String gruss(String param) {
return "Hallo " + param;
}
}
Ein Szenario für obiges Beispiel könnte sein, dass der Aufruf von http://localhost:port/kontext/gruss
stets die Ausgabe Hallo [Benutzername]
produziert und für [Benutzername]
der Name des angemeldeten Benutzers erscheint.
Natürlich könnte der Benutzername in diesem Fall auch über ein Attribut des HTTP-Kontext dem Aktor zufließen. Allerdings könnte es sein, dass der betreffende HTTP-Kontext nichts vom Benutzer 'weiß' und die Angabe aus einem anderen Bereich der Anwendung kommen muss. Zudem kann auch der Name des Datenelements variabel bleiben und muss nicht mit dem Namen user.name
wie im Beispiel hart codiert sein.
In diesen Fällen sind die Schnittstellen DataProvider
und DataConsumer
ein geeignetes Konstrukt.
Verschiedene Ports
Das Java-Modul jdk.httpserver
, auf dem Neon beruht, sieht vor, für jeden Port ein Objekt der Klasse Server auszuführen. Beispielsweise ließe sich denken, dass eine Anwendung einige Funktionen öffentlich bereitstellt und gewisse andere Funktionen mit z.B. administrativer Natur nur über ein lokales Netzwerk zugänglich macht.
In diesem Fall kann eine Serverbeschreibung veranlassen, zwei Ports über HTTP nutzbar zu machen.
{
"contentType": "text/json",
"contentId": "neon-descriptor-0.1.0",
"contentDescription":"Neon Descriptor Version 0.1.0",
"instanceName": "Mein Server",
"instanceDescription": "Beispiel-Serverinstanz",
"actorPackages": [
"com.example.my.app",
"com.example.my.other.library"
],
"server": [
{
"name": "oeffentlicher-server",
"port":7000,
"contexts": [
{
"className": "de.uhilger.neon.Handler",
"sharedHandler": "false",
"contextPath": "/meine/app",
"attributes": {
"contextName": "pub"
}
}
]
},
{
"name": "admin-server",
"port":7001,
"contexts": [
{
"className": "de.uhilger.neon.Handler",
"sharedHandler": "false",
"contextPath": "/meine/app",
"attributes": {
"contextName": "admin"
}
}
]
}
]
}
Mit einer Serverbeschreibung wie im obigen Beispiel würde eine Anwendung HTTP-Anfragen über folgende URLs entgegen nehmen:
http://localhost:7000/meine/app http://localhost:7001/meine/app
Über das Attribut contextName
lassen sich unterschiedliche Actor-Objekte an die verschiedenen Ports binden.
Shared Handler
Mit der Eigenschaft sharedHandler
kann dasselbe Handler-Objekt von verschiedenen HTTP-Kontexten genutzt werden. Sollen beispielsweise die Funktionen des öffentlichen Kontexts nicht nur über Port 7000 sondern auch über Port 7001 zugänglich sein, wird in der Serverbeschreibung ein zusätzlicher Kontext angelegt.
{
"contentType": "text/json",
"contentId": "neon-descriptor-0.1.0",
"contentDescription":"Neon Descriptor Version 0.1.0",
"instanceName": "Mein Server",
"instanceDescription": "Beispiel-Serverinstanz",
"actorPackages": [
"com.example.my.app",
"com.example.my.other.library"
],
"server": [
{
"name": "oeffentlicher-server",
"port":7000,
"contexts": [
{
"className": "de.uhilger.neon.Handler",
"sharedHandler": "true",
"contextPath": "/meine/app",
"attributes": {
"contextName": "pub"
}
}
]
},
{
"name": "admin-server",
"port":7001,
"contexts": [
{
"className": "de.uhilger.neon.Handler",
"sharedHandler": "false",
"contextPath": "/meine/app",
"attributes": {
"contextName": "admin"
}
},
{
"className": "de.uhilger.neon.Handler",
"sharedHandler": "true",
"contextPath": "/meine/app",
"attributes": {
"contextName": "pub"
}
}
]
}
]
}
Damit werden alle Actor-Objekte, die dem Kontext pub
zugeordnet sind, sowohl über Port 7000
als auch über Port 7001
zugänglich, ohne, dass ein weiterer Handler dafür erzeugt werden muss.
Neon stoppen
Eine laufende Java-Anwendung kann mit dem Befehl System.exit(0);
gestoppt werden. Auf eine App mit eingebettem HTTP-Server angewendet beendet dies auch den in die Anwendung eingebetteten Server. Vor dem Beenden der Anwendung sollte der Server aber zuvor auch explizit erst gestoppt werden. Dies erlaubt ein geordnetes Herunterfahren von Server und Anwendung.
Eine Actor-Klasse kann diese Schritte in geeigneter Weise ausführen und zugleich über HTTP aufrufbar machen.
@Actor(name="serverStopper")
public class ServerStopper {
@Action(handler={"admin"}, route="/server/stop", type=Action.Type.GET, handlesResponse = false)
public String run(HttpExchange exchange) {
HttpContext adminContext = exchange.getContext();
HttpServer server = adminContext.getServer();
Timer timer = new Timer();
if(server != null) {
timer.schedule(new ServerStopperTask(server), 1);
} else {
Logger.getLogger(ServerStopper.class.getName()).log(Level.INFO, "server ist null");
}
timer.schedule(new AppStopperTask(), 1200);
return "Server wird gestoppt.";
}
/**
* Die Klasse ServerStopperTask ermöglicht das asnychrone bzw.
* zeitgesteuerte Stoppen eines HttpServers.
*/
class ServerStopperTask extends TimerTask {
private final HttpServer server;
private final int port;
public ServerStopperTask(HttpServer server) {
this.server = server;
this.port = server.getAddress().getPort();
}
@Override
public void run() {
Logger.getLogger(ServerStopper.class.getName()).log(Level.INFO, "rufe server.stop fuer Port " + port);
server.stop(1);
}
}
/**
* Die Klasse AppStopperTask ermöglicht das asnychrone bzw.
* zeitgesteuerte Stoppen der Anwendung.
*/
class AppStopperTask extends TimerTask {
@Override
public void run() {
System.exit(0);
}
}
}
Die im Beispiel gezeigte Actor-Klasse verwendet in der Methode run
den Parameter exchange
, um den Server zu ermitteln, der gestoppt werden soll. Mit Hilfe eines Timers wird ein TimerTask eingeplant, der diesem Server ein Stopp-Signal gibt.
Der Aufruf server.stop(1)
im TimerTask bewirkt, dass die Socket-Verbindung des Servers geschlossen wird und damit keine weiteren HTTP-Anfragen mehr entgegen genommen und keine HTTP-Exchange-Objekte mehr vom Server erzeugt werden. Der Aufruf blockiert die Ausführung anderer Operationen im Thread dieses TimerTasks bis alle noch bestehenden exchange handler des Servers ausgeführt wurden oder eine Sekunde verstrichen ist, was immer früher eintritt. Dann werden alle offenen TCP-Verbindungen geschlossen und der Hintergrund-Thread des Servers, d.h. die Methode start() des Servers, endet. Auf diese Weise gestoppt kann der Server nicht wieder gestartet werden.
Mit dem Timer wird zugleich ein weiterer TimerTask eingeplant, der 1,2 Sekunden auf diese Schritte wartet und dann die Anwendung beendet.
Mehrere Server beenden
Macht eine Anwendung mehrere Ports auf, wie in Verschiedene Ports beschrieben, sollte jeder Server beendet werden, bevor die Anwendung gestoppt wird. Dies kann erreicht werden, indem mit Hilfe eines FactoryListener
beim Start Referenzen auf jeden weiteren Server als Attribut im Kontext des Stopp-Actors hinterlegt werden.
Der Actor aus Neon stoppen kann diese Referenzen nutzen, um alle Server zu stoppen.
Verweise
[1] Produktseite von Neon
[2] Übersicht von Modulen für Neon
[3] Java auf der Webseite von AdoptOpenJDK
[4] Gson