API REST en Java sans frameworks

La traduction de l'article a été préparée spécialement pour les étudiants "Développeur Java . "




Il existe de nombreux frameworks et bibliothèques dans l'écosystème Java. Bien qu'ils ne soient pas aussi nombreux qu'en JavaScript, ils n'expirent pas aussi rapidement. Cependant, cela m'a fait penser que nous avions déjà oublié comment écrire des applications sans framework.

Vous pouvez dire que le printemps est la norme et pourquoi réinventer la roue? Et Spark est un cadre REST agréable et pratique. Ou Light-rest-4jis . Et je dirai que vous avez bien sûr raison.

Mais avec le cadre, en plus des fonctionnalités finales, vous obtenez beaucoup de magie, des difficultés d'apprentissage, des fonctions supplémentaires que vous n'utiliserez probablement pas, ainsi que des bugs. Et plus il y a de code tiers dans votre service, plus vous risquez d'avoir des erreurs.

La communauté open source est très active et il y a une forte probabilité que les erreurs dans le framework soient rapidement corrigées. Mais tout de même, je vous exhorte à vous demander si vous avez vraiment besoin d'un cadre. Si vous avez un petit service ou une application console, vous pourrez peut-être vous en passer.

Que pouvez-vous gagner (ou perdre) en utilisant du code Java pur? Pensez-y:

  • votre code peut être beaucoup plus propre et plus compréhensible (ou peut-être dans un désordre complet si vous êtes un mauvais programmeur)
  • vous aurez plus de contrôle sur votre code, vous ne serez pas limité par le framework (même si vous devrez écrire plus de votre code pour les fonctionnalités que le framework fournit hors de la boîte)
  • votre application sera déployée et lancée beaucoup plus rapidement, car le framework n'a pas besoin d'initialiser des dizaines de classes (ou ne démarrera pas du tout si vous mélangez quelque chose, par exemple, en multithreading)
  • si vous déployez l'application dans Docker, vos images seront beaucoup plus petites, car votre pot sera également plus petit

J'ai fait une petite expérience et essayé de développer une API REST sans framework. Cela sera peut-être intéressant en termes d'apprentissage et de rafraîchissement des connaissances.

Lorsque j'ai commencé à écrire ce code, je suis souvent tombé sur des situations où Spring n'avait aucune fonctionnalité prête à l'emploi. À ces moments, au lieu de prendre le printemps, vous deviez repenser et développer tout vous-même.

J'ai réalisé que pour résoudre de vrais problèmes commerciaux, je préfèrerais toujours utiliser Spring plutôt que de réinventer la roue. Cependant, je pense que cet exercice a été une expérience assez intéressante.

Pour commencer


Je décrirai chaque étape, mais je ne donnerai pas toujours le code source complet. Vous pouvez voir le code complet dans des branches distinctes du référentiel git .

Créez d'abord un nouveau projet Maven avec le pom.xml suivant.

 <?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.consulner.httpserver</groupId> <artifactId>pure-java-rest-api</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>11</java.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> </properties> <dependencies></dependencies> </project> 

Ajoutez java.xml.bind comme dépendance car elle a été supprimée dans JDK 11 ( JEP-320 ).

 <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.4.0-b180608.0325</version> </dependency> 

et Jackson pour la sérialisation JSON

 <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.7</version> </dependency> 

Pour simplifier les classes POJO, nous utiliserons Lombok :

 <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.0</version> <scope>provided</scope> </dependency> 

et vavr pour les outils de programmation fonctionnels

 <dependency> <groupId>io.vavr</groupId> <artifactId>vavr</artifactId> <version>0.9.2</version> </dependency> 

Nous créons également l'application principale de classe vide.

Le code source dans la branche step-1 .

Premier critère d'évaluation


