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