Статьи

Как создать Java Web Framework с нуля, правильный объектно-ориентированный путь

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

Завтрак у Тиффани (1961) Блейк Эдвардс

Прежде всего мы должны создать HTTP-сервер, который будет открывать сокет сервера, прослушивать входящие соединения, читать все, что они должны сказать (запросы HTTP), и возвращать информацию, которую пожелает любой веб-браузер (ответы HTTP). Вы знаете, как работает HTTP , верно? Если вы этого не сделаете, вот краткое напоминание:

Веб-браузер отправляет запрос на сервер, и этот запрос выглядит следующим образом (это обычный текстовый фрагмент данных):

1
2
GET /index.html HTTP/1.1
Host: www.example.com

Сервер должен прочитать этот текст, подготовить ответ (который должен быть HTML-страницей, читаемой браузером) и вернуть его следующим образом:

1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-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, являются их собственными.