Notre application Web sera basée sur la classe com.sun.net.httpserver.HttpServer . Et le point de terminaison /api/hello le plus simple pourrait ressembler à ceci:

 package com.consulner.api; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import com.sun.net.httpserver.HttpServer; class Application { public static void main(String[] args) throws IOException { int serverPort = 8000; HttpServer server = HttpServer.create(new InetSocketAddress(serverPort), 0); server.createContext("/api/hello", (exchange -> { String respText = "Hello!"; exchange.sendResponseHeaders(200, respText.getBytes().length); OutputStream output = exchange.getResponseBody(); output.write(respText.getBytes()); output.flush(); exchange.close(); })); server.setExecutor(null); // creates a default executor server.start(); } } 

Le serveur Web fonctionne sur le port 8000 et fournit un point de terminaison qui renvoie simplement Hello! .. Cela peut être vérifié, par exemple, en utilisant curl:

curl localhost:8000/api/hello

Le code source dans la branche step-2 .

Prise en charge de diverses méthodes HTTP


Notre premier point de terminaison fonctionne bien, mais vous remarquerez peut-être que quelle que soit la méthode HTTP à utiliser, elle répond toujours de la même manière.

Par exemple:

 curl -X POST localhost:8000/api/hello curl -X PUT localhost:8000/api/hello 

La première chose à faire est d'ajouter du code pour distinguer les méthodes, par exemple:

 server.createContext("/api/hello", (exchange -> { if ("GET".equals(exchange.getRequestMethod())) { String respText = "Hello!"; exchange.sendResponseHeaders(200, respText.getBytes().length); OutputStream output = exchange.getResponseBody(); output.write(respText.getBytes()); output.flush(); } else { exchange.sendResponseHeaders(405, -1);// 405 Method Not Allowed } exchange.close(); })); 

Essayez à nouveau la requête:

 curl -v -X POST localhost:8000/api/hello 

la réponse serait quelque chose comme ceci:

 > POST /api/hello HTTP/1.1 > Host: localhost:8000 > User-Agent: curl/7.61.0 > Accept: */* > < HTTP/1.1 405 Method Not Allowed 

Il y a également quelques points à garder à l'esprit. Par exemple, n'oubliez pas de faire flush() pour OutputStream et close() pour exchange . Lorsque j'utilisais Spring, je n'avais même pas besoin d'y penser.

Le code source dans la branche étape-3 .

Analyse des paramètres de demande


L'analyse des paramètres de requête est une autre "fonction" que nous devons implémenter nous-mêmes.

Supposons que nous voulons que notre API Bonjour reçoive un nom dans le paramètre name , par exemple:

 curl localhost:8000/api/hello?name=Marcin Hello Marcin! 

Nous pourrions analyser les paramètres comme suit:

 public static Map<String, List<String>> splitQuery(String query) { if (query == null || "".equals(query)) { return Collections.emptyMap(); } return Pattern.compile("&").splitAsStream(query) .map(s -> Arrays.copyOf(s.split("="), 2)) .collect(groupingBy(s -> decode(s[0]), mapping(s -> decode(s[1]), toList()))); } 

et utilisez comme ci-dessous:

 Map<String, List<String>> params = splitQuery(exchange.getRequestURI().getRawQuery()); String noNameText = "Anonymous"; String name = params.getOrDefault("name", List.of(noNameText)).stream().findFirst().orElse(noNameText); String respText = String.format("Hello %s!", name); 

Exemple complet dans la branche étape-4 .

De même, si nous voulons utiliser des paramètres dans path. Par exemple:

 curl localhost:8000/api/items/1 

Pour obtenir l'élément par id = 1, nous devons analyser l'url nous-mêmes. Ça devient volumineux.

La sécurité


Souvent, nous devons protéger l'accès à certains points de terminaison. Par exemple, cela peut être fait en utilisant l'authentification de base.

Pour chaque HttpContext, nous pouvons définir un authentificateur, comme indiqué ci-dessous:

 HttpContext context = server.createContext("/api/hello", (exchange -> { //     })); context.setAuthenticator(new BasicAuthenticator("myrealm") { @Override public boolean checkCredentials(String user, String pwd) { return user.equals("admin") && pwd.equals("admin"); } }); 

La valeur «myrealm» dans le constructeur BasicAuthenticator est le nom de domaine. Le domaine est un nom virtuel qui peut être utilisé pour séparer les domaines d'authentification.

Vous pouvez en savoir plus à ce sujet dans RFC 1945 .

Vous pouvez maintenant appeler ce point de terminaison sécurisé en ajoutant l'en-tête d' Authorization :

 curl -v localhost:8000/api/hello?name=Marcin -H 'Authorization: Basic YWRtaW46YWRtaW4=' 

Le texte après «Basic» est le texte admin:admin codé en Base64, qui est les informations d'identification codées en dur dans notre exemple.

Pour l'authentification dans une application réelle, vous obtiendrez probablement les informations d'identification de l'en-tête et les comparerez avec le nom d'utilisateur et le mot de passe stockés dans la base de données.

Si vous ne spécifiez pas de titre, l'API répondra avec un statut

 HTTP/1.1 401 Unauthorized 

Exemple complet dans la branche étape-5 .

JSON, gestion des exceptions et plus


Il est maintenant temps pour un exemple plus complexe.

D'après mon expérience dans le développement de logiciels, l'API la plus courante que j'ai développée était l'échange JSON.

Nous allons développer une API pour enregistrer de nouveaux utilisateurs. Pour les stocker, nous utiliserons la base de données en mémoire.

Nous aurons un simple objet de domaine User :

 @Value @Builder public class User { String id; String login; String password; } 

J'utilise Lombok pour me débarrasser du passe-partout (constructeurs, getters).

Dans l'API REST, je souhaite transmettre uniquement le login et le mot de passe, j'ai donc créé un objet distinct:

 @Value @Builder public class NewUser { String login; String password; } 

User objets User sont créés dans le service que nous utiliserons dans le gestionnaire d'API. La méthode de service sauve simplement l'utilisateur.

 public String create(NewUser user) { return userRepository.create(user); } 

Vous pouvez faire plus dans une vraie application. Par exemple, envoyez des événements après une inscription utilisateur réussie.

L'implémentation du référentiel est la suivante:

 import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import com.consulner.domain.user.NewUser; import com.consulner.domain.user.User; import com.consulner.domain.user.UserRepository; public class InMemoryUserRepository implements UserRepository { private static final Map USERS_STORE = new ConcurrentHashMap(); @Override public String create(NewUser newUser) { String id = UUID.randomUUID().toString(); User user = User.builder() .id(id) .login(newUser.getLogin()) .password(newUser.getPassword()) .build(); USERS_STORE.put(newUser.getLogin(), user); return id; } } 

Enfin, collez tout ensemble dans la handle() :

 protected void handle(HttpExchange exchange) throws IOException { if (!exchange.getRequestMethod().equals("POST")) { throw new UnsupportedOperationException(); } RegistrationRequest registerRequest = readRequest(exchange.getRequestBody(), RegistrationRequest.class); NewUser user = NewUser.builder() .login(registerRequest.getLogin()) .password(PasswordEncoder.encode(registerRequest.getPassword())) .build(); String userId = userService.create(user); exchange.getResponseHeaders().set(Constants.CONTENT_TYPE, Constants.APPLICATION_JSON); exchange.sendResponseHeaders(StatusCode.CREATED.getCode(), 0); byte[] response = writeResponse(new RegistrationResponse(userId)); OutputStream responseBody = exchange.getResponseBody(); responseBody.write(response); responseBody.close(); } 

Ici, la demande JSON est convertie en un objet RegistrationRequest :

 @Value class RegistrationRequest { String login; String password; } 

que je NewUser plus tard à l'objet NewUser afin de l'enregistrer dans la base de données et envoyer la réponse en JSON.

J'ai également besoin de reconvertir l'objet RegistrationResponse en une chaîne JSON. Pour cela, nous utilisons Jackson
( com.fasterxml.jackson.databind.ObjectMapper ).

Voici comment je crée un nouveau gestionnaire dans main() :

 public static void main(String[] args) throws IOException { int serverPort = 8000; HttpServer server = HttpServer.create(new InetSocketAddress(serverPort), 0); RegistrationHandler registrationHandler = new RegistrationHandler(getUserService(), getObjectMapper(), getErrorHandler()); server.createContext("/api/users/register", registrationHandler::handle); // here follows the rest.. } 

Un exemple de travail peut être trouvé dans la branche étape-6 . Là, j'ai également ajouté un gestionnaire d'exceptions global pour envoyer des messages d'erreur JSON standard. Par exemple, si la méthode HTTP n'est pas prise en charge ou si la demande à l'API n'est pas correctement formée.

Vous pouvez exécuter l'application et essayer l'une des requêtes suivantes:

  • exemple de demande correcte

 curl -X POST localhost:8000/api/users/register -d '{"login": "test" , "password" : "test"}' 

réponse:

 {"id":"395eab24-1fdd-41ae-b47e-302591e6127e"} 

  • exemple d'erreur

 curl -v -X POST localhost:8000/api/users/register -d '{"wrong": "request"}' 

réponse:

 < HTTP/1.1 400 Bad Request < Date: Sat, 29 Dec 2018 00:11:21 GMT < Transfer-encoding: chunked < Content-type: application/json < * Connection #0 to host localhost left intact {"code":400,"message":"Unrecognized field \"wrong\" (class com.consulner.app.api.user.RegistrationRequest), not marked as ignorable (2 known properties: \"login\", \"password\"])\n at [Source: (sun.net.httpserver.FixedLengthInputStream); line: 1, column: 21] (through reference chain: com.consulner.app.api.user.RegistrationRequest[\"wrong\"])"} 

De plus, j'ai accidentellement rencontré le projet java-express , qui est un équivalent Java du framework Express pour Node.js. Il utilise également jdk.httpserver , vous pouvez donc apprendre tous les concepts décrits dans cet article sur un cadre réel, qui, de plus, est suffisamment petit pour étudier son code.

Source: https://habr.com/ru/post/fr480564/


All Articles