Introduction à Spring Cloud Function

Ce que c'est et quelques petits hello world pour faire connaissance...


Cet article fait partie d’une série traitant de Spring Cloud Function:

  1. Introduction à Spring Cloud Function
  2. Tests avec Spring Cloud Function
  3. Spring Cloud Function et Déploiement Web
  4. Spring Cloud Function et Spring Cloud Stream

Introduction

Cet article est une petite introduction au projet Spring Cloud Function. Celui-ci peut être utilisé en vue de découpler la logique métier des applications de leur lieu d’exécution.

L’intérêt est de pouvoir utiliser le même code et de le déployer dans des environnements différents: un serveur web, une infrastructure de messagerie ou en tant que batch par exemple.

Ce que c’est

Tout besoin peut normalement s’exprimer par une fonction correspondant qui:

  • soit consomme des données en entrée
  • soit produit des données en sortie
  • soit consomme et produit des données en entrée et en sortie

En Java, chacune de ces stratégies correspond à une interface: Consumer, Supplier, Function.

Spring Cloud Function est un framework basé sur Spring Boot capable de détecter et de manipuler des implémentations explicites ou non de ces interfaces.

Fini les bla-bla, passons tout de suite à la pratique.

Quelques petits exemples hello world pour illustrer tout ça

Pour apprendre les bases du framework, on va interagir avec une abstraction de type FunctionCatalog qu’il est possible de récupérer depuis un contexte Spring. Cette abstraction sert d’annuaire de fonctions et permet, entre autres, de composer les fonctions ou d’effectuer des conversions de type de façon transparente.

Basique

Créons une petite application Spring Boot pour jouer un peu avec le framework. Mon exemple s’appuie sur Java 11, Maven et Spring Boot 2.6.2 mais il est possible de l’adapter avec d’autres versions ou d’utiliser Kotlin ou Gradle à la place.

Voici le contenu de mon fichier pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>spring-cloud-function-hello</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-cloud-function-hello</name>
	<description>spring-cloud-function-hello</description>
	<properties>
		<java.version>11</java.version>
		<spring-cloud.version>2021.0.0</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-function-context</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

Il n’y a qu’une seule classe, celle qui contient la méthode main:

package com.example.springcloudfunctionhello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.context.annotation.Bean;

import java.util.function.Function;

@SpringBootApplication
public class SpringCloudFunctionHelloApplication {

	public static void main(String[] args) {
		var context = SpringApplication.run(SpringCloudFunctionHelloApplication.class, args);
		var catalog = context.getBean(FunctionCatalog.class);
		Function<String, String> hello = catalog.lookup("hello");
		System.out.println(hello.apply("world"));
	}

	@Bean
	public Function<String, String> hello() {
		return input -> "Hello, " + input;
	}

}

Le code expose la fonction hello en tant que bean spring, ce qui permet de la retrouver dans le contexte à l’aide du bean FunctionCatalog puis de l’exécuter en appelant la méthode apply.

Si vous lancez, l’application, vous devriez voir apparaître dans le terminal les lignes suivantes:

Hello, world

Process finished with exit code 0

Dans notre code, on spécifie le nom de la fonction à retrouver dans le catalogue mais par convention, si l’application ne contient qu’un seul bean qui soit une Function (ou Supplier ou Consumer), le catalogue retournera cette fonction par défaut, quelle que soit la valeur passée à la méthode lookup.

Concrètement, cela veut dire que si on écrivait:

Function<String, String> hello = catalog.lookup("");
System.out.println(hello.apply("world"));

ou

Function<String, String> hello = catalog.lookup("foo");
System.out.println(hello.apply("world"));

on obtiendrait exactement le même résultat.

Composition de fonctions

Rajoutons une seconde fonction qui permet de mettre en majuscule la 1ère lettre d’une chaîne de caractères:

@Bean
public Function<String, String> capitalize() {
    return input -> input.substring(0, 1).toUpperCase() + input.substring(1).toLowerCase();
}

Et composons nos deux fonctions grâce au caractère | au moment de l’invocation de la méthode lookup:

Function<String, String> function = catalog.lookup("capitalize|hello");
System.out.println(function.apply("world"));

La sortie devrait afficher la ligne suivante:

Hello, World

Si nous avions écrit hello|capitalize, l’ordre d’appel des fonctions aurait été inversé et nous aurions obtenu:

