تطبيق الويب MVC بدون أطر و servlets

سنكتب تطبيق ويب صغيرًا ، دون استخدام أطر عمل ويب ومكتبات خارجية وخادم تطبيقات.

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

ماذا يحدث عندما يقوم المستخدم بإدخال عنوان IP معين (أو نظام أسماء النطاقات يتحول إلى عنوان IP) في شريط عنوان المتصفح؟ يتم تقديم طلب إلى ServerSocket المضيف المحدد ، على المنفذ المحدد.

ننظم على المضيف المحلي لدينا ، مأخذ التوصيل على منفذ عشوائي عشوائي (على سبيل المثال 9001).

public class HttpRequestSocket { private static volatile Socket socket; private HttpRequestSocket() { } public static Socket getInstance() throws IOException { if (socket == null) { synchronized (HttpRequestSocket.class) { if (socket == null) { socket = new ServerSocket(9001).accept(); } } } return socket; } } 

لا تنس أن المستمع على المنفذ ، ككائن ، مرغوب فيه بالنسبة لنا في نسخة واحدة ، وبالتالي فردية (ليس بالضرورة تحقق مزدوج ، لكن يمكن أن يكون كذلك).

الآن على مضيفنا (المضيف المحلي) على المنفذ 9001 ، هناك مستمع يتلقى ما يدخله المستخدم كدفق من البايتات.

