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