Hello, world

Conversion de type

Rajoutons une nouvelle fonction capable de dire bonjour à un utilisateur:

@Bean
public Function<User, String> helloUser() {
    return input -> "Hello, " + input.getFirstName();
}

private static class User {
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

Appelons à présent cette fonction:

Function<String, String> function = catalog.lookup("helloUser");
System.out.println(function.apply("{\n" +
        " \"firstName\": \"Jane\",\n" +
        " \"lastName\": \"Doe\"\n" +
        "}"));

En sortie, vous devriez obtenir la ligne suivante:

Hello, Jane

Ce qu’il est intéressant de noter dans cet exemple, c’est le fait que l’on ne passe pas un objet de type User en entrée mais une chaîne de caractères représentant notre objet en JSON: la conversion est automatiquement faite par le framework.

Fonction POJO

Spring Cloud Framework est capable de reconnaître les POJOs ne contenant qu’une méthode publique et de les traiter comme une fonction ainsi que le montre l’exemple suivant:

@Bean
public Hello helloPojo() {
    return new Hello();
}

public static class Hello {
    public String doSomething(String input) {
        return "Hello, " + input + " from POJO";
    }
}

Puis le code suivant dans la méthode main:

Function<String, String> hello = catalog.lookup("helloPojo");
System.out.println(hello.apply("world"));

La sortie affiche le résultat suivant:

Hello, world from POJO

Ce qui est fort, c’est qu’il est possible de composer cette fonction avec une autre qui ne provient pas d’un POJO:

Function<String, String> hello = catalog.lookup("capitalize|helloPojo");
System.out.println(hello.apply("world"));

et ça marche !

Inputs/Outputs multiples

Pour indiquer au framework que plusieurs inputs ou outputs doivent être utilisés, il est nécessaire d’utiliser une des classes TupleN provenant du projet Reactor. Ce n’est probablement pas l’idéal mais c’est sans doute lié aux limitations du langage Java:

@Bean
public Function<Tuple2<String, String>, String> helloFirstNameLastName() {
    return input -> "Hello, " + input.getT1() + " " + input.getT2();
}

Pour exécuter la fonction:

Function<Tuple2<String, String>, String> hello = catalog.lookup("helloFirstNameLastName");
System.out.println(hello.apply(Tuples.of("Jane", "Doe")));

Et en sortie:

Hello, Jane Doe

Programmation Réactive

Comme indiqué au début de l’article, Spring Cloud Function supporte également la programmation réactive. Voici une version réactive de la fonction hello:

@Bean
public Function<Flux<String>, Flux<String>> helloReactive() {
    return input -> input.map(s -> "Hello, " + s);
}

Et une façon de l’utiliser:

Function<Flux<String>, Flux<String>> hello = catalog.lookup("helloReactive");
hello.apply(Flux.just("Jane Doe", "John Doe"))
     .subscribe(System.out::println);

Voici ce que vous devriez voir en sortie:

Hello, Jane Doe
Hello, John Doe

Et là encore, il est possible de combiner fonctions réactives et non-réactives:

Function<Flux<String>, Flux<String>> hello = catalog.lookup("capitalize|helloReactive");
hello.apply(Flux.just("foo", "bar"))
     .subscribe(System.out::println);

La sortie:

Hello, Foo
Hello, Bar

RoutingFunction.FUNCTION_NAME

Il est également possible de spécifier dynamiquement la définition de fonction que l’on souhaite exécuter à l’aide de la propriété spring.cloud.function.definition:

var context = SpringApplication.run(SpringCloudFunctionHelloApplication.class,
                "--spring.cloud.function.definition=capitalize|hello");
var catalog = context.getBean(FunctionCatalog.class);
Function<String, String> hello = catalog.lookup(RoutingFunction.FUNCTION_NAME);
System.out.println(hello.apply("world"));

Et en sortie:

Hello, World

Conclusion

Et voilà pour cette présentation des bases de Spring Cloud Function, j’espère que vous l’aurez trouvée claire !

Dans cet article, je n’ai pas parlé du déploiement des fonctions dans différentes contextes, je garde ça pour un prochain billet.

Liens

  1. Les exemples de l’article
  2. Le site du projet
  3. Présentation à Spring One

© 2023 Du côté de chez Fouad. Tous droits réservés.