REST API في Java بدون أطر

تم إعداد ترجمة المقال خصيصًا لطلاب الدورة "Java Developer . "




هناك العديد من الأطر والمكتبات في نظام جافا البيئي. على الرغم من أنها ليست بنفس قدر جافا سكريبت ، إلا أنها لا تنتهي صلاحيتها بهذه السرعة. ومع ذلك ، جعلني أعتقد أننا نسينا بالفعل كيفية كتابة التطبيقات دون أطر.

يمكنك أن تقول أن الربيع هو المعيار ولماذا إعادة اختراع العجلة؟ و Spark إطار REST لطيف ومريح. أو ضوء بقية 4jis . وسأقول إنك على حق ، بالطبع.

ولكن إلى جانب الإطار ، بالإضافة إلى الوظيفة النهائية ، ستحصل على الكثير من السحر والصعوبات في التعلم والوظائف الإضافية التي من المحتمل ألا تستخدمها ، بالإضافة إلى الأخطاء. وكلما زاد رمز الجهة الخارجية في خدمتك ، زاد احتمال حدوث أخطاء.

مجتمع المصادر المفتوحة نشط للغاية ، وهناك احتمال كبير أن يتم إصلاح الأخطاء في الإطار بسرعة. لكن مع ذلك ، أحثكم على التفكير فيما إذا كنت تحتاج حقًا إلى إطار عمل. إذا كان لديك خدمة صغيرة أو تطبيق وحدة التحكم ، فقد تتمكن من الاستغناء عنها.

ما الذي يمكن أن تكسبه (أو تخسره) باستخدام كود جافا النقي؟ فكر في الأمر:

  • قد يكون رمزك أكثر نظافة وأكثر قابلية للفهم (أو ربما في حالة فوضى كاملة إذا كنت مبرمجًا سيئًا)
  • سيكون لديك المزيد من التحكم في التعليمات البرمجية الخاصة بك ، فلن يقتصر إطار العمل على ذلك (على الرغم من أنه سيتعين عليك كتابة المزيد من التعليمات البرمجية الخاصة بك للحصول على الوظيفة التي يوفرها الإطار خارج الصندوق)
  • سيتم نشر التطبيق الخاص بك وتشغيله بشكل أسرع ، لأن الإطار لا يحتاج إلى تهيئة عشرات الفئات (أو لن يبدأ تشغيله على الإطلاق إذا قمت بخلط شيء ما ، على سبيل المثال ، في تعدد العمليات)
  • إذا قمت بنشر التطبيق في Docker ، فستكون صورك أصغر بكثير ، لأن جرةك ستكون أصغر أيضًا

لقد أجريت تجربة صغيرة وحاولت تطوير واجهة برمجة تطبيقات REST بدون إطار عمل. ربما سيكون هذا مثيرًا للاهتمام من حيث التعلم والمعرفة المنعشة.

عندما بدأت في كتابة هذا الرمز ، غالبًا ما صادفت مواقف عندما لم تكن هناك وظيفة لدى Spring من خارج منطقة الجزاء. في هذه اللحظات ، بدلاً من أخذ Spring ، كان عليك إعادة التفكير وتطوير كل شيء بنفسك.

أدركت أنه لحل مشاكل العمل الحقيقية ، إلا أنني أفضل استخدام Spring بدلاً من إعادة اختراع العجلة. ومع ذلك ، أعتقد أن هذا التمرين كان تجربة ممتعة للغاية.

الابتداء


سوف أصف كل خطوة ، لكنني لن أعطي دائمًا شفرة المصدر الكاملة. يمكنك رؤية الرمز الكامل في فروع منفصلة من مستودع git .

قم أولاً بإنشاء مشروع Maven جديد باستخدام 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> 

أضف java.xml.bind باعتباره التبعية لأنه تمت إزالته في JDK 11 ( JEP-320 ).

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

وجاكسون لتسلسل JSON

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

لتبسيط فئات POJO ، سوف نستخدم لومبوك :

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

و vavr لأدوات البرمجة الوظيفية

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

نحن أيضا إنشاء Application فئة فارغة الرئيسية.

شفرة المصدر في فرع الخطوة 1 .

نقطة النهاية الأولى


يستند تطبيق الويب الخاص بنا إلى com.sun.net.httpserver.HttpServer . وأبسط نقطة النهاية /api/hello قد تبدو كما يلي:

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

يعمل خادم الويب على المنفذ 8000 ويوفر نقطة نهاية تقوم ببساطة بإرجاع Hello! .. يمكن التحقق من ذلك ، على سبيل المثال ، باستخدام curl:

curl localhost:8000/api/hello

شفرة المصدر في فرع الخطوة 2 .

دعم أساليب HTTP المختلفة


تعمل نقطة النهاية الأولى الخاصة بنا بشكل جيد ، ولكن قد تلاحظ أنه بغض النظر عن طريقة HTTP التي يجب استخدامها ، فإنها تستجيب دائمًا بنفس الطريقة.

على سبيل المثال:

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

أول شيء يجب فعله هو إضافة رمز للتمييز بين الطرق ، على سبيل المثال:

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

حاول الاستعلام مرة أخرى:

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

