Notre métier vit une période de migration massive de ses infrastructures vers le Cloud. Un monde ou les services doivent être en toutes circonstances disponibles. Les applications doivent être stateless, scalables, légères et rapides à lancer.
Spring Boot est le premier à avoir tiré l’épingle de son jeu, sachez cependant qu’il n’est pas le seul sur le marché, bien au contraire.
Fin 2016 la fondation Eclipse a lancé l’initiative MicroProfile autour de deux idées :
- Définir un sous ensemble de spécifications JakartaEE pour proposer un serveur plus léger
- Définir de nouvelles spécifications pour une meilleure intégration au cloud
Voici le détail de la dernière version :
Vous retrouverez l’ensemble des spécifications sur le compte Github de la Fondation Eclipse. A noter qu’elle dispose de son propre système de validation de spécifications, l’Eclipse Foundation Specification Process (EFSP). Une nouvelle release est publiée tous les trimestres. L’objectif est que ces composants fassent l’objet d’une JCP (Java Community Process) pour intégrer les JSR (Java Specification Requests).
Mais de quoi est composé une spécification Eclipse Microprofile ?
- La spécification sous forme asciidoc
- L’API, ensemble d’interfaces / énumérations Java permettant de définir les fonctionnalités à implémenter
- Suite de tests appelée TCK (Technology Compatibility Kit) permettant de valider une implémentation
N’hésitez pas à y jeter un coup d’œil, les spécifications sont de véritables mines d’or. Elles décrivent et détaillent l’ensemble des fonctionnalités qui la compose. En les consultant vous y découvrirez peut être une feature qui vous sera utile !
N’oublions pas qu’il s’agit de spécifications, vous ne trouverez pas d’implémentation dans ce type de repos.
Faisons un petit tour du propriétaire
J’ai découpé les spécifications en 3 catégories.
1) Les composants JakartaEE
Il s’agit de composants repris des spécifications JakartaEE, ils forment les fondations de notre MicroProfile (n’hésitez pas à lire l’article JakartaEE sur notre blog).
CDI
Context and Dependency Injection est le mécanisme central sur lequel se repose bon nombre de composants. Ce système permet de limiter le couplage entre les objets en facilitant le remplacement des implémentations. La plupart de ses fonctionnalités s’effectuent en phase de compilation.
@Produces
@ApplicationScoped
public static ObjectMapper creerMapper() {
return new ObjectMapper();
}
...
@Inject
FishService fishService;
@Inject
ObjectMapper objectMapper;
JAX-RS
Je ne m’attarderai pas sur la spécification standardisant les applications Rest bien connue de tous. Il s’agit certainement avec CDI de la spécification la plus fournie en fonctionnalités.
JSON-P
Spécification permettant de faciliter la manipulation de json (parsing, création, transformation, recherche).
JsonBuilderFactory bf = Json.createBuilderFactory(null)
JsonArray array = bf.createArrayBuilder()
.add(bf.createObjectBuilder()
.add("id", 1)
.add("name", "Cichlidae")
.add("water_type", 0))
.add(bf.createObjectBuilder()
.add("id", 2)
.add("name", "Cyprinidae")
.add("water_type", 0))
.build();
JSON-B
Cette spécification permet de définir la façon de mapper un objet java en json et vice versa.
public class Fish {
public long id;
public String name;
public WaterType waterType;
}
Jsonb jsonb = JsonbBuilder.create();
Fish maurice = new Fish();
maurice.id = 1;
maurice.name = "Maurice";
maurice.waterType= WaterType.FRESH;
String mauriceJson = jsonb.toJson(maurice);
System.out.println(mauriceJson); // {"id":1,"name":"Maurice","waterType": 1}
Spécifications utilitaires
Ces spécifications ne sont pas indispensables au bon fonctionnement de votre projet mais elles vous faciliteront la vie.
Open API
Cette spécification standardise la documentation des API, c’est sur ce standard que repose Swagger.
@GET
@Path("/{fishname}")
@Operation(summary = "Get fish by fish name")
@APIResponse(description = "The fish",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Fish.class))),
@APIResponse(responseCode = "400", description = "Fish not found")
public Response getFishByName(
@Parameter(description = "The name that needs to be fetched. Use fish1 for testing. ", required = true) @PathParam("fishname") String fishname)
GET /openapi
/fish/{fishname}:
get:
summary: Get fish by fish name
operationId: getFishByName
parameters:
- name: fishname
in: path
description: 'The name that needs to be fetched. Use fish1 for testing. '
required: true
schema:
type: string
responses:
default:
description: The fish
content:
application/json:
schema:
$ref: '#/components/schemas/Fish'
400:
description: Fish not found
Rest Client
Cette spécification particulièrement riche standardise les clients Rest. La fonctionnalité principale de cette API est de pouvoir créer un proxy de service Rest.
@Path("/fishs")
@Produces("application/json")
@Consumes("application/json")
public interface FishServiceClient {
@GET
@Path("family/{fishFamily}")
Response countByFamily(@PathParam("fishFamily") String fishFamily);
@PUT
Response updateFish(@BeanParam PutFish putFish);
}
public class PutFish {
@HeaderParam("Authorization")
private String authorization;
@PathParam("fishId")
private String fishId;
// getters, setters, constructors omitted
}
Fault tolerance
Cette spécification permet de rendre votre application plus résiliente en cas de perte de dépendances externes (service, database, etc).
Prenons l’exemple d’une application affichant le détail d’une fiche d’un poisson. Son contenu doit être affiché même sous forme dégradé. Une partie de ces informations provient d’un service externe qui devient brusquement indisponible.
@Retry(maxRetries = 2)
@Fallback(fallbackMethod= "fallbackForFindFamilyName")
public String findFamilyName(String fishName) {
return fishServiceClient.findFamilyName(fishName);
}
private String fallbackForFindFamilyName() {
return "Missing information";
}
JWT Propagation
Json Web Token Propagation nous aide à récupérer les informations sur le Principal du contexte. Il suffit juste d’utiliser des annotations via son intégration à CDI (header, claim et signature).
@Inject
private JsonWebToken callerPrincipal;
@Inject
@Claim("raw_token")
private String rawToken;
@Inject
@Claim("groups")
private Set groups;
@Inject
@Claim("exp")
private long expiration;
@Inject
@Claim("sub")
private String subject;
Open tracing
Cette spécification permet de standardiser la manière de tracer les appels dans un contexte distribué. Ces flux pourront être exploités par des outils tels que Jaeger ou Zipkin. Cette fonctionnalité ne nécessite pas d’activation particulière si vous disposez d’une application JAX-RS. Dans le cas contraire il est possible d’utiliser l’annotation @Traced.
Spécifications permettant l’intégration à un environnement Cloud
Microprofile Config
C’est l’une des spécifications que je trouve la plus intéressante, elle standardise le chargement de configurations. Elle est capable de récupérer ces informations depuis plusieurs sources, fichiers, paramètres de lancement du serveur, variables d’environnements etc. Il existe des implémentations vers ZooKeeper Consule ou encore Etcd.
Prenons l’exemple d’une variable d’environnement appelée $SHOP_NAME=FishParadise, que vous souhaitez exploiter dans votre application.
@Inject @ConfigProperty(name="SHOP_NAME", defaultValue="FishMarket")
String shopName
A noter que le fichier de configuration par défaut pour un Microprofile Eclipse est le fichier microprofile-config.properties.
Microprofile Health
Cette spécification permet de sonder l’état d’une application. Elle expose plusieurs endpoints permettant de vérifier que votre application est lancée (Liveness), ou prête à fournir son service (Readyness). Il est possible d’ajouter soi-même ses vérifications en les implémentant.
GET /health/live
{
"status" : "UP",
"checks" : [ {
"name" : "DataSourceHealthCheck",
"status" : "UP"
} ]
}
Microprofile Metric
Spécification permettant de récupérer des statistiques sur l’usage de l’application. Ces métriques sont divisées en 3 catégories Base, Application, Vendor. Elles sont accessibles via l’url /metrics/[Base|Vendor|Application]. Vous y trouverez des informations sur l’usage du CPU, l’état de la jvm ou encore sur votre pool http. Il est également possible d’ajouter vos propres métriques métier.
La spécification fournit de nombreuses annotations @Counted @ConcurrentGauge @Gauge @Metered @Metric @Timed @SimplyTimed. Elles pourront être appliquées selon les cas, à des méthodes, types ou classes.
@Gauge(unit = MetricUnits.NONE, name = "fishQueueSize")
public int getFishQueueSize() {
return fishQueue.size;
}
GET /metrics
# TYPE base_jvm_uptime_seconds gauge
# HELP base_jvm_uptime_seconds Displays the start time of the Java virtual machine in milliseconds. This attribute displays the approximate time when the Java virtual machine started.
base_jvm_uptime_seconds{environment="dev",instanceId="9afb9093-e0a9-4518-8b66-852f6b9c310a",serviceName="UNKNOWN",serviceVersion="1.0.0"} 315.14300000000003
# TYPE base_classloader_loadedClasses_total_total counter
# HELP base_classloader_loadedClasses_total_total Displays the total number of classes that have been loaded since the Java virtual machine has started execution.
base_classloader_loadedClasses_total_total{environment="dev",instanceId="9afb9093-e0a9-4518-8b66-852f6b9c310a",serviceName="UNKNOWN",serviceVersion="1.0.0"} 12580
# TYPE base_thread_count gauge
# HELP base_thread_count Displays the current number of live threads including both daemon and non-daemon threads
base_thread_count{environment="dev",instanceId="9afb9093-e0a9-4518-8b66-852f6b9c310a",serviceName="UNKNOWN",serviceVersion="1.0.0"} 28
# TYPE base_memory_committedHeap_bytes gauge
# HELP base_memory_committedHeap_bytes Displays the amount of memory in bytes that is committed for the Java virtual machine to use. This amount of memory is guaranteed for the Java virtual machine to use.
base_memory_committedHeap_bytes{environment="dev",instanceId="9afb9093-e0a9-4518-8b66-852f6b9c310a",serviceName="UNKNOWN",serviceVersion="1.0.0"} 3.16669952E8
# TYPE base_gc_total_total counter
# HELP base_gc_total_total Displays the total number of collections that have occurred. This attribute lists -1 if the collection count is undefined for this collector.
base_gc_total_total{environment="dev",instanceId="9afb9093-e0a9-4518-8b66-852f6b9c310a",name="G1-Young-Generation",serviceName="UNKNOWN",serviceVersion="1.0.0"} 6
# TYPE base_gc_total_total counter
# HELP base_gc_total_total Displays the total number of collections that have occurred. This attribute lists -1 if the collection count is undefined for this collector.
base_gc_total_total{environment="dev",instanceId="9afb9093-e0a9-4518-8b66-852f6b9c310a",name="G1-Old-Generation",serviceName="UNKNOWN",serviceVersion="1.0.0"} 0
C’est bien beau de voir les spécifications, mais sans implémentations ça ne fonctionne pas !
Il en existe de nombreuses fournies par Oracle, RedHat, IBM etc. Je vous propose de jeter un coup d’œil sur l’implémentation la plus utilisée (licence apache) :
Mais quels serveurs sont compatibles ?
Voici un web starter permettant de créer votre projet MicroProfile Eclipse.
Vous y trouverez des noms bien connus tel que Wildfly et OpenLiberty TomEE et des petits nouveaux comme Payara, KumuluzEE, Helidon et Quarkus.
Un peu de pratique
J’ai choisi pour ce projet d’utiliser KumuluzEE . Il dispose d’une bonne documentation, de nombreuses implémentations et le support est particulièrement réactif.
Structure du projet:
C:\USERS\BMEYNIER\IDEAPROJECTS\MICROPROFILE-ARTICLE
├───.idea
│ └───libraries
└───src
├───assembly
│ └───conf
│ └───local
│ └───META-INF
├───doc
│ └───postman
└───main
├───docker
│ ├───app
│ └───db
│ ├───init
│ └───scripts
├───filters
├───java
│ └───com
│ └───bmeynier
│ └───article
│ └───microprofile
│ ├───domain
│ │ └───enums
│ ├───repository
│ └───rest
│ └───producer
├───k3s
└───resources
├───META-INF
└───WEB-INF
Configuration du pom
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.eclipse.microprofile</groupId>
<artifactId>microprofile</artifactId>
<version>${microprofile.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Import des specifications Microprofile
A cela il faut ajouter dans le dépendencyManagement le bom contenant toutes les implémentations fournies par KumuluzEE.
<dependency>
<groupId>com.kumuluz.ee</groupId>
<artifactId>kumuluzee-bom</artifactId>
<version>${kumuluzee.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Il faut ensuite faire ses courses en choisissant les implémentations dont vous avez besoin.
<dependency>
<groupId>com.kumuluz.ee</groupId>
<artifactId>kumuluzee-servlet-jetty</artifactId>
</dependency>
<dependency>
<groupId>com.kumuluz.ee</groupId>
<artifactId>kumuluzee-jpa-hibernate</artifactId>
</dependency>
Jetty en guise de server http et Hibernate pour le JPA
Les MicroProfiles sont très flexibles, j’aurai très bien pu choisir d’autres implémentations telles que Undertow et Eclipse-Link.
Microprofile.properties
kumuluzee.datasources[0].jndi-name = jdbc/FishsDS
kumuluzee.datasources[0].connection-url = ${KUMULUZEE_DATASOURCES0_CONNECTIONURL}
kumuluzee.datasources[0].driver-name = ${KUMULUZEE_DATASOURCES0_DRIVER}
kumuluzee.datasources[0].username = ${KUMULUZEE_DATASOURCES0_USERNAME}
kumuluzee.datasources[0].password = ${KUMULUZEE_DATASOURCES0_PASSWORD}
kumuluzee.datasources[0].pool.max-size = 20
kumuluzee.health.checks.data-source-health-check =
kumuluzee.health.checks.data-source-health-check.type = readiness
kumuluzee.health.checks.data-source-health-check.jndi-name = jdbc/FishsDS
kumuluzee.health.checks.disk-space-health-check =
kumuluzee.health.checks.disk-space-health-check.type = liveness
kumuluzee.health.checks.disk-space-health-check.threshold = 10000
Taille du jar:
[baptiste@BMEYNIER Microprofile-Article]$ ls -alhs ./target/
total 30M
4,0K drwxr-xr-x. 7 baptiste baptiste 4,0K 30 avril 07:40 .
4,0K drwxr-xr-x. 6 baptiste baptiste 4,0K 30 avril 07:44 ..
30M -rw-r--r--. 1 baptiste baptiste 30M 30 avril 07:40 microprofile-article-1.0.0-SNAPSHOT.jar
16K -rw-r--r--. 1 baptiste baptiste 15K 30 avril 07:40 microprofile-article-1.0.0-SNAPSHOT.jar.original
Vous trouverez dans ce jar de 30Mo votre code source ainsi que toutes les dépendances du serveur, il est exécutable tel quel.
Pour avoir une idée claire de ce que vous embarquez je vous conseille vivement de lancer la commande mvn dependency:tree et d’ouvrir le contenu du jar.
Temps de lancement:
[baptiste@BMEYNIER Microprofile-Article]$ $JAVA_11/bin/java -jar ./target/microprofile-article-1.0.0-SNAPSHOT.jar
2020-04-30 07:34:36.628 INFO -- com.kumuluz.ee.configuration.sources.FileConfigurationSource -- Loading configuration from .properties file: META-INF/microprofile-config.properties
2020-04-30 07:34:36.644 INFO -- com.kumuluz.ee.configuration.sources.FileConfigurationSource -- Configuration successfully read.
2020-04-30 07:34:36.645 INFO -- com.kumuluz.ee.EeApplication -- Initialized configuration source: EnvironmentConfigurationSource
2020-04-30 07:34:36.645 INFO -- com.kumuluz.ee.EeApplication -- Initialized configuration source: SystemPropertyConfigurationSource
2020-04-30 07:34:36.646 INFO -- com.kumuluz.ee.EeApplication -- Initialized configuration source: FileConfigurationSource
2020-04-30 07:34:36.647 INFO -- com.kumuluz.ee.EeApplication -- Initializing KumuluzEE
2020-04-30 07:34:36.647 INFO -- com.kumuluz.ee.EeApplication -- Checking for requirements
2020-04-30 07:34:36.648 INFO -- com.kumuluz.ee.EeApplication -- KumuluzEE running inside a JAR runtime.
[...]
2020-04-30 07:34:39.944 INFO -- org.eclipse.jetty.server.AbstractConnector -- Started ServerConnector@45cd8607{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2020-04-30 07:34:39.944 INFO -- org.eclipse.jetty.server.Server -- Started @3776ms
2020-04-30 07:34:39.944 INFO -- com.kumuluz.ee.EeApplication -- KumuluzEE started successfully
Consommation mémoire:
De 35 à 150 Mo (simulation réalisée avec activité modérée) avec comme limite -Xmx256m.
Et si on mettait tout ça dans le Cloud ?
Pour cet exemple j’ai installé une version allégée de Kubernetes sur mon poste, K3s de Rancher.
Nous définirons deux services :
- Une base de données de type H2
- Notre application KumuluzEE
La base de données
apiVersion: apps/v1
kind: Deployment
metadata:
name: fishs-database
namespace: default
spec:
selector:
matchLabels:
app: fishs-database
template:
metadata:
labels:
app: fishs-database
spec:
initContainers:
- name: init-database
image: bmeynier/microprofile/k3s-init-database:v1.0.0
command: ['sh', '-c', '/tmp/createDatabase.sh']
envFrom:
- secretRef:
name: k3s-fishs-secret
volumeMounts:
- name: database
mountPath: /database
containers:
- name: fishs-database
image: bmeynier/microprofile/k3s-database:v1.0.0
volumeMounts:
- name: database
mountPath: /database
ports:
- containerPort: 1521
volumes:
- emptyDir: {}
name: database
---
apiVersion: v1
kind: Service
metadata:
name: fishs-database
namespace: default
spec:
selector:
app: fishs-database
ports:
- port: 1521
targetPort: 1521
La base de données est initialisée à l’aide d’un init-container. A noter que le volume est partagé entre l’init-container et le container.
L’application KumuluzEE
apiVersion: apps/v1
kind: Deployment
metadata:
name: fishs-application
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: fishs-application
template:
metadata:
labels:
app: fishs-application
spec:
containers:
- name: fishs-application
image: bmeynier/microprofile/k3s-application:v1.0.0
envFrom:
- configMapRef:
name: k3s-fishs-config
- secretRef:
name: k3s-fishs-secret
# system probes
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 1
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 1
resources:
limits:
memory: 256Mi
cpu: 500m
requests:
memory: 64Mi
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: fishs-application
namespace: default
spec:
selector:
app: fishs-application
ports:
- port: 8080
targetPort: 8080
type: LoadBalancer
Vous remarquerez l’usage de ConfigMap et de Secret. Ces objets Kubernetes seront injectés en tant que variables d’environnement lors du lancement de mon Pod. Je pourrai ensuite les exploiter grâce à MicroProfile Config.
Le second point intéressant concerne la définition des Healths [Live|Ready] qui intéragissent avec MicroProfile Health.
Le composant Microprofile Metric permet de scaler de manière horizontale nos services, nous en parlerons dans un prochain article.
[baptiste@DESKTOP-BMEYNIER Microprofile-Article]$ sudo k3s kubectl get all
[sudo] Mot de passe de baptiste :
NAME READY STATUS RESTARTS AGE
pod/fishs-database-dbc9d98d-984ms 1/1 Running 0 73m
pod/svclb-fishs-application-25tlg 1/1 Running 0 17m
pod/fishs-application-764f69b78d-k7sqx 1/1 Running 0 17m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 3h41m
service/fishs-database ClusterIP 10.43.188.110 <none> 1521/TCP 73m
service/fishs-application LoadBalancer 10.43.229.234 X.X.X.X 8080:32158/TCP 17m
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/svclb-fishs-application 1 1 1 1 1 <none> 17m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/fishs-database 1/1 1 1 73m
deployment.apps/fishs-application 1/1 1 1 17m
NAME DESIRED CURRENT READY AGE
replicaset.apps/fishs-database-dbc9d98d 1 1 1 73m
replicaset.apps/fishs-application-764f69b78d 1 1 1 17m
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
horizontalpodautoscaler.autoscaling/fishs-application Deployment/fishs-application <unknown>/1k, 0%/50% 1 3 1 16m
Retrouvez le code source sur Gitlab !
Conclusion
- Il existe un large choix de serveurs
- Une nouvelle release sort tous les trimestres
- Ces serveurs sont flexibles, légers et rapides !
- Il est toujours possible de surcharger le comportement d’un composants, il ne s’agit pas d’une boite noire
- Il sont parfaitement intégrés aux environnements cloud