Microservizi con Oracle Helidon

1. Panoramica

Helidon è il nuovo framework di microservizi Java che è stato recentemente reso open source da Oracle. È stato utilizzato internamente nei progetti Oracle con il nome J4C (Java for Cloud).

In questo tutorial tratteremo i concetti principali del framework e poi passeremo alla creazione ed esecuzione di un microservizio basato su Helidon.

2. Modello di programmazione

Attualmente, il framework supporta due modelli di programmazione per la scrittura di microservizi: Helidon SE e Helidon MP.

Mentre Helidon SE è progettato per essere un microframework che supporta il modello di programmazione reattiva, Helidon MP, d'altra parte, è un runtime Eclipse MicroProfile che consente alla comunità EE di Jakarta di eseguire microservizi in modo portatile.

In entrambi i casi, un microservizio Helidon è un'applicazione Java SE che avvia un minuscolo server HTTP dal metodo principale.

3. Helidon SE

In questa sezione scopriremo più in dettaglio i componenti principali di Helidon SE: WebServer, Config e Security.

3.1. Configurazione del WebServer

Per iniziare con l' API WebServer , dobbiamo aggiungere la dipendenza Maven richiesta al file pom.xml :

 io.helidon.webserver helidon-webserver 0.10.4 

Per avere una semplice applicazione web, possiamo usare uno dei seguenti metodi di builder: WebServer.create (serverConfig, routing) o semplicemente WebServer.create (routing) . L'ultimo prende una configurazione del server predefinita che consente al server di funzionare su una porta casuale.

Ecco una semplice applicazione Web che viene eseguita su una porta predefinita. Abbiamo anche registrato un semplice gestore che risponderà con un messaggio di saluto per qualsiasi richiesta HTTP con percorso '/ greet' e metodo 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()) ); }

L'ultima riga serve per avviare il server e attendere per servire le richieste HTTP. Ma se eseguiamo questo codice di esempio nel metodo principale, otterremo l'errore:

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

Il WebServer è in realtà uno SPI e dobbiamo fornire un'implementazione runtime. Attualmente, Helidon fornisce l' implementazione NettyWebServer basata su Netty Core.

Ecco la dipendenza da Maven per questa implementazione:

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

Ora possiamo eseguire l'applicazione principale e verificare che funzioni invocando l'endpoint configurato:

//localhost:9001/greet

In questo esempio, abbiamo configurato sia la porta che il percorso utilizzando il pattern builder.

Helidon SE consente anche di utilizzare un modello di configurazione in cui i dati di configurazione sono forniti dall'API di configurazione . Questo è l'argomento della prossima sezione.

3.2. L' API di configurazione

L' API di configurazione fornisce strumenti per leggere i dati di configurazione da un'origine di configurazione .

Helidon SE fornisce implementazioni per molte sorgenti di configurazione. L'implementazione predefinita è fornita da helidon-config dove l'origine della configurazione è un file application.properties situato sotto il classpath:

 io.helidon.config helidon-config 0.10.4 

Per leggere i dati di configurazione, è sufficiente utilizzare il builder predefinito che per impostazione predefinita prende i dati di configurazione da application.properties:

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

Creiamo un file application.properties nella directory src / main / resource con il seguente contenuto:

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

Per leggere i valori possiamo utilizzare il metodo Config.get () seguito da un comodo casting ai corrispondenti tipi Java:

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

In effetti, il generatore predefinito carica il primo file trovato in questo ordine di priorità: application.yaml, application.conf, application.json e application.properties. Gli ultimi tre formati richiedono un'ulteriore dipendenza di configurazione correlata. Ad esempio, per utilizzare il formato YAML, dobbiamo aggiungere la relativa dipendenza di configurazione YAML:

 io.helidon.config helidon-config-yaml 0.10.4 

E poi, aggiungiamo un application.yml :

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

Allo stesso modo, per utilizzare CONF, che è un formato semplificato JSON, o formati JSON, dobbiamo aggiungere la dipendenza helidon-config-hocon.

Notare che i dati di configurazione in questi file possono essere sovrascritti dalle variabili di ambiente e dalle proprietà di Java System.

Possiamo anche controllare il comportamento del builder predefinito disabilitando la variabile d'ambiente e le proprietà di sistema o specificando esplicitamente l'origine della configurazione:

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

Oltre a leggere i dati di configurazione dal classpath, possiamo anche utilizzare due configurazioni di sorgenti esterne, ovvero le configurazioni git e etcd. Per questo, abbiamo bisogno delle dipendenze helidon-config-git e helidon-git-etcd.

Infine, se tutte queste sorgenti di configurazione non soddisfano le nostre esigenze, Helidon ci consente di fornire un'implementazione per la nostra sorgente di configurazione. Ad esempio, possiamo fornire un'implementazione in grado di leggere i dati di configurazione da un database.

3.3. L' API di routing

L' API di routing fornisce il meccanismo mediante il quale leghiamo le richieste HTTP ai metodi Java. È possibile ottenere ciò utilizzando il metodo e il percorso della richiesta come criteri di corrispondenza o l' oggetto RequestPredicate per l'utilizzo di più criteri.

Quindi, per configurare una rotta, possiamo semplicemente usare il metodo HTTP come criterio:

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

Oppure possiamo combinare il metodo HTTP con il percorso della richiesta:

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

Possiamo anche usare RequestPredicate per un maggiore controllo. Ad esempio, possiamo verificare la presenza di un'intestazione esistente o il tipo di contenuto:

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

Fino ad ora, abbiamo fornito gestori in stile funzionale. Possiamo anche usare la classe Service che consente di scrivere gestori in modo più sofisticato.

Quindi, creiamo prima un modello per l'oggetto con cui stiamo lavorando, la classe Book :

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

Possiamo creare servizi REST per la classe Book implementando il metodo Service.update () . Ciò consente di configurare i sottopercorsi della stessa risorsa:

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

Abbiamo anche configurato il tipo di supporto come JSON, quindi abbiamo bisogno della dipendenza helidon-webserver-json per questo scopo:

 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 questo articolo, abbiamo esplorato i componenti principali di Helidon, mostrando anche come configurare Helidon SE e MP. Poiché Helidon MP è solo un runtime di Eclipse MicroProfile, possiamo eseguire qualsiasi microservizio basato su MicroProfile esistente che lo utilizza.

Come sempre, il codice di tutti gli esempi sopra può essere trovato su GitHub.