REST API di Java tanpa kerangka kerja

Terjemahan artikel disiapkan khusus untuk siswa kursus "Pengembang Java . "




Ada banyak kerangka kerja dan perpustakaan di ekosistem Jawa. Meskipun tidak sebanyak di JavaScript, mereka tidak berakhir dengan cepat. Namun, itu membuat saya berpikir bahwa kami sudah lupa bagaimana menulis aplikasi tanpa kerangka kerja.

Anda dapat mengatakan bahwa Spring adalah standar dan mengapa menciptakan kembali kemudi? Dan Spark adalah kerangka REST yang bagus dan nyaman. Atau Cahaya-istirahat-4 ji . Dan saya akan mengatakan bahwa Anda, tentu saja, benar.

Namun seiring dengan kerangka itu, selain fungsi yang telah selesai, Anda mendapatkan banyak keajaiban, kesulitan belajar, fungsi tambahan yang kemungkinan besar tidak akan Anda gunakan, serta bug. Dan semakin banyak kode pihak ketiga dalam layanan Anda, semakin besar kemungkinan Anda akan mengalami kesalahan.

Komunitas open source sangat aktif, dan ada kemungkinan besar bahwa kesalahan dalam kerangka kerja akan cepat diperbaiki. Tapi tetap saja, saya ingin Anda berpikir apakah Anda benar-benar membutuhkan kerangka kerja. Jika Anda memiliki layanan kecil atau aplikasi konsol, Anda mungkin dapat melakukannya tanpa itu.

Apa yang bisa Anda dapatkan (atau kehilangan) menggunakan kode Java murni? Pikirkan tentang itu:

  • kode Anda bisa menjadi jauh lebih bersih dan lebih mudah dipahami (atau mungkin berantakan jika Anda seorang programmer yang buruk)
  • Anda akan memiliki kontrol lebih besar atas kode Anda, Anda tidak akan dibatasi oleh kerangka kerja (meskipun Anda harus menulis lebih banyak kode Anda untuk fungsionalitas yang disediakan kerangka kerja di luar kotak)
  • aplikasi Anda akan digunakan dan diluncurkan lebih cepat, karena kerangka kerja tidak perlu menginisialisasi puluhan kelas (atau tidak akan memulai sama sekali jika Anda mencampur sesuatu, misalnya, dalam multithreading)
  • jika Anda menggunakan aplikasi di Docker, maka gambar Anda akan jauh lebih kecil, karena tabung Anda juga akan lebih kecil

Saya melakukan sedikit percobaan dan mencoba mengembangkan REST API tanpa kerangka. Mungkin ini akan menarik dalam hal pembelajaran dan pengetahuan yang menyegarkan.

Ketika saya mulai menulis kode ini, saya sering menjumpai situasi ketika tidak ada fungsi yang dimiliki Spring. Pada saat-saat ini, alih-alih mengambil Spring, Anda harus memikirkan kembali dan mengembangkan semuanya sendiri.

Saya menyadari bahwa untuk menyelesaikan masalah bisnis nyata, saya masih lebih suka menggunakan Spring daripada menciptakan kembali roda. Namun, saya pikir latihan ini adalah pengalaman yang sangat menarik.

Memulai


Saya akan menjelaskan setiap langkah, tetapi saya tidak akan selalu memberikan kode sumber lengkap. Anda dapat melihat kode lengkap di cabang terpisah dari repositori git .

Pertama buat proyek Maven baru dengan pom.xml berikut.

 <?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> 

Tambahkan java.xml.bind sebagai dependensi karena telah dihapus di JDK 11 ( JEP-320 ).

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

dan Jackson untuk serialisasi JSON

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

Untuk menyederhanakan kelas POJO, kita akan menggunakan Lombok :

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

dan vavr untuk alat pemrograman fungsional

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

Kami juga membuat Application kelas kosong utama.

Kode sumber di cabang langkah-1 .

Titik akhir pertama


Aplikasi web kami akan didasarkan pada com.sun.net.httpserver.HttpServer kelas. Dan titik akhir paling sederhana /api/hello mungkin terlihat seperti ini:

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

Server web berjalan pada port 8000 dan menyediakan titik akhir yang hanya mengembalikan Hello! .. Ini dapat diperiksa, misalnya, menggunakan curl:

curl localhost:8000/api/hello

Kode sumber di cabang langkah-2 .

Dukungan untuk berbagai metode HTTP


Titik akhir pertama kami berfungsi dengan baik, tetapi Anda mungkin memperhatikan bahwa apa pun metode HTTP yang digunakan, selalu merespons dengan cara yang sama.

Sebagai contoh:

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

