Microservices avec Oracle Helidon

1. Vue d'ensemble

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

Dans ce didacticiel, nous couvrirons 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, le framework prend en charge deux modèles de programmation pour l'écriture de microservices: Helidon SE et Helidon MP.

Alors qu'Helidon SE est conçu pour être un microframework prenant en charge le modèle de programmation réactive, Helidon MP, en revanche, est un environnement d'exécution Eclipse MicroProfile qui permet à la communauté Jakarta 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. Helidon SE

Dans cette section, nous découvrirons plus en détail les principaux composants d'Helidon SE: WebServer, Config et Security.

3.1. Configuration du serveur Web

Pour commencer avec l' API WebServer , nous devons ajouter la dépendance Maven requise au fichier pom.xml :

 io.helidon.webserver helidon-webserver 0.10.4 

Pour avoir une application Web simple, nous pouvons utiliser l'une des méthodes de création suivantes: WebServer.create (serverConfig, routage) ou simplement WebServer.create (routage) . 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 avec un message d'accueil pour toute requête HTTP avec le chemin '/ greet' et la méthode GET :

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

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

Le serveur Web est en fait un SPI, et nous devons fournir une implémentation d'exécution. Actuellement, Helidon fournit l' implémentation NettyWebServer qui est basée sur Netty Core.

Voici la dépendance Maven 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 qu'elle fonctionne en appelant le point de terminaison configuré:

//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' API de configuration . C'est le sujet de la section suivante.

3.2. L' API Config

L' API Config fournit des outils pour lire les données de configuration à partir d'une source de configuration .

Helidon SE fournit des implémentations pour de nombreuses sources de configuration. L'implémentation par défaut est fournie par helidon-config où la source de configuration est un fichier application.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 prend par défaut les données de configuration de application.properties:

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

Créons un fichier application.properties sous le répertoire src / main / resource avec le contenu suivant:

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

Pour lire les valeurs, nous pouvons utiliser la méthode Config.get () suivie 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 et 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 de configuration YAML associée:

 io.helidon.config helidon-config-yaml 0.10.4 

Et puis, on ajoute un application.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épendance helidon-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.

Nous pouvons également contrôler le comportement par défaut du générateur 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 classpath, nous pouvons également utiliser deux configurations de sources externes, c'est-à-dire les configs git et etcd. Pour cela, nous avons besoin des dépendances helidon-config-git et helidon-git-etcd.

Finally, if all of these configuration sources don't satisfy our need, Helidon allows us to provide an implementation for our configuration source. For example, we can provide an implementation that can read the configuration data from a database.

3.3. The Routing API

The Routing API provides the mechanism by which we bind HTTP requests to Java methods. We can accomplish this by using the request method and path as matching criteria or the RequestPredicate object for using more criteria.

So, to configure a route, we can just use the HTTP method as criteria:

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

Or we can combine the HTTP method with the request path:

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

We can also use the RequestPredicate for more control. For example, we can check for an existing header or for the content type:

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

Until now, we have provided handlers in the functional style. We can also use the Service class which allows writing handlers in a more sophisticated manner.

So, let's first create a model for the object we're working with, the Book class:

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

We can create REST Services for the Book class by implementing the Service.update() method. This allows configuring the subpaths of the same resource:

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

We've also configured the Media Type as JSON, so we need the helidon-webserver-json dependency for this purpose:

 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

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 Eclipse MicroProfile, nous pouvons exécuter n'importe quel microservice MicroProfile existant qui l'utilise.

Comme toujours, le code de tous les exemples ci-dessus se trouve à l'adresse over sur GitHub.