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

от автора

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

Цель данной статьи показать общую суть происходящего под капотом веб-сервиса, на примере Java. Итак, поехали. Мы не должны использовать сторонние библиотеки, а также сервлет. Поэтому проект соберем Maven-ом, но без зависимостей.

Что происходит когда пользователь вводит некий ip-адрес(ну или dns который превращается в ip-адрес) в адресной строке браузера? Происходит запрос к ServerSocket указанного host-a, на указанный порт.

Организуем на нашем localhost, socket на случайном свободном порту(например 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;     } } 

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

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

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

Для парсинга сделаем небольшой util-интерфейс с default-методами, на мой взгляд довольно удобно для подобных целей, (к тому же если это все таки 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<>();         }     } } 

Данный util умеет парсить типа запроса, url, и список параметров, как для GET, так и для POST запросов.

В процессе парсинга формируем модель request, с целевым url и Map с параметрами запроса.

Контроллер для нашего сервиса представляет собой небольшую абстракцию на библиотеку, в которой мы можем добавлять книги(в данной реализации просто в List), удалять книги и возвращать список всех книг.

1. Controller

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

Контроллер у нас также singleton.

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

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

Также можно было добавить аннотацию Controller над классом и при старте приложения собирать все классы помеченные этой аннотацией, и их методы, и добавлять в некую Map-ку c маппингов url. Но в текущей реализации ограничимся одним контроллером.

Перед контроллером, у нас будет некий PreProcessor, который будет формировать понятную программе модель request и осуществлять мэппинг к методам контроллера.

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. Model

В качестве модели у нас будет класс 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='" + title + '\'';     } } 

и service

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

который будет собирать коллекцию книг, и класть в Model(некую Map) данные полученные из service.

3. View

В качестве View, мы сделаем html шаблон, и разместим его в отдельной директории resources/pages, обосабливая уровень представления.

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

В качестве тестирования приложения на работоспособность добавим пару книг в наше приложение:

image

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


ссылка на оригинал статьи https://habr.com/ru/post/461965/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *