Microservices mit Oracle Helidon

Microservices mit Oracle Helidon

1. Überblick

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

Derzeitthe framework supports two programming models for writing microservices: Helidon SE and Helidon MP.

Während Helidon SE als Mikroframework konzipiert wurde, das das reaktive Programmiermodell unterstützt, ist Helidon MP eine Eclipse MicroProfile-Laufzeit, mit der die Java EE-Community Microservices portabel 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 mitWebServer API, zu beginnen, müssen wir die erforderlichenMaven dependency zurpom.xml-Datei hinzufügen:


    io.helidon.webserver
    helidon-webserver
    0.10.4

Um eine einfache Webanwendung zu haben, müssen Siewe can use one of the following builder methods: WebServer.create(serverConfig, routing) or just WebServer.create(routing). Bei der letzten wird eine Standard-Serverkonfiguration verwendet, die es dem Server ermöglicht, an 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 Methode "GET" 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: http://localhost:" + ws.port())
      );
}

In der letzten Zeile wird der Server gestartet und auf die Bearbeitung von HTTP-Anforderungen gewartet. 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

WebServer ist eigentlich ein SPI, und wir müssen eine Laufzeitimplementierung bereitstellen. DerzeitHelidon provides the NettyWebServer implementation, basierend aufNetty Core.

Hier sind dieMaven dependency 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:

http://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 die Verwendung eines Konfigurationsmusters, bei dem die Konfigurationsdaten von derConfig-API bereitgestellt werden. Dies ist das Thema des nächsten Abschnitts.


3.2. DieConfig API

The Config API provides tools for reading configuration data from a configuration source.

Helidon SE bietet Implementierungen für viele Konfigurationsquellen. Die Standardimplementierung wird vonhelidon-config bereitgestellt, wobei die Konfigurationsquelle eineapplication.properties-Datei 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 vonapplication.properties: übernimmt

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

Erstellen wir eineapplication.properties-Datei im Verzeichnissrc/main/resource mit folgendem Inhalt:

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

To read the values we can use the Config.get() method gefolgt von einem praktischen Casting zu den 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, and 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 Abhängigkeit von YAMLconfighinzufügen:


    io.helidon.config
    helidon-config-yaml
    0.10.4

Und dann addieren wir einapplication.yml:

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 vonhelidon-config-hoconhinzufügen.

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

We can also control the default builder behavior durch Deaktivieren der Umgebungsvariablen und der Systemeigenschaften oder durch explizite Angabe der Konfigurationsquelle:

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

Zusätzlich zum Lesen der Konfigurationsdaten aus dem Klassenpfad können auch zwei externe Quellenkonfigurationen verwendet werden, nämlich die git- und die etcd-Konfiguration. Dazu benötigen wir die Abhängigkeitenhelidon-config-git undhelidon-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. DieRouting API

The Routing API provides the mechanism by which we bind HTTP requests to Java methods. Wir können dies erreichen, indem wir die Anforderungsmethode und den Anforderungspfad als Übereinstimmungskriterien oder das ObjektRequestPredicate 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 auchRequestPredicate für mehr Kontrolle verwenden. Beispielsweise 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 zur Verfügung gestellt. Wir können auch die KlasseServiceverwenden, 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 KlasseBook:

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

Wir können REST-Services für die KlasseBookerstellen, indem wir die MethodeService.update()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 vonhelidon-webserver-json:


    io.helidon.webserver
    helidon-webserver-json
    0.10.4

Schließlich wirdwe use the register() method of the Routing builder to bind the root path to the resource. In diesem Fall wird dem vom Dienst konfiguriertenPaths der Stammpfad vorangestellt:

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

Jetzt können wir den Server starten und die Endpunkte überprüfen:

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

3.4. Sicherheit

In diesem Abschnitt werdenwe’re going to secure our resources using the Security module.

Beginnen wir mit der Deklaration aller erforderlichen Abhängigkeiten:


    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

Die Abhängigkeitenhelidon-security,helidon-security-provider-http-auth undhelidon-security-integration-webserver sind bei Maven Central erhältlich.

Das Sicherheitsmodul bietet viele Anbieter für Authentifizierung und Autorisierung. For this example, we’ll use the HTTP basic authentication provider ist ziemlich einfach, aber der Prozess für andere Anbieter ist fast der gleiche.

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();

Oder wir können einen Konfigurationsansatz verwenden.

In diesem Fall deklarieren wir die gesamte Sicherheitskonfiguration in der Dateiapplication.yml, die wir über die APIConfigladen:

#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"]

Und um es zu laden, müssen wir nur einConfig-Objekt erstellen und dann dieSecurity.fromConfig()-Methode aufrufen:

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

Sobald wir dieSecurity-Instanz haben, müssen wir sie zuerst mit derWebServer-Methode beiWebServer registrieren:

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

Wir können auch eineWebSecurity-Instanz direkt mithilfe des Konfigurationsansatzes erstellen, mit dem wir sowohl die Sicherheits- als auch die Webserverkonfiguration laden:

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

Wir können jetzt einige Handler für die Pfade/user und/adminhinzufügen, den Server starten und versuchen, auf sie zuzugreifen:

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 und bietet auch eine Laufzeit für die Ausführung von MicroProfile-basierten Microservices.

Da wir bereitsan article about Eclipse MicroProfile haben, werden wir diesen Quellcode überprüfen und ihn so ändern, dass er auf Helidon MP ausgeführt wird.

Nach dem Auschecken des Codes entfernen wir alle Abhängigkeiten und Plugins und fügen die Helidon MP-Abhängigkeiten zur POM-Datei hinzu:


    io.helidon.microprofile.bundles
    helidon-microprofile-1.2
    0.10.4


    org.glassfish.jersey.media
    jersey-media-json-binding
    2.26

Die Abhängigkeitenhelidon-microprofile-1.2 undjersey-media-json-binding sind bei Maven Central erhältlich.

Als nächsteswe’ll add the beans.xml file under the src/main/resource/META-INF directory mit diesem Inhalt:


Überschreiben Sie in der KlasseLibraryApplicationdie MethodegetClasses(), damit der Server nicht nach Ressourcen sucht:

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

Erstellen Sie schließlich eine Hauptmethode und fügen Sie diesen Codeausschnitt hinzu:

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

Und das ist es. Wir können jetzt alle Buchressourcen aufrufen.

5. Fazit

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 jeden vorhandenen MicroProfile-basierten Microservice damit ausführen.

Wie immer kann der Code aller obigen Beispieleover on GitHub gefunden werden.