الجواب سيكون شيء مثل هذا:

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

هناك أيضًا بعض النقاط التي يجب وضعها في الاعتبار. على سبيل المثال ، لا تنسى القيام flush() لـ OutputStream close() exchange . عند استخدام Spring ، لم أضطر حتى للتفكير في الأمر.

شفرة المصدر في فرع الخطوة 3 .

تحليل معلمات الطلب


تحليل معلمات الاستعلام هو "وظيفة" أخرى نحتاج إلى تنفيذها بمفردنا.

لنفترض أننا نريد الحصول على hello api اسمًا في معلمة name ، على سبيل المثال:

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

يمكننا تحليل المعلمات على النحو التالي:

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

واستخدامها على النحو التالي:

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

المثال الكامل في فرع الخطوة 4 .

وبالمثل ، إذا كنا نريد استخدام المعلمات في المسار. على سبيل المثال:

 curl localhost:8000/api/items/1 

للحصول على العنصر بمعرف = 1 ، نحتاج إلى تحليل عنوان url بأنفسنا. انها تحصل ضخمة.

سلامة


غالبًا ما نحتاج إلى حماية الوصول إلى بعض نقاط النهاية. على سبيل المثال ، يمكن القيام بذلك باستخدام المصادقة الأساسية.

لكل HttpContext ، يمكننا تعيين مصدق ، كما هو موضح أدناه:

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

القيمة "myrealm" في مُنشئ BasicAuthenticator هي مجال الاسم. المجال هو اسم افتراضي يمكن استخدامه لفصل مجالات المصادقة.

يمكنك قراءة المزيد حول هذا الموضوع في RFC 1945 .

يمكنك الآن الاتصال بنقطة النهاية الآمنة هذه عن طريق إضافة عنوان Authorization :

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

النص بعد "Basic" هو admin:admin المشفر Base64 admin:admin نص admin:admin ، وهو بيانات الاعتماد المشفرة في مثالنا.

للمصادقة في تطبيق حقيقي ، من المحتمل أن تحصل على بيانات الاعتماد من الرأس ومقارنتها باسم المستخدم وكلمة المرور المخزنة في قاعدة البيانات.

إذا لم تحدد عنوانًا ، فسيستجيب واجهة برمجة التطبيقات بحالة

 HTTP/1.1 401 Unauthorized 

المثال الكامل في فرع الخطوة 5 .

JSON ، التعامل مع استثناء وأكثر من ذلك


حان الوقت الآن للحصول على مثال أكثر تعقيدًا.

من تجربتي في تطوير البرمجيات ، كانت واجهة برمجة التطبيقات الأكثر شيوعًا التي طورتها هي تبادل JSON.

سنقوم بتطوير واجهة برمجة التطبيقات لتسجيل المستخدمين الجدد. لتخزينها ، سوف نستخدم قاعدة البيانات في الذاكرة.

سيكون لدينا كائن مجال User بسيط:

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

يمكنني استخدام لومبوك للتخلص من النمطي (الصانعين ، والأعراس).

في واجهة برمجة تطبيقات REST ، أريد تمرير تسجيل الدخول وكلمة المرور فقط ، لذلك قمت بإنشاء كائن منفصل:

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

يتم إنشاء كائنات User في الخدمة التي سنستخدمها في معالج API. طريقة الخدمة ببساطة يحفظ المستخدم.

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

يمكنك فعل المزيد في تطبيق حقيقي. على سبيل المثال ، أرسل الأحداث بعد تسجيل المستخدم بنجاح.

تنفيذ المستودع على النحو التالي:

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

وأخيرا ، الغراء كل شيء معا في 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(); } 

هنا ، يتم تحويل طلب JSON إلى كائن RegistrationRequest :

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

الذي قمت لاحقًا NewUser كائن NewUser أجل حفظه في قاعدة البيانات وإرسال الاستجابة كـ JSON.

أحتاج أيضًا إلى تحويل كائن RegistrationResponse إلى سلسلة JSON. لهذا نستخدم جاكسون
( com.fasterxml.jackson.databind.ObjectMapper ).

هذه هي الطريقة التي أقوم بها بإنشاء معالج جديد في 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.. } 

يمكن العثور على مثال عملي في فرع الخطوة 6 . هناك أيضًا أضفت معالج استثناء عمومي لإرسال رسائل خطأ JSON القياسية. على سبيل المثال ، إذا كانت طريقة HTTP غير مدعومة أو لم يتم تكوين الطلب إلى API بشكل صحيح.

يمكنك تشغيل التطبيق وتجربة أحد الاستعلامات التالية:

  • مثال الطلب الصحيح

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

الإجابة:

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

  • مثال الخطأ

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

الإجابة:

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

بالإضافة إلى ذلك ، لقد صادفت عن طريق الخطأ مشروع java-Express ، وهو نظير Java لإطار Express لـ Node.js. كما أنه يستخدم jdk.httpserver ، حتى تتمكن من معرفة جميع المفاهيم الموضحة في هذه المقالة على إطار حقيقي ، وهو علاوة على ذلك صغير بما يكفي لدراسة الكود.

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


All Articles