إذا قمت بطرح البايت [] من المقبس في DataInputStream وقمت بتحويله إلى سلسلة ، فستحصل على شيء مثل هذا:

 GET /index HTTP/1.1 Host: localhost:9001 Connection: keep-alive Cache-Control: no-cache Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Postman-Token: 838f4680-a363-731d-aa74-10ee46b9a87a Accept: */* Accept-Encoding: gzip, deflate, br Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7 

طلب HTTP القياسي مع كل الرؤوس اللازمة.

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

 public interface InputStringUtil { default String parseRequestMapping(final String inputData) { return inputData.split((" "))[1]; } default RequestType parseRequestType(final String source) { return valueOf(source.split(("/"))[0].trim()); } default Map<String, String> parseRequestParameter(final String source) { if (parseRequestType(source) == GET) { return parseGetRequestParameter(source); } else { return parsePostRequestParameter(source); } } @SuppressWarnings("unused") class ParameterParser { static Map<String, String> parseGetRequestParameter(final String source) { final Map<String, String> parameterMap = new HashMap<>(); if(source.contains("?")){ final String parameterBlock = source.substring(source.indexOf("?") + 1, source.indexOf("HTTP")).trim(); for (final String s : parameterBlock.split(Pattern.quote("&"))) { parameterMap.put(s.split(Pattern.quote("="))[0], s.split(Pattern.quote("="))[1]); } } return parameterMap; } static Map<String, String> parsePostRequestParameter(final String source) { //todo task #2 return new HashMap<>(); } } } 

يمكن لهذا الاستخدام تحليل نوع الطلب وعنوان url وقائمة المعلمات لكل من طلبات GET و POST.

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

وحدة التحكم في خدمتنا هي عبارة عن تجريد صغير للمكتبة حيث يمكننا إضافة كتب (في هذا التطبيق ، فقط إلى القائمة) ، وحذف الكتب وإرجاع قائمة بجميع الكتب.

1. تحكم

 public class BookController { private static volatile BookController bookController; private BookController() { } public static BookController getInstance() { if (bookController == null) { synchronized (BookController.class) { if (bookController == null) { bookController = new BookController(); } } } return bookController; } @RequestMapping(path = "/index") @SuppressWarnings("unused") public void index(final Map<String, String> paramMap) { final Map<String, List<DomainBook>> map = new HashMap<>(); map.put("book", DefaultBookService.getInstance().getCollection()); HtmlMarker.getInstance().makeTemplate("index", map); } @RequestMapping(path = "/add") @SuppressWarnings("unused") public void addBook(final Map<String, String> paramMap) { DefaultBookService.getInstance().addBook(paramMap); final Map<String, List<DomainBook>> map = new HashMap<>(); map.put("book", DefaultBookService.getInstance().getCollection()); HtmlMarker.getInstance().makeTemplate("index", map); } } 

لدينا أيضا وحدة تحكم المفرد.

نحن تسجيل RequestMapping. توقف ، ونحن نفعل ذلك دون إطار عمل ، ما RequestMapping؟ سيتعين علينا كتابة هذا التعليق التوضيحي بأنفسنا.

 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequestMapping { String path() default "/"; } 

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

قبل وحدة التحكم ، سيكون لدينا PreProcessor معينة ، والتي سوف تشكل نموذج الطلب مفهومة للبرنامج وتعيين أساليب التحكم.

 public class HttpRequestPreProcessor implements InputStringUtil { private final byte[] BYTE_BUFFER = new byte[1024]; public void doRequest() { try { while (true) { System.out.println("Socket open"); final Socket socket = HttpRequestSocket.getInstance(); final DataInputStream in = new DataInputStream(new BufferedInputStream(socket.getInputStream())); final String inputUrl = new String(BYTE_BUFFER, 0, in.read(BYTE_BUFFER)); processRequest(inputUrl); System.out.println("send request " + inputUrl); } } catch (final IOException e) { e.printStackTrace(); } } private void processRequest(final String inputData) { final String urlMapping = parseRequestMapping(inputData); final Map<String, String> paramMap = parseRequestParameter(inputData); final Method[] methods = BookController.getInstance().getClass().getMethods(); for (final Method method : methods) { if (method.isAnnotationPresent(RequestMapping.class) && urlMapping.contains(method.getAnnotation(RequestMapping.class).path())) { try { method.invoke(BookController.getInstance(), paramMap); return; } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } } } HtmlMarker.getInstance().makeTemplate("error", emptyMap()); } 

2. نموذج

كنموذج ، سيكون لدينا كتاب صف

 public class DomainBook { private String id; private String author; private String title; public DomainBook(String id, String author, String title) { this.id = id; this.author = author; this.title = title; } public String getId() { return id; } public String getAuthor() { return author; } public String getTitle() { return title; } @Override public String toString() { return "id=" + id + " author='" + author + '\'' + " title='" + title + '\''; } } 

و الخدمة

 public class DefaultBookService implements BookService { private static volatile BookService bookService; private List<DomainBook> bookList = new ArrayList<>(); private DefaultBookService() { } public static BookService getInstance() { if (bookService == null) { synchronized (DefaultBookService.class) { if (bookService == null) { bookService = new DefaultBookService(); } } } return bookService; } @Override public List<DomainBook> getCollection() { System.out.println("get collection " + bookList); return bookList; } @Override public void addBook(Map<String, String> paramMap) { final DomainBook domainBook = new DomainBook(paramMap.get("id"), paramMap.get("author"), paramMap.get("title")); bookList.add(domainBook); System.out.println("add book " + domainBook); } @Override public void deleteBookById(long id) { //todo #1 } } 

التي سوف تجمع مجموعة من الكتب ، وتضع في نموذج (بعض الخريطة) البيانات الواردة من الخدمة.

3. عرض

كطريقة عرض ، سنقوم بإنشاء قالب html ، ونضعه في دليل موارد / صفحات منفصلة ، مما يعزز مستوى العرض التقديمي.

 <html> <head> <title>Example</title> </head> <br> <table> <td>${book.id}</td><td>${book.author}</td><td>${book.title}</td> </table> </br> </br> </br> <form method="get" action="/add"> <p>Number<input type="text" name="id"></p> <p>Author<input type="text" name="author"></p> <p>Title<input type="text" name="title"></p> <p><input type="submit" value="Send"></p> </form> </body> </html> 

نكتب محرك القوالب الخاص بنا ، يجب أن يكون الفصل قادراً على تقييم الاستجابة المتلقاة من الخدمة ، وإنشاء رأس http الضروري (في حالتنا ، OK أو BAD REQUEST) ، واستبدال المتغيرات الضرورية في مستند HTML بقيم من النموذج ، وأخيراً تقديم HTML الكامل الذي يمكن للمستعرض والمستخدم فهمه.

 public class HtmlMarker { private static volatile HtmlMarker htmlMarker; private HtmlMarker() { } public static HtmlMarker getInstance() { if (htmlMarker == null) { synchronized (HtmlMarker.class) { if (htmlMarker == null) { htmlMarker = new HtmlMarker(); } } } return htmlMarker; } public void makeTemplate(final String fileName, Map<String, List<DomainBook>> param) { try { final BufferedWriter bufferedWriter = new BufferedWriter( new OutputStreamWriter( new BufferedOutputStream(HttpRequestSocket.getInstance().getOutputStream()), StandardCharsets.UTF_8)); if (fileName.equals("error")) { bufferedWriter.write(ERROR + ERROR_MESSAGE.length() + OUTPUT_END_OF_HEADERS + readFile(fileName, param)); bufferedWriter.flush(); } else { bufferedWriter.write(SUCCESS + readFile(fileName, param).length() + OUTPUT_END_OF_HEADERS + readFile(fileName, param)); bufferedWriter.flush(); } } catch (IOException e) { e.printStackTrace(); } } private String readFile(final String fileName, Map<String, List<DomainBook>> param) { final StringBuilder builder = new StringBuilder(); final String path = "src\\resources\\pages\\" + fileName + ".html"; try (BufferedReader br = Files.newBufferedReader(Paths.get(path))) { String line; while ((line = br.readLine()) != null) { if (line.contains("${")) { final String key = line.substring(line.indexOf("{") + 1, line.indexOf("}")); final String keyPrefix = key.split(Pattern.quote("."))[0]; for (final DomainBook domainBook : param.get(keyPrefix)) { builder.append("<tr>"); builder.append( line.replace("${book.id}", domainBook.getId()) .replace("${book.author}", domainBook.getAuthor()) .replace("${book.title}", domainBook.getTitle()) ).append("</tr>"); } if(param.get(keyPrefix).isEmpty()){ builder.append(line.replace("${book.id}</td><td>${book.author}</td><td>${book.title}", "<p>library is EMPTY</p>")); } continue; } builder.append(line).append("\n"); } return builder.toString(); } catch (IOException e) { e.printStackTrace(); } return ""; } } 

كاختبار لتطبيق الأداء ، نضيف بعض الكتب إلى تطبيقنا:

صورة

شكرا لك على القراءة حتى النهاية ، هذا المقال هو للإرشاد فقط ، وآمل أن يكون مثيرا للاهتمام ومفيدة قليلا.

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


All Articles