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);
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);
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: *
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 -> {
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);
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"}
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.