Un serveur HTTP peut utiliser la négociation de contenu pour répondre à différents clients. Les clients modernes attendent généralement une réponse JSON. Parfois, un format différent est requis, tel que XML pour les clients plus anciens ou binaire pour les plus récents. Le processus utilisé pour gérer ce défi, ainsi que d’autres comme la gestion de nombreuses langues et même la compression des requêtes HTTP, est connu sous le nom de négociation de contenu.
Dans cet article, nous allons parcourir le processus de création d’une petite application Java MicroProfile tout en expliquant le fonctionnement de la négociation de contenu.
Conditions préalables
Si vous voulez aller de l’avant, la source de cet exemple se trouve sur GitHub.
Qu’est-ce que MicroProfile ?
Vers 2016, les spécifications Java EE avaient commencé à stagner et souffraient de longs cycles de publication ; en conséquence, les nouvelles API de services Web prenaient du retard. Créé pour stimuler l’innovation, le nouveau projet MicroProfile répondait au besoin de gérer les API Java JAX-RS, CDI et JSON-P existantes, ainsi que d’autres nouvelles API pour le monde en évolution des micro-services.
Avance rapide jusqu’à aujourd’hui, Java EE a déménagé à Jakarta EE sous la fondation Eclipse et de nombreux projets MicroProfile sont devenus des projets Jakarta EE.
Certains projets modifient les packages racine au fur et à mesure qu’ils se déplacent entre les projets. Tout ce qui commence par javax.* peut avoir été déplacé vers jakarta.* ou org.eclipse.*.
Qu’est-ce que la négociation de contenu ?
Dans la négociation de contenu pilotée par le serveur, le client envoie une requête à un serveur avec des instructions sur le type de réponse qu’il peut gérer. Lorsque cela est possible, le serveur répond avec le format approprié ou renvoie un 406
ou 415
code d’état.
À un niveau élevé, la conversation ressemble à ceci :
Client:
Salut serveur !
j’aimerais regarder https://api.example.com/user/123
.
J’ai besoin de votre réponse en JSON.
Serveur:
Client sans problème ! Voici cette réponse…
Un autre exemple plus détaillé pourrait être :
Client:
Salut serveur ! j’ai besoin de https://api.example.com/user/123
, de préférence en JSON !
Mais, je vais prendre XML si c’est tout ce que vous avez.
J’ai aussi besoin de l’info en anglais ou en français.
Oh, veuillez également compresser le contenu.
GET /user/123 HTTP/1.1
Accept: application/json,application/xml;q=0.9
Accept-Encoding: gzip
Accept-Language: en,fr
Host: api.example.com
User-Agent: Client/2.0
Serveur:
Hé cliente !
Tout ce que j’ai c’est XML (désolé), la réponse est en anglais,
et j’ai pu le zip; Voici…
HTTP/1.1 200 OK
Content-Type: application/xml
Content-Encoding: gzip
Content-Language: en
<user id="123">
...
</user>
J’aimerais profiter de cette occasion pour m’excuser d’avoir utilisé XML dans cet exemple
La négociation de contenu pilotée par agent fonctionne différemment. Dans ce cas, le client doit déjà connaître les capacités du serveur ou doit les déterminer en adressant une requête au serveur. Cette communication n’est pas standardisée.
En-têtes de négociation de contenu
Dans l’exemple précédent, j’ai utilisé les trois en-têtes de négociation de contenu :
-
Accept
– La liste des types de médias pris en charge par le client. -
Accept-Encoding
– La liste des algorithmes de compression pris en charge par le client. -
Accept-Language
– La liste des langues prises en charge par le client.
Ces en-têtes « accept » permettent une « valeur de qualité » ou une facteur q pour définir les préférences du client. En cas d’omission, la valeur par défaut est 1.0
. L’exemple ci-dessus de application/json,application/xml;q=0.9
indique au serveur que JSON est préféré, mais XML serait le prochain choix.
La réponse du serveur contient des en-têtes pour indiquer au client quelles options ont été sélectionnées.
-
Content-Type
– Le type de média contenu dans la réponse. -
Content-Encoding
– L’algorithme de compression utilisé. -
Content-Language
– La langue de la réponse.
Lorsque j’examine la demande que mon navigateur a faite pour afficher cet article de blog, cela ressemble à ceci :
J’accepte |
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/échange-signé;v=b3;q=0.9 |
Accepter-Encodage |
gzip, dégonfler, br |
Accepter-Langue |
en-US, en; q = 0.9 |
Mon navigateur dit au serveur : je prends n’importe quoi, mais voici une liste ordonnée de ce que je préfère :
-
HTML, XHTML, AVIF, WebP ou APNG
-
XML ou un échange signé
-
Rien d’autre.
Le navigateur prend en charge gzip
, deflate
, et br
compression. Enfin, ma préférence linguistique est l’anglais américain (comme dans la couleur orthographiée sans « u ») ou n’importe quel anglais.
Il y a un inconvénient. Ces en-têtes peuvent également être utilisés pour le suivi du navigateur et l’empreinte digitale.
Assez d’introduction, passons au code !
Créer un projet MicroProfile avec Quarkus
La plupart du code ci-dessous doit être indépendant du fournisseur, sauf indication contraire. Essayez l’exemple avec votre fournisseur MicroProfile préféré et dites-moi comment cela s’est passé dans les commentaires !
Créer un nouveau projet :
mvn io.quarkus:quarkus-maven-plugin:2.0.2.Final:create
-DprojectGroupId=com.example
-DprojectArtifactId=content-negotation
-DclassName="com.example.DiceResource"
-Dextensions="resteasy-jsonb"
-Dpath="/roll"
cd content-negotation
Si vous ne l’avez pas deviné, cet exemple va évaluer la notation des dés. La bibliothèque Dice Notation Parser évaluera les expressions de dés telles que 2d8+2
et renvoie les résultats. Ouvrez votre pom.xml
fichier et ajoutez le dice-parser
dépendance:
<dependency>
<groupId>dev.diceroll</groupId>
<artifactId>dice-parser</artifactId>
<version>0.1.0</version>
</dependency>
Mettre à jour le DiceResource
classe dans src/main/java/com/example/resources
d’ajouter un GET
méthode:
package com.example.resources;
import dev.diceroll.parser.ParseException;
import dev.diceroll.parser.ResultTree;
import javax.ws.rs.*;
import javax.ws.rs.core.*; // wildcard for brevity
import static dev.diceroll.parser.Dice.detailedRoll;
@Path("/roll")
public class DiceResource {
@GET
public ResultTree rollObject(@QueryParam("dice") String dice) throws ParseException {
return detailedRoll(dice);
}
}
Démarrez le serveur depuis votre IDE préféré ou depuis la ligne de commande avec :
Toutes les modifications de code que vous apporterez devrait rechargement à chaud, mais si vous ne voyez pas les changements au fur et à mesure que vous continuez, arrêtez simplement le processus et redémarrez-le.
Assurez-vous que les choses fonctionnent en faisant une demande et lancez un seul dé à six faces.
HTTP/1.1 200 OK
Content-Length: 148
Content-Type: application/json
{
"expression": {
"numberOfDice": 1,
"numberOfFaces": 6
},
"results": [{
"expression": {
"numberOfDice": 1,
"numberOfFaces": 6
},
"results": [],
"value": 6
}],
"value": 6
}
Maintenant que l’application fonctionne, modifions certaines choses et regardons comment la réponse change !
Activer la compression pour les ressources REST
La compression n’est pas l’une de ces fonctionnalités dont vous devriez vous soucier. Tu pourrait gérez vous-même la logique de compression, mais la plupart des fournisseurs ont une propriété de configuration que vous pouvez modifier pour l’activer.
Pour Quarkus, ajoutez la ligne suivante à votre src/main/resources/application.properties
:
quarkus.http.enable-compression=true
Faites une autre requête HTTP et incluez le Accept-Encoding
en-tête, et cette fois nous allons rouler 2d6
:
http :8080/roll dice==2d6 "Accept-Encoding: gzip"
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip
Content-Length: 106
{
"expression": {
"numberOfDice": 2,
"numberOfFaces": 6
},
...
"value": 9
}
L’en-tête Content-Encoding de la réponse a été défini (HTTPie a automatiquement décompressé la demande)
Si vous vouliez faire la même chose avec curl
vous auriez besoin de rediriger le résultat vers gunzip :
curl localhost:8080/roll?dice=2d6 -H "Accept-Encoding: gzip" | gunzip
Utilisation de l’en-tête Request Accept-Language
Nous sommes en 2021 et la mise en œuvre de l’internationalisation (i18n) est encore difficile. Dans le monde Java, i18n signifie généralement créer un Locale
objet contenant la langue de l’utilisateur. C’est un peu maladroit à utiliser, mais l’API JAX-RS définit un moyen de résoudre le problème de l’utilisateur Locale
. Regardons quelques façons différentes de gérer cela.
Ajouter une nouvelle méthode de point de terminaison dans DiceResource
:
@Path("/lang")
@GET
public Response getLang(@Context Request request) {
List<Variant> variants = Variant.VariantListBuilder.newInstance()
.languages(Locale.ENGLISH, Locale.GERMAN)
.build();
Variant variant = request.selectVariant(variants);
if (variant == null) {
return Response.notAcceptable(variants).build();
}
// set the response header, to the client knows which language was selected
String lang = variant.getLanguageString();
return Response.ok(lang)
.header(HttpHeaders.CONTENT_LANGUAGE, lang)
.build();
}
- Utilisez le générateur de liste de variantes pour sélectionner la langue dans l’en-tête « Accepter les langues ».
- Vous devrez lister toutes vos langues prises en charge ; l’ordre est important ; l’option par défaut est la première s’il n’y a pas d’en-tête Accept-Language.
- Utilisez la demande pour sélectionner la bonne variante.
- Si la variante sélectionnée est nulle, retournez un 406.
- Créez et renvoyez une réponse 200.
Le VariantBuilder prend également en charge différents encodages et MediaType.
Essaye le! Faire une demande à /roll/lang
:
http :8080/roll/lang "Accept-Language: de"
HTTP/1.1 200 OK
Content-Language: de
Content-Type: text/plain;charset=UTF-8
Vary: Accept-Language
content-encoding: gzip
content-length: 28
de
- Noter la
Content-Language
entête.
L’approche ci-dessus fonctionne bien pour montrer l’API, mais elle est un peu limitée dans les utilisations réelles car chaque méthode de point de terminaison renvoie un Response
et gère directement les en-têtes. Il serait plus agréable d’extraire cette préoccupation transversale.
Une autre option consiste à utiliser des filtres de demande et de réponse. L’exemple suivant implémente les deux ContainerRequestFilter
et ContainerResponseFilter
interfaces. Créer une nouvelle classe LanguageFilter
:
package com.example;
import javax.ws.rs.container.*;
import javax.ws.rs.core.*; // wildcard for brevity
import javax.ws.rs.ext.Provider;
import java.util.List;
import java.util.Locale;
@Provider
public class LanguageFilter implements ContainerRequestFilter, ContainerResponseFilter {
final private static String LANG = "LanguageFilter.lang";
final public static List<Variant> VARIANTS = Variant.VariantListBuilder.newInstance()
.languages(Locale.ENGLISH, Locale.GERMAN)
.build();
@Override
public void filter(ContainerRequestContext requestContext) {
Variant variant = requestContext.getRequest().selectVariant(VARIANTS);
if (variant == null) {
// Error, respond with 406
requestContext.abortWith(Response.notAcceptable(VARIANTS).build());
} else {
// keep the resolved lang around for the response
requestContext.setProperty(LANG, variant.getLanguageString());
}
}
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
String lang = (String) requestContext.getProperty(LANG);
responseContext.getHeaders().putSingle(HttpHeaders.CONTENT_LANGUAGE, lang);
}
}
- Définissez les langues prises en charge.
- Sélectionnez la variante en fonction de la demande.
- Si un…