Веб-Приложение Mvc Без Фреймворков И Сервлетов

Давайте напишем небольшое веб-приложение без использования веб-фреймворков, внешних библиотек и сервера приложений.

Цель этой статьи — показать общую суть того, что происходит под капотом веб-сервиса, на примере Java. Итак, начнем.

Нам не следует использовать сторонние библиотеки, а также сервлеты.

Поэтому собирать проект мы будем на Maven, но без зависимостей.

Что происходит, когда пользователь вводит определенный IP-адрес (или DNS, который превращается в 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; } }

Не забывайте, что нам нужен прослушиватель на порту, как объект, в единственном экземпляре, то есть синглтон (не обязательно перепроверять, но это возможно).

Теперь на нашем хосте (localhost) на порту 9001 есть прослушиватель, который получает то, что вводит пользователь, в виде потока байтов.

Если вы вычтете byte[] из сокета в 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-запрос со всеми необходимыми заголовками.

Для парсинга сделаем небольшой служебный интерфейс с методами по умолчанию, на мой взгляд это вполне удобно для таких целей (к тому же, если это Spring, то мы уменьшим количество зависимостей в классе).



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-адресов.

Но в текущей реализации мы ограничимся одним контроллером.

Перед контроллером у нас будет некий Препроцессор, который будет формировать понятную программе модель запроса и отображать методы контроллера.



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. Модель В качестве модели у нас будет класс Book.

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='" + название +'\''; } }

и сервис

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

Чтобы протестировать приложение на работоспособность, добавим в наше приложение пару книг:

Веб-приложение MVC без фреймворков и сервлетов

Спасибо, что дочитали до конца, статья носит ознакомительный характер, надеюсь, что было немного интересно и немного полезно.

Теги: #java #MVC

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.