Microservices avec Oracle Helidon

Microservices avec Oracle Helidon

1. Vue d'ensemble

Helidon est le nouveau framework de microservices Java récemment ouvert par Oracle. Il était utilisé en interne dans les projets Oracle sous le nom J4C (Java for Cloud).

Dans ce didacticiel, nous aborderons les principaux concepts du framework, puis nous passerons à la création et à l'exécution d'un microservice basé sur Helidon.

2. Modèle de programmation

Actuellement,the framework supports two programming models for writing microservices: Helidon SE and Helidon MP.

Alors que Helidon SE est conçu pour être un micro-cadre prenant en charge le modèle de programmation réactive, Helidon MP, quant à lui, est un environnement d'exécution Eclipse MicroProfile qui permet à la communauté Java EE d'exécuter des microservices de manière portable.

Dans les deux cas, un microservice Helidon est une application Java SE qui démarre un petit serveur HTTP à partir de la méthode principale.

3. Hélidon SE

Dans cette section, nous découvrirons plus en détail les principaux composants d’Helidon SE: serveur Web, configuration et sécurité.

3.1. Configuration du serveur Web

Pour commencer avec lesWebServer API,, nous devons ajouter lesMaven dependency requis au fichierpom.xml:


    io.helidon.webserver
    helidon-webserver
    0.10.4

Pour avoir une application Web simple,we can use one of the following builder methods: WebServer.create(serverConfig, routing) or just WebServer.create(routing). Le dernier prend une configuration de serveur par défaut permettant au serveur de s’exécuter sur un port aléatoire.

Voici une application Web simple qui s’exécute sur un port prédéfini. Nous avons également enregistré un gestionnaire simple qui répondra par un message d’accueil pour toute requête HTTP avec le chemin ‘/greet' et la méthodeGET:

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

La dernière ligne consiste à démarrer le serveur et à attendre le traitement des requêtes HTTP. Mais si nous exécutons cet exemple de code dans la méthode principale, nous obtiendrons l'erreur:

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

LeWebServer est en fait un SPI, et nous devons fournir une implémentation d'exécution. Actuellement,Helidon provides the NettyWebServer implementation qui est basé surNetty Core.

Voici lesMaven dependency pour cette implémentation:


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

Maintenant, nous pouvons exécuter l’application principale et vérifier son fonctionnement en appelant le noeud final configuré:

http://localhost:9001/greet

Dans cet exemple, nous avons configuré le port et le chemin à l'aide du modèle de générateur.

Helidon SE permet également d'utiliser un modèle de configuration où les données de configuration sont fournies par l'APIConfig. C'est le sujet de la section suivante.


3.2. L'APIConfig

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

Helidon SE fournit des implémentations pour de nombreuses sources de configuration. L'implémentation par défaut est fournie parhelidon-config où la source de configuration est un fichierapplication.properties situé sous le chemin de classe:


    io.helidon.config
    helidon-config
    0.10.4

Pour lire les données de configuration, il suffit d'utiliser le générateur par défaut qui par défaut prend les données de configuration deapplication.properties:

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

Créons un fichierapplication.properties sous le répertoiresrc/main/resource avec le contenu suivant:

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 suivi d'un cast pratique vers les types Java correspondants:

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

En fait, le générateur par défaut charge le premier fichier trouvé dans cet ordre de priorité:application.yaml, application.conf, application.json, and application.properties. Les trois derniers formats nécessitent une dépendance de configuration supplémentaire. Par exemple, pour utiliser le format YAML, nous devons ajouter la dépendance associée YAMLconfig:


    io.helidon.config
    helidon-config-yaml
    0.10.4

Et puis, on ajoute unapplication.yml:

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

De même, pour utiliser le CONF, qui est un format JSON simplifié, ou les formats JSON, nous devons ajouter la dépendancehelidon-config-hocon.

Notez que les données de configuration de ces fichiers peuvent être remplacées par des variables d'environnement et des propriétés Java System.

We can also control the default builder behavior en désactivant la variable d'environnement et les propriétés système ou en spécifiant explicitement la source de configuration:

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

En plus de lire les données de configuration à partir du chemin de classe, nous pouvons également utiliser deux configurations de sources externes, à savoir les configs git et etcd. Pour cela, nous avons besoin des dépendanceshelidon-config-git ethelidon-git-etcd.

