REST-API in Java ohne Frameworks

Die Übersetzung des Artikels wurde speziell für die Kursteilnehmer vorbereitet. "Java-Entwickler . "




Es gibt viele Frameworks und Bibliotheken im Java-Ökosystem. Obwohl nicht so häufig wie in JavaScript, verfallen sie nicht so schnell. Ich hatte jedoch den Eindruck, wir hätten bereits vergessen, wie man Anwendungen ohne Framework schreibt.

Man kann sagen, dass Frühling der Standard ist und warum das Rad neu erfinden? Und Spark ist ein schönes, praktisches REST-Framework. Oder Light-Rest-4jis . Und ich werde sagen, dass Sie natürlich Recht haben.

Aber zusammen mit dem Framework erhalten Sie neben der fertigen Funktionalität eine Menge Magie, Lernschwierigkeiten, zusätzliche Funktionen, die Sie höchstwahrscheinlich nicht verwenden werden, sowie Fehler. Und je mehr Code von Drittanbietern in Ihrem Dienst enthalten ist, desto größer ist die Wahrscheinlichkeit, dass Fehler auftreten.

Die Open Source Community ist sehr aktiv und es besteht eine hohe Wahrscheinlichkeit, dass die Fehler im Framework schnell behoben werden. Trotzdem möchte ich Sie dringend bitten, darüber nachzudenken, ob Sie wirklich einen Rahmen benötigen. Wenn Sie einen kleinen Dienst oder eine Konsolenanwendung haben, können Sie möglicherweise darauf verzichten.

Was können Sie mit reinem Java-Code gewinnen (oder verlieren)? Denken Sie darüber nach:

  • Ihr Code kann viel sauberer und verständlicher sein (oder in einem völligen Chaos, wenn Sie ein schlechter Programmierer sind)
  • Sie haben mehr Kontrolle über Ihren Code, Sie sind nicht an das Framework gebunden (obwohl Sie mehr Code schreiben müssen, damit das Framework sofort funktioniert).
  • Ihre Anwendung wird viel schneller implementiert und gestartet, da das Framework nicht Dutzende von Klassen initialisieren muss (oder überhaupt nicht startet, wenn Sie etwas mischen, z. B. in Multithreading).
  • Wenn Sie die Anwendung in Docker bereitstellen, sind Ihre Bilder viel kleiner, da Ihr jar auch kleiner ist

Ich habe ein kleines Experiment durchgeführt und versucht, eine REST-API ohne Framework zu entwickeln. Vielleicht ist dies in Bezug auf das Lernen und das Auffrischen von Wissen interessant.

Als ich anfing, diesen Code zu schreiben, stieß ich oft auf Situationen, in denen es keine Funktionalität gab, die Spring sofort zur Verfügung stellte. Anstatt den Frühling zu nehmen, musste man in diesen Momenten alles selbst überdenken und entwickeln.

Mir wurde klar, dass ich zur Lösung echter geschäftlicher Probleme lieber Spring verwenden würde, als das Rad neu zu erfinden. Ich denke jedoch, dass diese Übung eine ziemlich interessante Erfahrung war.

Erste Schritte


Ich werde jeden Schritt beschreiben, aber nicht immer den vollständigen Quellcode angeben. Sie können den vollständigen Code in separaten Zweigen des Git-Repositorys sehen .

Erstellen Sie zunächst ein neues Maven-Projekt mit der folgenden 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 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> 

Fügen Sie java.xml.bind als Abhängigkeit hinzu, da es in JDK 11 ( JEP-320 ) entfernt wurde.

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

und Jackson für die JSON-Serialisierung

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

Um die POJO-Klassen zu vereinfachen, verwenden wir Lombok :

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

und vavr für funktionale Programmiertools

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

Wir erstellen auch die leere Hauptklasse Application .

Der Quellcode in der Step-1- Verzweigung.

Erster Endpunkt


Unsere Webanwendung basiert auf der Klasse com.sun.net.httpserver.HttpServer . Und der einfachste Endpunkt /api/hello könnte so aussehen:

 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(); } } 

Der Webserver wird auf Port 8000 ausgeführt und bietet einen Endpunkt, der einfach Hello! Zurückgibt. Dies kann beispielsweise mit curl überprüft werden:

curl localhost:8000/api/hello

Der Quellcode in der Step-2- Verzweigung.

Unterstützung für verschiedene HTTP-Methoden


Unser erster Endpunkt funktioniert einwandfrei, aber Sie werden feststellen, dass er unabhängig von der zu verwendenden HTTP-Methode immer auf die gleiche Weise reagiert.

Zum Beispiel:

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

Als Erstes müssen Sie Code hinzufügen, um zwischen Methoden zu unterscheiden. Beispiel:

 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(); })); 

Versuchen Sie die Abfrage erneut:

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

Die Antwort wäre ungefähr so:

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