Hal pertama yang harus dilakukan adalah menambahkan kode untuk membedakan antara metode, misalnya:

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

Coba kueri lagi:

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

jawabannya akan seperti ini:

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

Ada juga beberapa hal yang perlu diingat. Sebagai contoh, jangan lupa untuk melakukan flush() untuk OutputStream dan close() untuk exchange . Saat menggunakan Spring, saya bahkan tidak perlu memikirkannya.

Kode sumber di cabang langkah-3 .

Parameter permintaan parsing


Memilah parameter kueri adalah "fungsi" lain yang perlu kita implementasikan sendiri.

Misalkan kita ingin hello api kita mendapatkan nama dalam parameter name , misalnya:

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

Kita dapat menguraikan parameter sebagai berikut:

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

dan gunakan seperti di bawah ini:

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

Contoh lengkap di cabang langkah-4 .

Demikian pula, jika kita ingin menggunakan parameter di jalur. Sebagai contoh:

 curl localhost:8000/api/items/1 

Untuk mendapatkan elemen dengan id = 1, kita perlu mengurai url sendiri. Itu menjadi besar.

Keamanan


Seringkali kita perlu melindungi akses ke beberapa titik akhir. Misalnya, ini dapat dilakukan dengan menggunakan otentikasi dasar.

Untuk setiap HttpContext, kita dapat menetapkan autentikator, seperti yang ditunjukkan di bawah ini:

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

Nilai "myrealm" di konstruktor BasicAuthenticator adalah ranah nama. Realm adalah nama virtual yang dapat digunakan untuk memisahkan domain otentikasi.

Anda dapat membaca lebih lanjut tentang ini di RFC 1945 .

Sekarang Anda dapat memanggil titik akhir aman ini dengan menambahkan header Authorization :

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

Teks setelah "Dasar" adalah admin:admin disandikan Base64 admin:admin teks admin:admin , yang merupakan kredensial yang dikodekan dalam contoh kami.

Untuk otentikasi dalam aplikasi nyata, Anda mungkin akan mendapatkan kredensial dari header dan membandingkannya dengan nama pengguna dan kata sandi yang tersimpan dalam database.

Jika Anda tidak menentukan judul, API akan merespons dengan status

 HTTP/1.1 401 Unauthorized 

Contoh lengkap di cabang langkah-5 .

JSON, penanganan pengecualian dan banyak lagi


Sekarang saatnya untuk contoh yang lebih kompleks.

Dari pengalaman saya dalam pengembangan perangkat lunak, API paling umum yang saya kembangkan adalah pertukaran JSON.

Kami akan mengembangkan API untuk mendaftarkan pengguna baru. Untuk menyimpannya, kami akan menggunakan database dalam memori.

Kami akan memiliki objek domain User sederhana:

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

Saya menggunakan Lombok untuk menyingkirkan boilerplate (konstruktor, getter).

Di REST API, saya hanya ingin memasukkan login dan kata sandi, jadi saya membuat objek terpisah:

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

Objek User dibuat di layanan yang akan kami gunakan di penangan API. Metode layanan hanya menghemat pengguna.

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

Anda dapat melakukan lebih banyak dalam aplikasi nyata. Misalnya, kirim acara setelah pendaftaran pengguna berhasil.

Implementasi repositori adalah sebagai berikut:

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

Akhirnya, rekatkan semuanya dalam 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(); } 

Di sini, permintaan JSON dikonversi ke objek RegistrationRequest :

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

yang kemudian saya petakan ke objek NewUser untuk menyimpannya dalam database dan mengirim respons sebagai JSON.

Saya juga perlu mengonversi objek RegistrationResponse kembali ke string JSON. Untuk ini kami menggunakan Jackson
( com.fasterxml.jackson.databind.ObjectMapper ).

Inilah cara saya membuat penangan baru di 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.. } 

Contoh kerja dapat ditemukan di cabang langkah-6 . Di sana saya juga menambahkan penangan pengecualian global untuk mengirim pesan kesalahan JSON standar. Misalnya, jika metode HTTP tidak didukung atau permintaan ke API tidak dibentuk dengan benar.

Anda dapat menjalankan aplikasi dan mencoba salah satu dari pertanyaan berikut:

  • contoh permintaan yang benar

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

jawab:

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

  • contoh kesalahan

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

jawab:

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

Selain itu, saya tidak sengaja berlari ke proyek java-express , yang merupakan mitra Java dari kerangka kerja Express untuk Node.js. Ini juga menggunakan jdk.httpserver , sehingga Anda dapat mempelajari semua konsep yang dijelaskan dalam artikel ini pada kerangka nyata, yang, lebih dari itu, cukup kecil untuk mempelajari kodenya.

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


All Articles