Давайте напишем небольшое веб-приложение без использования веб-фреймворков, внешних библиотек и сервера приложений.
Цель этой статьи — показать общую суть того, что происходит под капотом веб-сервиса, на примере 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 "";
}
}
Чтобы протестировать приложение на работоспособность, добавим в наше приложение пару книг:
Спасибо, что дочитали до конца, статья носит ознакомительный характер, надеюсь, что было немного интересно и немного полезно.
Теги: #java #MVC
-
Основы Идеального Поста В Блоге
19 Oct, 24 -
Спиннакер - Нейронный Компьютер
19 Oct, 24 -
Превращаем Xbox 360 В Пк. Немного О Моддинге
19 Oct, 24 -
Базовое Управление Репутацией
19 Oct, 24 -
Облако Css3 3D-Преобразований
19 Oct, 24