Microservices mit Oracle Helidon

1. Übersicht

Helidon ist das neue Java-Microservice-Framework, das kürzlich von Oracle als Open-Source-Version bereitgestellt wurde. Es wurde intern in Oracle-Projekten unter dem Namen J4C (Java for Cloud) verwendet.

In diesem Tutorial werden die Hauptkonzepte des Frameworks behandelt und anschließend ein Helidon-basierter Mikroservice erstellt und ausgeführt.

2. Programmiermodell

Derzeit unterstützt das Framework zwei Programmiermodelle zum Schreiben von Microservices: Helidon SE und Helidon MP.

Während Helidon SE als Mikroframework konzipiert ist, das das reaktive Programmiermodell unterstützt, ist Helidon MP eine Eclipse MicroProfile-Laufzeit, mit der die Jakarta EE-Community Microservices auf tragbare Weise ausführen kann.

In beiden Fällen ist ein Helidon-Mikroservice eine Java SE-Anwendung, die einen kleinen HTTP-Server über die Hauptmethode startet.

3. Helidon SE

In diesem Abschnitt werden die Hauptkomponenten von Helidon SE ausführlicher beschrieben: WebServer, Config und Security.

3.1. Einrichten des WebServers

Um mit der WebServer-API zu beginnen , müssen Sie der Datei pom.xml die erforderliche Maven-Abhängigkeit hinzufügen :

 io.helidon.webserver helidon-webserver 0.10.4 

Um eine einfache Webanwendung zu haben, können wir eine der folgenden Builder-Methoden verwenden: WebServer.create (serverConfig, routing) oder nur WebServer.create (routing) . Die letzte nimmt eine Standardserverkonfiguration an, die es dem Server ermöglicht, auf einem zufälligen Port ausgeführt zu werden.

Hier ist eine einfache Webanwendung, die auf einem vordefinierten Port ausgeführt wird. Wir haben auch einen einfachen Handler registriert, der mit einer Begrüßungsnachricht auf jede HTTP-Anfrage mit dem Pfad '/ greet' und der GET- Methode antwortet :

public static void main(String... args) throws Exception { ServerConfiguration serverConfig = ServerConfiguration.builder() .port(9001).build(); Routing routing = Routing.builder() .get("/greet", (request, response) -> response.send("Hello World !")).build(); WebServer.create(serverConfig, routing) .start() .thenAccept(ws -> System.out.println("Server started at: //localhost:" + ws.port()) ); }

Die letzte Zeile besteht darin, den Server zu starten und auf die Bearbeitung von HTTP-Anforderungen zu warten. Wenn wir diesen Beispielcode jedoch in der Hauptmethode ausführen, wird der folgende Fehler angezeigt:

Exception in thread "main" java.lang.IllegalStateException: No implementation found for SPI: io.helidon.webserver.spi.WebServerFactory

Der WebServer ist eigentlich ein SPI, und wir müssen eine Laufzeitimplementierung bereitstellen. Derzeit bietet Helidon die NettyWebServer- Implementierung an, die auf Netty Core basiert.

Hier ist die Maven-Abhängigkeit für diese Implementierung:

 io.helidon.webserver helidon-webserver-netty 0.10.4 runtime 

Jetzt können wir die Hauptanwendung ausführen und überprüfen, ob sie funktioniert, indem wir den konfigurierten Endpunkt aufrufen:

//localhost:9001/greet

In diesem Beispiel haben wir sowohl den Port als auch den Pfad mithilfe des Builder-Musters konfiguriert.

Helidon SE ermöglicht auch ein Konfigurationsmuster verwendet , wo die Konfigurationsdaten von der vorgesehen ist , Config API. Dies ist das Thema des nächsten Abschnitts.

3.2. Die Konfigurations- API

Der Config - API bietet Tools für die Konfigurationsdaten aus einer Konfigurationsquelle zu lesen .

Helidon SE bietet Implementierungen für viele Konfigurationsquellen. Die Standardimplementierung wird von helidon-config bereitgestellt, wobei die Konfigurationsquelle eine Datei application.properties ist, die sich unter dem Klassenpfad befindet:

 io.helidon.config helidon-config 0.10.4 

Um die Konfigurationsdaten zu lesen, müssen wir nur den Standard-Builder verwenden, der standardmäßig die Konfigurationsdaten aus application.properties übernimmt :

Config config = Config.builder().build();

Erstellen wir eine application.properties- Datei im Verzeichnis src / main / resource mit folgendem Inhalt:

server.port=9080 web.debug=true web.page-size=15 user.home=C:/Users/app

Um die Werte zu lesen, können wir die Config.get () -Methode verwenden, gefolgt von einem praktischen Casting in die entsprechenden Java-Typen:

int port = config.get("server.port").asInt(); int pageSize = config.get("web.page-size").asInt(); boolean debug = config.get("web.debug").asBoolean(); String userHome = config.get("user.home").asString();

Tatsächlich lädt der Standard-Builder die zuerst gefundene Datei in dieser Prioritätsreihenfolge: application.yaml, application.conf, application.json und application.properties. Die letzten drei Formate benötigen eine zusätzliche zugehörige Konfigurationsabhängigkeit. Um beispielsweise das YAML-Format zu verwenden, müssen Sie die zugehörige YAML-Konfigurationsabhängigkeit hinzufügen:

 io.helidon.config helidon-config-yaml 0.10.4 

Und dann fügen wir eine application.yml hinzu :

server: port: 9080 web: debug: true page-size: 15 user: home: C:/Users/app

Um die CONF, ein vereinfachtes JSON-Format, oder JSON-Formate zu verwenden, müssen Sie die Abhängigkeit helidon-config-hocon hinzufügen.

Beachten Sie, dass die Konfigurationsdaten in diesen Dateien durch Umgebungsvariablen und Java-Systemeigenschaften überschrieben werden können.

Sie können das Standardverhalten des Builders auch steuern, indem Sie die Umgebungsvariablen und Systemeigenschaften deaktivieren oder die Konfigurationsquelle explizit angeben:

ConfigSource configSource = ConfigSources.classpath("application.yaml").build(); Config config = Config.builder() .disableSystemPropertiesSource() .disableEnvironmentVariablesSource() .sources(configSource) .build();

Zusätzlich zum Lesen von Konfigurationsdaten aus dem Klassenpfad können wir auch zwei externe Quellkonfigurationen verwenden, nämlich die Konfiguration git und etcd. Dazu benötigen wir die Abhängigkeiten helidon-config-git und helidon-git-etcd.

Wenn alle diese Konfigurationsquellen nicht unseren Anforderungen entsprechen, können wir mit Helidon eine Implementierung für unsere Konfigurationsquelle bereitstellen. Beispielsweise können wir eine Implementierung bereitstellen, die die Konfigurationsdaten aus einer Datenbank lesen kann.

3.3. Die Routing- API

Die Routing- API bietet den Mechanismus, mit dem wir HTTP-Anforderungen an Java-Methoden binden. Dies können wir erreichen, indem wir die Anforderungsmethode und den Pfad als Übereinstimmungskriterien oder das RequestPredicate- Objekt verwenden, um weitere Kriterien zu verwenden.

Um eine Route zu konfigurieren, können wir einfach die HTTP-Methode als Kriterium verwenden:

Routing routing = Routing.builder() .get((request, response) -> {} );

Oder wir können die HTTP-Methode mit dem Anforderungspfad kombinieren:

Routing routing = Routing.builder() .get("/path", (request, response) -> {} );

Wir können auch das RequestPredicate für mehr Kontrolle verwenden. Zum Beispiel können wir nach einem vorhandenen Header oder nach dem Inhaltstyp suchen:

Routing routing = Routing.builder() .post("/save", RequestPredicate.whenRequest() .containsHeader("header1") .containsCookie("cookie1") .accepts(MediaType.APPLICATION_JSON) .containsQueryParameter("param1") .hasContentType("application/json") .thenApply((request, response) -> { }) .otherwise((request, response) -> { })) .build();

Bisher haben wir Handler im funktionalen Stil bereitgestellt. Wir können auch die Service- Klasse verwenden, mit der Handler auf komplexere Weise geschrieben werden können.

Erstellen wir also zunächst ein Modell für das Objekt, mit dem wir arbeiten, die Book- Klasse:

public class Book { private String id; private String name; private String author; private Integer pages; // ... }

Wir können REST-Services für die Book- Klasse erstellen, indem wir die Service.update () -Methode implementieren . Auf diese Weise können Sie die Unterpfade derselben Ressource konfigurieren:

public class BookResource implements Service { private BookManager bookManager = new BookManager(); @Override public void update(Routing.Rules rules) { rules .get("/", this::books) .get("/{id}", this::bookById); } private void bookById(ServerRequest serverRequest, ServerResponse serverResponse) { String id = serverRequest.path().param("id"); Book book = bookManager.get(id); JsonObject jsonObject = from(book); serverResponse.send(jsonObject); } private void books(ServerRequest serverRequest, ServerResponse serverResponse) { List books = bookManager.getAll(); JsonArray jsonArray = from(books); serverResponse.send(jsonArray); } //... }

Wir haben den Medientyp auch als JSON konfiguriert, daher benötigen wir für diesen Zweck die Abhängigkeit helidon-webserver-json:

 io.helidon.webserver helidon-webserver-json 0.10.4 

Finally, we use the register() method of the Routing builder to bind the root path to the resource. In this case, Paths configured by the service are prefixed by the root path:

Routing routing = Routing.builder() .register(JsonSupport.get()) .register("/books", new BookResource()) .build();

We can now start the server and check the endpoints:

//localhost:9080/books //localhost:9080/books/0001-201810

3.4. Security

In this section, we're going to secure our resources using the Security module.

Let's start by declaring all the necessary dependencies:

 io.helidon.security helidon-security 0.10.4   io.helidon.security helidon-security-provider-http-auth 0.10.4   io.helidon.security helidon-security-integration-webserver 0.10.4 

The helidon-security, helidon-security-provider-http-auth, and helidon-security-integration-webserver dependencies are available from Maven Central.

The security module offers many providers for authentication and authorization. For this example, we'll use the HTTP basic authentication provider as it's fairly simple, but the process for other providers is almost the same.

The first thing to do is create a Security instance. We can do it either programmatically for simplicity:

Map users = //... UserStore store = user -> Optional.ofNullable(users.get(user)); HttpBasicAuthProvider httpBasicAuthProvider = HttpBasicAuthProvider.builder() .realm("myRealm") .subjectType(SubjectType.USER) .userStore(store) .build(); Security security = Security.builder() .addAuthenticationProvider(httpBasicAuthProvider) .build();

Or we can use a configuration approach.

In this case, we'll declare all the security configuration in the application.yml file which we load through the Config API:

#Config 4 Security ==> Mapped to Security Object security: providers: - http-basic-auth: realm: "helidon" principal-type: USER # Can be USER or SERVICE, default is USER users: - login: "user" password: "user" roles: ["ROLE_USER"] - login: "admin" password: "admin" roles: ["ROLE_USER", "ROLE_ADMIN"] #Config 4 Security Web Server Integration ==> Mapped to WebSecurity Object web-server: securityDefaults: authenticate: true paths: - path: "/user" methods: ["get"] roles-allowed: ["ROLE_USER", "ROLE_ADMIN"] - path: "/admin" methods: ["get"] roles-allowed: ["ROLE_ADMIN"]

And to load it, we need just to create a Config object and then we invoke the Security.fromConfig() method:

Config config = Config.create(); Security security = Security.fromConfig(config);

Once we have the Security instance, we first need to register it with the WebServer using the WebSecurity.from() method:

Routing routing = Routing.builder() .register(WebSecurity.from(security).securityDefaults(WebSecurity.authenticate())) .build();

We can also create a WebSecurity instance directly using the config approach by which we load both the security and the web server configuration:

Routing routing = Routing.builder() .register(WebSecurity.from(config)) .build();

We can now add some handlers for the /user and /admin paths, start the server and try to access them:

Routing routing = Routing.builder() .register(WebSecurity.from(config)) .get("/user", (request, response) -> response.send("Hello, I'm Helidon SE")) .get("/admin", (request, response) -> response.send("Hello, I'm Helidon SE")) .build();

4. Helidon MP

Helidon MP is an implementation of Eclipse MicroProfile and also provides a runtime for running MicroProfile based microservices.

As we already have an article about Eclipse MicroProfile, we'll check out that source code and modify it to run on Helidon MP.

After checking out the code, we'll remove all dependencies and plugins and add the Helidon MP dependencies to the POM file:

 io.helidon.microprofile.bundles helidon-microprofile-1.2 0.10.4   org.glassfish.jersey.media jersey-media-json-binding 2.26 

The helidon-microprofile-1.2 and jersey-media-json-binding dependencies are available from Maven Central.

Next, we'll add the beans.xml file under the src/main/resource/META-INF directory with this content:

In the LibraryApplication class, override getClasses() method so that the server won't scan for resources:

@Override public Set
    
      getClasses() { return CollectionsHelper.setOf(BookEndpoint.class); }
    

Finally, create a main method and add this code snippet:

public static void main(String... args) { Server server = Server.builder() .addApplication(LibraryApplication.class) .port(9080) .build(); server.start(); }

And that's it. We'll now be able to invoke all the book resources.

5. Conclusion

In diesem Artikel haben wir die Hauptkomponenten von Helidon untersucht und gezeigt, wie Helidon SE und MP eingerichtet werden. Da Helidon MP nur eine Eclipse MicroProfile-Laufzeit ist, können wir damit jeden vorhandenen MicroProfile-basierten Microservice ausführen.

Wie immer finden Sie den Code aller obigen Beispiele auf GitHub.