Es gibt auch ein paar Punkte zu beachten. Vergessen Sie beispielsweise nicht, flush() für OutputStream und close() für den exchange OutputStream . Bei der Verwendung von Spring musste ich nicht einmal darüber nachdenken.

Der Quellcode in der Step-3- Verzweigung.

Analysieren von Anforderungsparametern


Das Parsen von Abfrageparametern ist eine weitere "Funktion", die wir selbst implementieren müssen.

Angenommen, wir möchten, dass unsere Hallo-API einen Namen im Parameter name erhält, zum Beispiel:

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

Wir könnten die Parameter wie folgt analysieren:

 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()))); } 

und wie folgt verwenden:

 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); 

Vollständiges Beispiel in Schritt-4- Zweig.

Ebenso, wenn wir Parameter im Pfad verwenden möchten. Zum Beispiel:

 curl localhost:8000/api/items/1 

Um das Element mit id = 1 zu erhalten, müssen wir die URL selbst analysieren. Es wird sperrig.

Sicherheit


Oft müssen wir den Zugriff auf einige Endpunkte schützen. Dies kann beispielsweise mithilfe der Standardauthentifizierung erfolgen.

Für jeden HTTP-Kontext können wir einen Authentifikator festlegen, wie unten gezeigt:

 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"); } }); 

Der Wert "myrealm" im BasicAuthenticator Konstruktor ist der Namensbereich. Realm ist ein virtueller Name, der zum Trennen von Authentifizierungsdomänen verwendet werden kann.

Sie können mehr darüber in RFC 1945 lesen.

Jetzt können Sie diesen sicheren Endpunkt aufrufen, indem Sie den Authorization Header hinzufügen:

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

Der Text nach „Basic“ ist Base64-codierter admin:admin Text. Dies sind die in unserem Beispiel fest codierten Anmeldeinformationen.

Für die Authentifizierung in einer realen Anwendung werden Sie wahrscheinlich die Anmeldeinformationen aus dem Header abrufen und mit dem in der Datenbank gespeicherten Benutzernamen und Kennwort vergleichen.

Wenn Sie keinen Titel angeben, antwortet die API mit einem Status

 HTTP/1.1 401 Unauthorized 

Vollständiges Beispiel in der Step-5- Verzweigung.

JSON, Ausnahmebehandlung und mehr


Jetzt ist es Zeit für ein komplexeres Beispiel.

Aufgrund meiner Erfahrung in der Softwareentwicklung war die häufigste API, die ich entwickelt habe, JSON-Austausch.

Wir werden eine API für die Registrierung neuer Benutzer entwickeln. Zum Speichern verwenden wir die Datenbank im Speicher.

Wir werden ein einfaches User :

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

Ich benutze Lombok, um das Boilerplate (Konstrukteure, Getter) loszuwerden.

In der REST-API möchte ich nur den Benutzernamen und das Kennwort übergeben. Daher habe ich ein separates Objekt erstellt:

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

User werden in dem Dienst erstellt, den wir im API-Handler verwenden. Die Dienstmethode speichert einfach den Benutzer.

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

In einer realen Anwendung können Sie mehr tun. Senden Sie beispielsweise Ereignisse nach erfolgreicher Benutzerregistrierung.

Die Repository-Implementierung sieht wie folgt aus:

 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; } } 

Zum Schluss kleben Sie alles in 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(); } 

Hier wird die JSON-Anforderung in ein RegistrationRequest Objekt konvertiert:

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

Das NewUser ich später dem NewUser Objekt zu, um es in der Datenbank zu speichern und die Antwort als JSON zu senden.

Ich muss auch das RegistrationResponse Objekt zurück in eine JSON-Zeichenfolge konvertieren. Dafür benutzen wir Jackson
( com.fasterxml.jackson.databind.ObjectMapper ).

So erstelle ich einen neuen Handler in 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.. } 

Ein funktionierendes Beispiel finden Sie im Schritt-6- Zweig. Dort habe ich auch einen globalen Ausnahmebehandler hinzugefügt, um Standard-JSON-Fehlermeldungen zu senden. Zum Beispiel, wenn die HTTP-Methode nicht unterstützt wird oder die Anforderung an die API nicht korrekt erstellt wurde.

Sie können die Anwendung ausführen und eine der folgenden Abfragen ausführen:

  • richtiges Anforderungsbeispiel

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

antworte:

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

  • Fehlerbeispiel

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

antworte:

 < 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\"])"} 

Außerdem bin ich aus Versehen auf das Java-Express- Projekt gestoßen , das ein Java-Gegenstück zum Express- Framework für Node.js ist. Es wird auch jdk.httpserver , sodass Sie alle in diesem Artikel beschriebenen Konzepte in einem realen Framework erlernen können, das außerdem klein genug ist, um seinen Code zu studieren.

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


All Articles