Enfin, si toutes ces sources de configuration ne répondent pas à nos besoins, Helidon nous permet de fournir une implémentation pour notre source de configuration. Par exemple, nous pouvons fournir une implémentation capable de lire les données de configuration d'une base de données.

3.3. L'APIRouting

The Routing API provides the mechanism by which we bind HTTP requests to Java methods. Nous pouvons accomplir cela en utilisant la méthode de requête et le chemin comme critères de correspondance ou l'objetRequestPredicate pour utiliser plus de critères.

Donc, pour configurer un itinéraire, nous pouvons simplement utiliser la méthode HTTP comme critère:

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

Ou nous pouvons combiner la méthode HTTP avec le chemin de requête:

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

Nous pouvons également utiliser lesRequestPredicate pour plus de contrôle. Par exemple, nous pouvons rechercher un en-tête existant ou le type de contenu:

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

Jusqu'à présent, nous avons fourni des gestionnaires dans le style fonctionnel. Nous pouvons également utiliser la classeService qui permet d'écrire des gestionnaires de manière plus sophistiquée.

Commençons par créer un modèle pour l’objet avec lequel nous travaillons, la classeBook:

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

Nous pouvons créer des services REST pour la classeBook en implémentant la méthodeService.update(). Cela permet de configurer les sous-chemins de la même ressource:

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);
    }
    //...
}

Nous avons également configuré le type de média en tant que JSON, nous avons donc besoin de la dépendancehelidon-webserver-json à cette fin:


    io.helidon.webserver
    helidon-webserver-json
    0.10.4

Enfin,we use the register() method of the Routing builder to bind the root path to the resource. Dans ce cas, lesPaths configurés par le service sont préfixés par le chemin racine:

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

Nous pouvons maintenant démarrer le serveur et vérifier les ordinateurs d'extrémité:

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

3.4. Sécurité

Dans cette section,we’re going to secure our resources using the Security module.

Commençons par déclarer toutes les dépendances nécessaires:


    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

Les dépendanceshelidon-security,helidon-security-provider-http-auth ethelidon-security-integration-webserver sont disponibles depuis Maven Central.

Le module de sécurité propose de nombreux fournisseurs pour l'authentification et l'autorisation. For this example, we’ll use the HTTP basic authentication provider car c'est assez simple, mais le processus pour les autres fournisseurs est presque le même.

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

Ou nous pouvons utiliser une approche de configuration.

Dans ce cas, nous déclarerons toute la configuration de sécurité dans le fichierapplication.yml que nous chargeons via l'APIConfig:

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

Et pour le charger, il suffit de créer un objetConfig puis d'appeler la méthodeSecurity.fromConfig():

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

Une fois que nous avons l'instanceSecurity, nous devons d'abord l'enregistrer avec leWebServer en utilisant la méthodeWebSecurity.from():

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

Nous pouvons également créer une instanceWebSecurity directement en utilisant l'approche de configuration par laquelle nous chargeons à la fois la sécurité et la configuration du serveur Web:

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

Nous pouvons maintenant ajouter des gestionnaires pour les chemins/user et/admin, démarrer le serveur et essayer d'y accéder:

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. Hélidon MP

Helidon MP is an implementation of Eclipse MicroProfile et fournit également un runtime pour exécuter des microservices basés sur MicroProfile.

Comme nous avons déjàan article about Eclipse MicroProfile, nous allons vérifier ce code source et le modifier pour fonctionner sur Helidon MP.

Après avoir vérifié le code, nous supprimerons toutes les dépendances et plug-ins et ajouterons les dépendances Helidon MP au fichier POM:


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


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

Les dépendanceshelidon-microprofile-1.2 etjersey-media-json-binding sont disponibles auprès de Maven Central.

Ensuite,we’ll add the beans.xml file under the src/main/resource/META-INF directory avec ce contenu:


Dans la classeLibraryApplication, remplacez la méthodegetClasses() afin que le serveur ne recherche pas de ressources:

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

Enfin, créez une méthode principale et ajoutez cet extrait de code:

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

Et c'est tout. Nous allons maintenant pouvoir invoquer toutes les ressources du livre.

5. Conclusion

Dans cet article, nous avons exploré les principaux composants d'Helidon, en montrant également comment configurer Helidon SE et MP. Comme Helidon MP n’est qu’un runtime d’Eclipse MicroProfile, nous pouvons utiliser tout microservice existant basé sur MicroProfile.

Comme toujours, le code de tous les exemples ci-dessus peut être trouvéover on GitHub.