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(App.class, 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. Der Aufruf von runInstance erfordert zudem die Angabe der aufrufenden Klasse. Auf diese Weise können Objekte der Klasse Factory deren ClassLoader zum dynamischen Laden von Aktor-Klassen verwenden.
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
classNamewird die Klasse angegeben, die für diesen Kontext als HttpHandler dient. Die Klassede.uhilger.neon.Handlerkann für alle Kontexte als generischer Handler dienen. Sie erzeugt zur Laufzeit dynamisch Objekte des TypsActoraus den Klassen, die inactorPackagesgefunden werden. - sharedHandler
-
Über
sharedHandlerkann ein einzelnes Handler-Objekt für mehrere Kontexte verwendet werden. - contextPath
-
Jeder Kontext ist an einen bestimmten Pfad gebunden, der mit dem Attribut
contextPathangegeben wird. - contextName
-
Das Attribut
contextNamegibt den Namen an, über den die Bindung von Klassen des TypsActorerfolgt. 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.
Actorpackage 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 AnnotationActionverbindet die Methode einerActor-Klasse mit der Konfiguration eines HTTP-Kontexts aus der Serverbeschreibung. Das AttributcontextPathdes 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/appergeben. Das Attributhandlererlaubt 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
routenennt 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/beispielherangezogen. Über diesen URL kann der Code der Methoderundes Beispiels ausgeführt werden. - type
-
Mit dem Attribut
typewird angegeben, mit welcher HTTP-Methode die betreffende Methode aufrufbar sein soll,GET,PUT,POSToderDELETE. - handlesResponse
-
Mit
handlesResponsewird dem Handler mitgeteilt, ob die Action sich selbst um das Senden einer Antwort kümmert. Ein Eintrag vonfalseveranlasst 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