Как вы разрабатываете веб-приложение на Java? Вы устанавливаете Spring, читаете руководство, создаете контроллеры , создаете некоторые представления, добавляете некоторые аннотации , и это работает. Что бы вы сделали, если бы не было Spring (и не было Ruby on Rails в Ruby, и не было Symphony в PHP, и нет… и т. Д.)? Давайте попробуем создать веб-приложение с нуля, начиная с чистого Java SDK и заканчивая полнофункциональным веб-приложением, охватываемым модульными тестами. Я записал вебинар №42 об этом всего несколько недель назад, но эта статья должна объяснить все это еще более подробно.

Прежде всего мы должны создать HTTP-сервер, который будет открывать сокет сервера, прослушивать входящие соединения, читать все, что они должны сказать (запросы HTTP), и возвращать информацию, которую пожелает любой веб-браузер (ответы HTTP). Вы знаете, как работает HTTP , верно? Если вы этого не сделаете, вот краткое напоминание:
Веб-браузер отправляет запрос на сервер, и этот запрос выглядит следующим образом (это обычный текстовый фрагмент данных):
|
1
2
|
GET /index.html HTTP/1.1Host: www.example.com |
Сервер должен прочитать этот текст, подготовить ответ (который должен быть HTML-страницей, читаемой браузером) и вернуть его следующим образом:
|
1
2
3
4
5
|
HTTP/1.1 200 OKContent-Type: text/html; charset=UTF-8Content-Length: 26<html>Hello, world!</html> |
Вот и все. Это очень простой и, я бы сказал, примитивный протокол. Реализация веб-сервера на Java также не так сложна. Вот в очень упрощенной форме:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;import java.net.SocketTimeoutException;import java.util.Arrays;public class Main { public static void main(String... argv) { try (ServerSocket server = new ServerSocket(8080)) { server.setSoTimeout(1000); while (true) { try (Socket socket = server.accept()) { try (InputStream input = socket.getInputStream(); OutputStream output = socket.getOutputStream()) { byte[] buffer = new byte[10000]; int total = input.read(buffer); String request = new String(Arrays.copyOfRange(buffer, 0, total)); String response = "HTTP/1.1 200 OK\r\n\r\nHello, world!"; output.write(response.getBytes()); } } catch (SocketTimeoutException ex) { if (Thread.currentThread().isInterrupted()) { break; } } } } }} |
Попробуйте запустить его, оно должно работать. Вы должны иметь возможность открыть страницу http://localhost:8080 в вашем браузере и увидеть Hello, world! текст.
Это еще не веб-приложение, а просто скелет, который выполняет простую отправку HTTP-запросов в HTTP-ответы. В этом нет серьезного ООП. Это довольно процедурно, но работает. Теперь мы должны сосредоточиться на более важном вопросе: как мы можем добавить больше функций в веб-приложение и сделать возможным обработку различных страниц, рендеринг большего контента и обработку ошибок? Переменная request в приведенном выше фрагменте должна быть каким-то образом преобразована в response .
Самым простым способом было бы: 1) преобразовать запрос в DTO со всеми деталями внутри, затем 2) отправить его «контроллеру», который знает, что делать с данными из DTO, и затем 3) получить ответ DTO из контроллера вынуть данные и обработать ответ. Вот так весна и самый все остальные фреймворки делают это. Однако мы не пойдем по этому пути, мы постараемся сделать его без DTO и чисто объектно-ориентированным
Я должен сказать, что может быть несколько дизайнов, все в стиле ООП. Я покажу вам только один из этих вариантов. Вы, без сомнения, знакомы с нашей структурой Takes , которая родилась несколько лет назад — она имеет собственный дизайн, также объектно-ориентированный. Но тот, который я собираюсь предложить сейчас, кажется, лучше. Вы также можете придумать что-то еще, поэтому не стесняйтесь размещать свои идеи в комментариях ниже или даже создавать репозиторий GitHub и делиться своими мыслями прямо здесь.
Я предлагаю ввести два интерфейса: Resource и Output . Resource — это объект на стороне сервера, который изменяется в зависимости от входящих параметров запроса. Например, когда все, что мы знаем о запросе, это то, что это GET / , это один ресурс. Но если мы также знаем, что запрос имеет, например, Accept: text/plain , мы можем изменить запрос и создать новый, который доставляет простой текст. Вот интерфейс:
|
1
2
3
|
interface Resource { Resource refine(String name, String value);} |
Вот как мы его создаем и мутируем:
|
1
2
3
4
|
Resource r = new DefaultResource() .refine("X-Method", "GET") .refine("X-Query", "/") .refine("Accept", "text/plain"); |
Обратите внимание: каждый вызов .refine() возвращает новый экземпляр интерфейса Resource . Все они неизменны, как и объекты. Благодаря этому дизайну мы не отделяем данные от их процессора. Ресурс — это данные и процессор. Каждый ресурс знает, что делать с данными, и получает только те данные, которые он должен получить. Технически, мы просто реализуем диспетчеризацию запросов , но объектно-ориентированным способом.
Затем нам нужно преобразовать ресурс в ответ. Мы даем ресурсу возможность подвести себя к ответу. Мы не хотим, чтобы данные в виде некоторого DTO покидали ресурс. Мы хотим, чтобы ресурс распечатал ответ. Как насчет предоставления дополнительного метода print() для ресурса:
|
1
2
3
4
|
interface Resource { Resource refine(String name, String value); void print(Output output);} |
И тогда интерфейс Output выглядит так:
|
1
2
3
|
interface Output { void print(String name, String value);} |
Вот примитивная реализация Output :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public class StringBuilderOutput implements Output { private final StringBuilder buffer; StringBuilderOutput(StringBuilder buf) { this.buffer = buf; } @Override public void print(String name, String value) { if (this.buffer.length() == 0) { this.buffer.append("HTTP/1.1 200 OK\r\n"); } if (name.equals("X-Body")) { this.buffer.append("\r\n").append(value); } else { this.buffer.append(name).append(": ").append(value).append("\r\n"); } }} |
Чтобы создать ответ HTTP, мы можем сделать это:
|
1
2
3
4
5
6
|
StringBuilder builder = new StringBuilder();Output output = new StringBuilderOutput(builder);output.print("Content-Type", "text/plain");output.print("Content-Length", "13");output.print("X-Body", "Hello, world!");System.out.println(builder.toString()); |
Теперь давайте создадим класс, который будет принимать входящий запрос String и генерировать ответный String , используя экземпляр Resource в качестве диспетчера :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public class Session { private final Resource resource; Session(Resource res) { this.resource = res; } String response(String request) throws IOException { Map<String, String> pairs = new HashMap<>(); String[] lines = request.split("\r\n"); for (int idx = 1; idx < lines.length; ++idx) { String[] parts = lines[idx].split(":"); pairs.put(parts[0].trim(), parts[1].trim()); if (lines[idx].empty()) { break; } } String[] parts = lines[0].split(" "); pairs.put("X-Method", parts[0]); pairs.put("X-Query", parts[1]); pairs.put("X-Protocol", parts[2]); App.Resource res = this.resource; for (Map.Entry<String, String> pair : pairs.entrySet()) { res = res.refine(pair.getKey(), pair.getValue()); } StringBuilder buf = new StringBuilder(); res.print(new StringBuilderOutput(buf)); return buf.toString(); }} |
Сначала мы анализируем запрос, разбивая его заголовок на строки и игнорируя тело запроса. Вы можете изменить код, чтобы проанализировать тело и передать его в метод refine() , используя X-Body в качестве ключа. На данный момент код выше не делает этого. Но ты получил идею. Часть синтаксического анализа подготавливает пары, которые он может найти в запросе, и передает их одну за другой инкапсулированному ресурсу, изменяя его, пока не получит окончательную форму. Простой ресурс, который всегда возвращает текст, может выглядеть так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
class TextResource implements Resource { private final String body; public TextResource(String text) { this.body = text; } @Override public Resource refine(String name, String value) { return this; } @Override public void print(Output output) { output.print("Content-Type", "text/plain"); output.print("Content-Length", Integer.toString(this.body.length())); output.print("X-Body", this.body); }} |
Ресурс, который обращает внимание на строку запроса и отправляет запрос другим ресурсам, в зависимости от пути в запросе, может выглядеть следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
new Resource() { @Override public Resource refine(String name, String value) { if (name.equals("X-Query")) { if (value.equals("/")) { return new TextResource("Hello, world!"); } else if (value.equals("/balance")) { return new TextResource("256"); } else if (value.equals("/id")) { return new TextResource("yegor"); } else { return new TextResource("Not found!"); } } else { return this; } } @Override public void print(final Output output) { throws IllegalStateException("This shouldn't happen"); }} |
Я надеюсь, у вас есть идея. Приведенный выше код довольно схематичен, и большинство сценариев использования не реализованы, но вы можете сделать это самостоятельно, если вам это интересно. Код находится в репозитории yegor256 / jpages . Не стесняйтесь вносить свой вклад с помощью запроса на извлечение и сделайте этот небольшой фреймворк реальным.
|
Опубликовано на Java Code Geeks с разрешения Егора Бугаенко, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Как создать Java Web Framework с нуля, правильный объектно-ориентированный путь Мнения, высказанные участниками Java Code Geeks, являются их собственными. |