Перейти к содержимому

Как написать сервер на java

  • автор:

Http-server на Java: изучение серверных основ на примере создания собственного чата

Программирование на Java – перспективное направление у современных разработчиков. Согласно данным Google данный язык кодинга пользуется огромным спросом, несмотря на то, что был создан изначально весьма давно. А именно – в 1995 году. С тех пор Джава перетерпел много модификаций. Нынче это – один из самых перспективных языков программирования. Весьма прост в освоении, что значительно упрощает задачи, поставленные перед новичками.

Особенности Java

Google и другие поисковые системы помогут понять, что это за язык программирования. Он имеет ряд ключевых особенностей. А именно:

  • относительно простой синтаксис;
  • отличное комьюнити;
  • множество документации (в том числе на русском языке);
  • наличие ООП;
  • собственный движок.

New programs создавать при помощи данного варианта способен даже начинающий программист.

Основное предназначение Java – это работа с Сетью. Идеально подходит для веб-программирования. Но и «обычные» утилиты посредством соответствующего семейства пишутся без существенных затруднений.

Сервер типа http – определение

При работе с сетью (new или old – не так важно) компьютеры подключаются к так называемым веб-серверам. Без них невозможна работа в интернете.

HTTP-сервер – это веб server. Имеет непосредственное отношение как к «железу» компьютера, так и к программному обеспечению:

  1. В качестве аппаратного устройства это – new машина, которая отвечает за хранение ресурсов того или иного сайта. Включает в себя доставку на устройство юзера через интернет-обозреватели и иные утилиты. Чаще всего подключается к интернету. Доступ предоставляется через доменные имена.
  2. Как ПО, согласно Google, веб-сервер представляет собой некое «приложение», совмещающее в себе функционал для контроля доступа web-пользователей к размещенным на сервере документов. HTTP Sever – часть программного обеспечения, понимающая URLs и HTTP-протоколы (они нужны для просмотра и отображения страничек в Сети).

Google указывает на то, что, когда браузер нуждается в каком-то файле, помещенном на веб-сервере, происходит запрос посредством http. Когда запрос доходит до нужного «железа», соответствующий сервер (программное обеспечение) осуществляет обратную передачу через упомянутый ранее «канал».

Виды серверов

Если разработчик на Джаве решил создать new servers, важно понимать – для опубликования сайта требуется подобрать один из нескольких видов оных. Упомянутый элемент бывает:

  1. Статическим (static void main). Носит название стека. Включает в себя компьютер с сервером HTTP. Последний будет посылать new файлы в интернет-обозреватель без каких-либо корректировок.
  2. Динамическим. Включает в себя статическую «модель» и дополнительное ПО. Чаще всего – базы данные или серверные приложения. Последние будут вносить изменения в документы перед тем, как отправить их в обозреватель Сети.

За счет new servers можно отображать страницы в браузерах. Итоговый результат удобен и понятен пользователям без навыков в сфере программирования. Для своей работы, согласно Google, рассматриваемый элемент может задействовать шаблоны информации из БД Пример – Википедия. Это – не полноценный сайт, а HTML-шаблон. За счет соответствующего приема удается значительно ускорить сопровождение web-софта.

Все наготове – встроенные возможности Java

При помощи Google и иных поисковиков можно достаточно быстро разобраться в упомянутом элементе, задействованном при программировании. Но не каждый пользователь готов заниматься созданием полноценного сервисного контента.

Для Джава поставленная задача не выступает в качестве тривиальной. Язык программирования содержит встроенные возможности, посредством которых new http создается без существенных затруднений. Всего 100 строчек кода – и перед разработчиком окажется весьма неплохой вариант, поддерживающий обработку запросов и иные HTTP-команд.

HTTPServer

Серверные возможности в Джаве предоставляются через SDK. Они имеют следующие особенности:

  • название – HttpServer;
  • пакетный класс – com.sun.net;
  • запись: httpServer server = httpServer.create (new InetSocketAddress(“localhost”, 8001), 0));.

Приведенный пример – это создание экземпляра в пределах локального узла. Номер порта – 8001. Аргумент 0 здесь выступает для организации так называемой обратной регистрации.

Как выполняется запрос – очередность

Если рассматриваемый элемент работает исправно, он будет отправлять и получать разнообразные команды-запросы. В Google по этому поводу очень много информации. Новички в ней рискуют запутаться.

Чтобы понимать принципы работы серверных возможностей Джавы, стоит уяснить, какой очередности подчиняется обработка посланного запроса:

  1. Клиент формирует запрос. Происходит его отправка.
  2. Ставится очередь операционной системы.
  3. Происходит передача на сервер для дальнейшей обработки.
  4. Одновременные запросы ставятся в очередь. Их количество определяется ОС автоматически.

Важно: предложенный пример не требует отправлять в очередь никаких «команд», так как имеет значение аргумента, равное нулю.

О коде

Вот пример, который поможет лучше разобрать в изучаемой сфере даже без Google:

Http-server на Java: изучение серверных основ на примере создания собственного чата

Здесь происходит создание контекста test. Он выступает корнем контекста утилиты. Второй параметр – экземпляр так называемого обработчика. Он будет работать с HTTP-командами.

Теперь допускается применение потокового пула. В приведенном примере их 10 штук:

New Thread PoolExecutor ThreadPoolExecutor = (ThreadPoolExecutor)Executors.newFixedThreadPool(10);

Далее new server требует запуска. Операция осуществляется путем задействования кода: server.start();.

Handler

А вот интерфейс согласно данным Google, использующий метод handle():

Http-server на Java: изучение серверных основ на примере создания собственного чата

Внимание: в приведенном примере вышедший за рамки изображения код имеет вид private void (может быть и public static void) handleResponse(HttpExchange httpExchange, String requestParamValue) throws IOException

OutputStream outputStream = httpExchange.getResponseBody();

StringBuilder htmlBuilder = new StringBuilder();

Кодификация обрабатывает запрос, затем отправляет ответ непосредственно клиенту. Обработка осуществляется через класс HttpExchange.

Запрос GET

Об обработке запроса Get необходимо знать следующее:

  • написавшие его будут использовать метод HandleGETRequest;
  • далее происходит вызов getRequestURL(), который принадлежит классу HttpExchange.

Несмотря на то, что это – минимум, обрабатывающий единичный запрос, он поможет справиться с самыми разными задачами.

Работа с ответом

После того, как произошел поиск ответа, его нужно направить клиенту. Делается это через handleResponse(). Пользователь получит выходной поток через обращение к методу gerResponseBody(). Чуть позже удастся записать информацию из HTML в выходные потоки.

Response header – это крайне важный момент. Если он будет упущен, в обозревателе Сети юзер увидит ошибку ERR_Empty_Response. В случае, когда все хорошо, браузер покажет тот или иной ответ.

Близкое знакомство – собственный чат через WebSoket и SpringBoot на Java

Сегодня трудно представить юзера, который не умел бы работать в интернете. Здесь не только отдыхают, но и отправляют сообщения, трудятся, создают что-то полезное. Коммуникации помогает поддерживать так называемый чат. И это – элементарный серверный пример.

В Google можно отыскать немало new идей относительно того, как создать собственный chat. При определенной сноровке соответствующий ресурс будет действительно уникальным: со смайликами, эмодзи, анимацией и другими элементами.

Для написания подобного контента в Java можно использовать:

При помощи соответствующих элементов даже новичок сможет без труда разобраться с поставленной задачей. Ему не придется долго изучать Google, а также «непонятные» элементы кода (типа ioexception e, public void run, override public и так далее – на первых порах в них можно запутаться).

Определение WebSoket

WebSoket – это протокол, при помощи которого осуществляется установка двусторонней связи клиент-сервер. Переключение, как говорит Google, происходит после специального http-запроса. Его формирует и отправляет клиент: Upgrade: websocket.

При поддержке вебсокетов будет получен ответ «Yes». Далее произойдет общение через new протоколы WebSocket. С HTTP оный не имеет ничего общего.

Как создать приложение

Для того, чтобы сделать new application, можно прибегнуть к работе с сайтом-инициализатором. В этом случае алгоритм действий будет следующим:

  1. Зайти на страницу .
  2. Выбрать в списке Spring Boot 2.
  3. Указать имя группы и артефакта проекта.
  4. Активировать зависимость «вебсокета».
  5. Провести генерацию new project.

Далее предстоит разархивировать проект, а также сделать import java в редактор. Именем утилиты будет – Maven. Дополнительно необходимо создать пакеты config, controller и model.

Настройка

Теперь предстоит работать с public class, а также с такими элементами как static final int и private static. Ведь самое простое позади – далее требуется провести настройку «вебсокета».

Начинается процесс с конечной точки и брокера сообщений. Проводится операция в config. Класс конфигурации будет иметь следующий вид:

Http-server на Java: изучение серверных основ на примере создания собственного чата

  • аннотация @configuration – устанавливается обязательно в классе конфигурации Spring;
  • аннотация @EnableWebSocketMessageBroker – активирует new WebsokcketServer;
  • метод registerStompEndpoints() – отвечает за регистрацию конечной точки, которую клиенты задействуют для подключения к серверу;
  • configMessageBroker() – настройка брокера для отправки сообщений между клиентами.

В рассматриваемом примере задействован встроенный брокер. Это самый простой вариант.

Модели сообщений

Следующие исходники – это настройка моделей сообщений. Создается пакет model, в котором после размещается класс ChatMessage:

Http-server на Java: изучение серверных основ на примере создания собственного чата

Соответствующий фрагмент кода еще не позволит отправлять текст друг другу. Для реализации поставленной задачи предстоит выполнить иные действия.

Контроллер сообщений

Google поможет разобраться в том, как создавать собственные чаты. Чтобы в них можно было отправлять сообщения и получать оные, стоит:

  • сделать пакет controller;
  • разместить внутри класс chatController;
  • внутри ЧатКонтроллер присутствуют методы, которые отвечают за доставку сообщений от одного пользователя и трансляцию всем остальным.

Для настройки контроллера используется следующая кодификация:

Http-server на Java: изучение серверных основ на примере создания собственного чата

События и Front-End

Немаловажно настроить события подключение/отключение. Это необходимо для передачи сообщений на всеобщее обозрение:

Http-server на Java: изучение серверных основ на примере создания собственного чата

Статистика – тоже немаловажный нюанс. If юзер хочет получить полноценный чат, ему предстоит выйти за пределы Джавы.

Чтобы справиться с поставленной задачей требуется:

  • сделать папку static;
  • расположить ее по пути scr/main/resources.

Выглядеть это будет так:

Http-server на Java: изучение серверных основ на примере создания собственного чата

HTML и скрипты

В Google также говорится о том, что для работы полноценного чата требуется создать HTML-файл и JavaScript.

В первом случае используется new запись:

Http-server на Java: изучение серверных основ на примере создания собственного чата Http-server на Java: изучение серверных основ на примере создания собственного чата

Скрипт Джавы требуется для соединения с итоговой точкой, а также отправки/получения сообщений. Он будет иметь имя main.js:

Http-server на Java: изучение серверных основ на примере создания собственного чата Http-server на Java: изучение серверных основ на примере создания собственного чата Http-server на Java: изучение серверных основ на примере создания собственного чата

Стилизация

Исходники CSS можно создавать самостоятельно или подключать уже готовые варианты. Данная «опция» отвечает за внешний вид программы.

Здесь можно найти исходники соответствующего кода (пункт 7).

Запуск

Все, что теперь остается – это проверить Spring через Boot-файл с функцией main в корне иерархии имеющихся папок.

После запуска веб-сервера требуется перейти по адресу и пользоваться созданным контентом.

Для того, чтобы лучше разбираться в соответствующей сфере и термины int port, close и иные составляющие Джавы не были чем-то непонятным, стоит закончить специализированные курсы. Они без Google помогут освоить Java, а также всего его тонкости.

Создание простого HTTP-сервера на Java с использованием только Java SE API

Иногда разработчикам требуется создать очень базовый HTTP-сервер, который поддерживает только методы GET и POST. Java SE API предоставляет удобный инструментарий для работы с HTTP-клиентами, например, HttpURLConnection . Однако, разработчики часто сталкиваются с проблемой при попытке найти аналогичный инструмент для работы с HTTP-серверами.

Примером может служить ситуация, когда вам нужно создать приложение, которое обрабатывает HTTP-запросы от других приложений. При этом вы не хотите заниматься ручной обработкой HTTP-запросов и форматированием HTTP-ответов.

Использование HttpServer из com.sun.net.httpserver

Пакет com.sun.net.httpserver предоставляет базовую функциональность для создания простого HTTP-сервера. Этот пакет является частью Java SE, начиная с версии 6.

Вот пример создания простого HTTP-сервера, который слушает на порту 8000 и отвечает на все GET-запросы простым текстовым сообщением «Hello, World!»:

import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpExchange; import java.io.OutputStream; import java.io.IOException; import java.net.InetSocketAddress; public class SimpleHttpServer < public static void main(String[] args) throws Exception < HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0); server.createContext("/", new MyHandler()); server.setExecutor(null); // creates a default executor server.start(); >static class MyHandler implements HttpHandler < @Override public void handle(HttpExchange t) throws IOException < String response = "Hello, World!"; t.sendResponseHeaders(200, response.length()); OutputStream os = t.getResponseBody(); os.write(response.getBytes()); os.close(); >> >

Этот сервер поддерживает только GET-запросы. Для поддержки POST-запросов вам нужно будет добавить дополнительную логику в обработчик MyHandler .

Вместо того чтобы ручками обрабатывать HTTP-запросы и форматировать HTTP-ответы, вы можете использовать этот класс для создания простого HTTP-сервера. Однако, обратите внимание на то, что этот класс предоставляет ограниченные возможности и не подходит для создания полноценных веб-приложений.

Клиент-сервер шаг — за — шагом, от однопоточного до многопоточного (Client-Server step by step)

Цель публикации показать начинающим Java программистам все этапы создания многопоточного сервера. Для полного понимания данной темы основная информация содержится в комментариях моего кода и в выводимых в консоли сообщениях для лучшего понимания что именно происходит и в какой именно последовательности.

В начале будет рассмотрено создание элементарного клиент-сервера, для усвоения базовых знаний, на основе которых будет строиться многопоточная архитектура.

— Потоки: для того чтобы не перепутать что именно подразумевается под потоком я буду использовать существующий в профессиональной литературе синоним — нить, чтобы не путать Stream и Thread, всё-таки более профессионально выражаться — нить, говоря про Thread.

— Сокеты(Sockets): данное понятие тоже не однозначно, поскольку в какой-то момент сервер выполняет — клиентские действия, а клиент — серверные. Поэтому я разделил понятие серверного сокета — (ServerSocket) и сокета (Socket) через который практически осуществляется общение, его будем называть сокет общения, чтобы было понятно о чём речь.

 Кроме того сокетов общения создаётся по одному на каждом из обменивающихся данными приложении, поэтому сокет приложения которое имеет у себя объект - ServerSocket и первоначально открывает порт в ожидании подключения будем называть сокет общения на стороне сервера, а сокет который создаёт подключающееся к порту по известному адресу второе приложение будем называть сокетом общения на стороне клиента.

Спасибо за подсказку про Thread.sleep();!
Конечно в реальном коде Thread.sleep(); устанавливать не нужно — это моветон! В данной публикации я его использую только для того чтобы выполнение программы было нагляднее, что бы успевать разобраться в происходящем.
Так что тестируйте, изучайте и в своём коде никогда не используйте Thread.sleep();!

1) Однопоточный элементарный сервер.
2) Клиент.
3) Многопоточный сервер – сам по себе этот сервер не участвует в общении напрямую, а лишь является фабрикой однонитевых делегатов(делегированных для ведения диалога с клиентами серверов) для общения с вновь подключившимися клиентами, которые закрываются после окончания общения с клиентом.
4) Имитация множественного обращения клиентов к серверу.

Итак, начнём с изучения структуры однопоточного сервер, который может принять только одного клиента для диалога. Код приводимый ниже необходимо запускать в своей IDE в этом идея всей статьи. Предлагаю все детали уяснить из подробно задокументированного кода ниже:

  • 1) Однопоточный элементарный сервер.
import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class TestAsServer < /** * * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException < // стартуем сервер на порту 3345 try (ServerSocket server= new ServerSocket(3345))< // становимся в ожидание подключения к сокету под именем - "client" на серверной стороне Socket client = server.accept(); // после хэндшейкинга сервер ассоциирует подключающегося клиента с этим сокетом-соединением System.out.print("Connection accepted."); // инициируем каналы для общения в сокете, для сервера // канал записи в сокет DataOutputStream out = new DataOutputStream(client.getOutputStream()); System.out.println("DataOutputStream created"); // канал чтения из сокета DataInputStream in = new DataInputStream(client.getInputStream()); System.out.println("DataInputStream created"); // начинаем диалог с подключенным клиентом в цикле, пока сокет не закрыт while(!client.isClosed())< System.out.println("Server reading from channel"); // сервер ждёт в канале чтения (inputstream) получения данных клиента String entry = in.readUTF(); // после получения данных считывает их System.out.println("READ from client message - "+entry); // и выводит в консоль System.out.println("Server try writing to channel"); // инициализация проверки условия продолжения работы с клиентом по этому сокету по кодовому слову - quit if(entry.equalsIgnoreCase("quit"))< System.out.println("Client initialize connections suicide . "); out.writeUTF("Server reply - "+entry + " - OK"); out.flush(); Thread.sleep(3000); break; >// если условие окончания работы не верно - продолжаем работу - отправляем эхо-ответ обратно клиенту out.writeUTF("Server reply - "+entry + " - OK"); System.out.println("Server Wrote message to client."); // освобождаем буфер сетевых сообщений (по умолчанию сообщение не сразу отправляется в сеть, а сначала накапливается в специальном буфере сообщений, размер которого определяется конкретными настройками в системе, а метод - flush() отправляет сообщение не дожидаясь наполнения буфера согласно настройкам системы out.flush(); > // если условие выхода - верно выключаем соединения System.out.println("Client disconnected"); System.out.println("Closing connections & channels."); // закрываем сначала каналы сокета ! in.close(); out.close(); // потом закрываем сам сокет общения на стороне сервера! client.close(); // потом закрываем сокет сервера который создаёт сокеты общения // хотя при многопоточном применении его закрывать не нужно // для возможности поставить этот серверный сокет обратно в ожидание нового подключения System.out.println("Closing connections & channels - DONE."); > catch (IOException e) < e.printStackTrace(); >> >

Сервер запущен и находится в блокирующем ожидании server.accept(); обращения к нему с запросом на подключение. Теперь можно подключаться клиенту, напишем код клиента и запустим его. Клиент работает когда пользователь вводит что-либо в его консоли (внимание! в данном случае сервер и клиент запускаются на одном компьютере с локальным адресом — localhost, поэтому при вводе строк, которые должен отправлять клиент не забудьте убедиться, что вы переключились в рабочую консоль клиента!).
После ввода строки в консоль клиента и нажатия enter строка проверяется не ввёл ли клиент кодовое слово для окончания общения дальше отправляется серверу, где он читает её и то же проверяет на наличие кодового слова выхода. Оба и клиент и сервер получив кодовое слово закрывают ресурсы после предварительных приготовлений и завершают свою работу.
Посмотрим как это выглядит в коде:

import java.io.BufferedReader; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; import java.net.UnknownHostException; public class TestASClient < /** * * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException < // запускаем подключение сокета по известным координатам и нициализируем приём сообщений с консоли клиента try(Socket socket = new Socket("localhost", 3345); BufferedReader br =new BufferedReader(new InputStreamReader(System.in)); DataOutputStream oos = new DataOutputStream(socket.getOutputStream()); DataInputStream ois = new DataInputStream(socket.getInputStream()); ) < System.out.println("Client connected to socket."); System.out.println(); System.out.println("Client writing channel = oos & reading channel = ois initialized."); // проверяем живой ли канал и работаем если живой while(!socket.isOutputShutdown())< // ждём консоли клиента на предмет появления в ней данных if(br.ready())< // данные появились - работаем System.out.println("Client start writing in channel. "); Thread.sleep(1000); String clientCommand = br.readLine(); // пишем данные с консоли в канал сокета для сервера oos.writeUTF(clientCommand); oos.flush(); System.out.println("Clien sent message " + clientCommand + " to server."); Thread.sleep(1000); // ждём чтобы сервер успел прочесть сообщение из сокета и ответить // проверяем условие выхода из соединения if(clientCommand.equalsIgnoreCase("quit"))< // если условие выхода достигнуто разъединяемся System.out.println("Client kill connections"); Thread.sleep(2000); // смотрим что нам ответил сервер на последок перед закрытием ресурсов if(ois.read() >-1) < System.out.println("reading. "); String in = ois.readUTF(); System.out.println(in); >// после предварительных приготовлений выходим из цикла записи чтения break; > // если условие разъединения не достигнуто продолжаем работу System.out.println("Client sent message & start waiting for data from server. "); Thread.sleep(2000); // проверяем, что нам ответит сервер на сообщение(за предоставленное ему время в паузе он должен был успеть ответить) if(ois.read() > -1) < // если успел забираем ответ из канала сервера в сокете и сохраняем её в ois переменную, печатаем на свою клиентскую консоль System.out.println("reading. "); String in = ois.readUTF(); System.out.println(in); >> > // на выходе из цикла общения закрываем свои ресурсы System.out.println("Closing connections & channels on clentSide - DONE."); > catch (UnknownHostException e) < // TODO Auto-generated catch block e.printStackTrace(); >catch (IOException e) < // TODO Auto-generated catch block e.printStackTrace(); >> >

А что если к серверу хочет подключиться ещё один клиент!? Ведь описанный выше сервер либо находится в ожидании подключения одного клиента, либо общается с ним до завершения соединения, что делать остальным клиентам? Для такого случая нужно создать фабрику которая будет создавать описанных выше серверов при подключении к сокету новых клиентов и не дожидаясь пока делегированный подсервер закончит диалог с клиентом откроет accept() в ожидании следующего клиента. Но чтобы на серверной машине хватило ресурсов для общения со множеством клиентов нужно ограничить количество возможных подключений. Фабрика будет выдавать немного модифицированный вариант предыдущего сервера(модификация будет касаться того что класс сервера для фабрики будет имплементировать интерфейс — Runnable для возможности его использования в пуле нитей — ExecutorServices). Давайте создадим такую серверную фабрику и ознакомимся с подробным описанием её работы в коде:

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author mercenery * */ public class MultiThreadServer < static ExecutorService executeIt = Executors.newFixedThreadPool(2); /** * @param args */ public static void main(String[] args) < // стартуем сервер на порту 3345 и инициализируем переменную для обработки консольных команд с самого сервера try (ServerSocket server = new ServerSocket(3345); BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) < System.out.println("Server socket created, command console reader for listen to server commands"); // стартуем цикл при условии что серверный сокет не закрыт while (!server.isClosed()) < // проверяем поступившие комманды из консоли сервера если такие // были if (br.ready()) < System.out.println("Main Server found any messages in channel, let's look at them."); // если команда - quit то инициализируем закрытие сервера и // выход из цикла раздачии нитей монопоточных серверов String serverCommand = br.readLine(); if (serverCommand.equalsIgnoreCase("quit")) < System.out.println("Main Server initiate exiting. "); server.close(); break; >> // если комманд от сервера нет то становимся в ожидание // подключения к сокету общения под именем - "clientDialog" на // серверной стороне Socket client = server.accept(); // после получения запроса на подключение сервер создаёт сокет // для общения с клиентом и отправляет его в отдельную нить // в Runnable(при необходимости можно создать Callable) // монопоточную нить = сервер - MonoThreadClientHandler и тот // продолжает общение от лица сервера executeIt.execute(new MonoThreadClientHandler(client)); System.out.print("Connection accepted."); > // закрытие пула нитей после завершения работы всех нитей executeIt.shutdown(); > catch (IOException e) < e.printStackTrace(); >> >
  • Модифицированный Runnable сервер для запуска из предыдущего кода:
import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; public class MonoThreadClientHandler implements Runnable < private static Socket clientDialog; public MonoThreadClientHandler(Socket client) < MonoThreadClientHandler.clientDialog = client; >@Override public void run() < try < // инициируем каналы общения в сокете, для сервера // канал записи в сокет следует инициализировать сначала канал чтения для избежания блокировки выполнения программы на ожидании заголовка в сокете DataOutputStream out = new DataOutputStream(clientDialog.getOutputStream()); // канал чтения из сокета DataInputStream in = new DataInputStream(clientDialog.getInputStream()); System.out.println("DataInputStream created"); System.out.println("DataOutputStream created"); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // основная рабочая часть // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // начинаем диалог с подключенным клиентом в цикле, пока сокет не // закрыт клиентом while (!clientDialog.isClosed()) < System.out.println("Server reading from channel"); // серверная нить ждёт в канале чтения (inputstream) получения // данных клиента после получения данных считывает их String entry = in.readUTF(); // и выводит в консоль System.out.println("READ from clientDialog message - " + entry); // инициализация проверки условия продолжения работы с клиентом // по этому сокету по кодовому слову - quit в любом регистре if (entry.equalsIgnoreCase("quit")) < // если кодовое слово получено то инициализируется закрытие // серверной нити System.out.println("Client initialize connections suicide . "); out.writeUTF("Server reply - " + entry + " - OK"); Thread.sleep(3000); break; >// если условие окончания работы не верно - продолжаем работу - // отправляем эхо обратно клиенту System.out.println("Server try writing to channel"); out.writeUTF("Server reply - " + entry + " - OK"); System.out.println("Server Wrote message to clientDialog."); // освобождаем буфер сетевых сообщений out.flush(); // возвращаемся в началло для считывания нового сообщения > /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // основная рабочая часть // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // если условие выхода - верно выключаем соединения System.out.println("Client disconnected"); System.out.println("Closing connections & channels."); // закрываем сначала каналы сокета ! in.close(); out.close(); // потом закрываем сокет общения с клиентом в нити моносервера clientDialog.close(); System.out.println("Closing connections & channels - DONE."); > catch (IOException e) < e.printStackTrace(); >catch (InterruptedException e) < // TODO Auto-generated catch block e.printStackTrace(); >> >

Для имитации множественного обращения клиентов к серверу, создадим и запустим (после запуска серверной части) фабрику Runnable клиентов которые будут подключаться серверу и писать сообщения в цикле:

  • 4) Имитация множественного обращения клиентов к серверу.
import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Main < // private static ServerSocket server; public static void main(String[] args) throws IOException, InterruptedException < // запустим пул нитей в которых колличество возможных нитей ограничено - // 10-ю. ExecutorService exec = Executors.newFixedThreadPool(10); int j = 0; // стартуем цикл в котором с паузой в 10 милисекунд стартуем Runnable // клиентов, // которые пишут какое-то количество сообщений while (j < 10) < j++; exec.execute(new TestRunnableClientTester()); Thread.sleep(10); >// закрываем фабрику exec.shutdown(); > >

Как видно из предыдущего кода фабрика запускает — TestRunnableClientTester() клиентов, напишем для них код и после этого запустим саму фабрику, чтобы ей было кого исполнять в своём пуле:

import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; public class TestRunnableClientTester implements Runnable < static Socket socket; public TestRunnableClientTester() < try < // создаём сокет общения на стороне клиента в конструкторе объекта socket = new Socket("localhost", 3345); System.out.println("Client connected to socket"); Thread.sleep(2000); >catch (Exception e) < e.printStackTrace(); >> @Override public void run() < try ( // создаём объект для записи строк в созданный скокет, для // чтения строк из сокета // в try-with-resources стиле DataOutputStream oos = new DataOutputStream(socket.getOutputStream()); DataInputStream ois = new DataInputStream(socket.getInputStream())) < System.out.println("Client oos & ois initialized"); int i = 0; // создаём рабочий цикл while (i < 5) < // пишем сообщение автогенерируемое циклом клиента в канал // сокета для сервера oos.writeUTF("clientCommand " + i); // проталкиваем сообщение из буфера сетевых сообщений в канал oos.flush(); // ждём чтобы сервер успел прочесть сообщение из сокета и // ответить Thread.sleep(10); System.out.println("Client wrote & start waiting for data from server. "); // забираем ответ из канала сервера в сокете // клиента и сохраняем её в ois переменную, печатаем на // консоль System.out.println("reading. "); String in = ois.readUTF(); System.out.println(in); i++; Thread.sleep(5000); >> catch (IOException e) < // TODO Auto-generated catch block e.printStackTrace(); >catch (InterruptedException e) < // TODO Auto-generated catch block e.printStackTrace(); >> >

Запускайте, вносите изменения в код, только так на самом деле можно понять работу этой структуры.

Современная серверная разработка на языке Java: 2. Архитектура серверного приложения

В этой части мы создадим работоспособный микросервис, придерживаясь правильной архитектуры, которая обеспечивает качества, важные для промышленного проекта: гибкость, простота внесения доработок и исправления ошибок, масштабируемость.

Выполнив последующие задания, вы получите приложение с такой архитектурой.

Запросы от клиента через транспортный слой попадают в слой сервисов. Сервисы при помощи классов DAO-слоя посылают запросы к базе данных. Совершив необходимые операции, классы сервисного слоя передают их результат в транспортный слой, где формируется ответ на обработанный запрос. Иногда, операции сервисного слоя инициируются не по запросу, а по таймеру при помощи планировщика заданий.

Spring Boot

Spring – это стандартный framework для создания backend-сервисов на языке Java. Spring Boot — это расширение Spring, которое позволяет быстро подключать типовые функции приложения (web-сервер, подключение к базе данных, безопасность и т.д.) при помощи «стартеров».

Подключите библиотеки Spring Boot в проект, отредактировав файл build.gradle:

plugins < id 'java' // Плагины для Spring Boot проектов id "org.springframework.boot" version "2.6.7" id "io.spring.dependency-management" version "1.0.11.RELEASE" >group 'org.example' version '1.0-SNAPSHOT' repositories < mavenCentral() >dependencies < // Стартер для web-сервиса implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' >test

Плагины облегчают жизнь, гарантируя использование неконфликтующих между собой версий библиотек Spring Boot. Стартер spring-boot-starter-web создаст и запустит для нас готовый к эксплуатации web-сервер.

Затем необходимо задекларировать и стартовать Spring Boot приложение в файле Main.java:

package org.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; // Декларируем Spring Boot приложение @SpringBootApplication public class Main < public static void main(String[] args) < // Стартуем приложение SpringApplication.run(Main.class, args); >> 

При старте приложения мы увидим в журнале сообщений на консоли, как стартует web-сервер Tomcat:

Обратите внимание, приложение не завершается, как это было раньше. Оно работает как сервис — web-сервер ожидает запросов на порту 8080.

DTO – объекты для передачи данных

В большинстве запросов к сервисам передаются какие-то данные. Например, если мы хотим создать пользователя, то скорее всего нам надо передать в запросе на его создание хотя бы имя. Стандартом передачи данных в REST запросах являются Data Transfer Objects (DTO).

Предположим, функциональность нашего приложения будет связана с пользователями. Тогда первым делом надо написать обработчик запросов на их создание, а значит нам нужно создать соответствующий DTO-класс, чтобы передавать данные о новых пользователях.

Добавьте новый java-package, где будут размещаться DTO-классы, назовите его web.dto:

Добавьте новый класс CreateUserDto в пакет web.dto:

package org.example.web.dto; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; /** * Запрос на создание пользователя */ /** Чтобы воспользоваться DTO-классом необходим механизм десериализации - превращения JSON-строки вида в экземпляр класса CreateUserDto. Класс Builder реализует шаблон Строитель, который принято использовать в классах моделей и DTO */ @JsonDeserialize(builder = CreateUserDto.Builder.class) public class CreateUserDto < /** Имя пользователя */ private final String name; public static Builder builder() < return new Builder(); >/** * Конструктор сделан закрытым, потому что объекты этого класса * надо порождать таким образом: * dto = CreateUserDto.builder().setName("John Doe").build() */ private CreateUserDto(Builder builder) < this.name = builder.name; >public String getName() < return name; >/** * Используется при выводе сообщений на экран */ @Override public String toString() < return "'; > /** * Подсказываем механизму десериализации, * что методы установки полей начинаются с set */ @JsonPOJOBuilder(withPrefix = "set") public static class Builder < private String name; public Builder setName(String name) < this.name = name; return this; >public CreateUserDto build() < return new CreateUserDto(this); >> > 

REST-контроллеры — обработчики запросов

Стандартом для написания web-сервисов является REST-архитектура. Она крайне проста – сервис получает http-запросы, обрабатывает их и отправляет ответы. Занимаются этим REST-контроллеры приложения.

На данном этапе у нас есть работающий web-сервер и описано, как будут передаваться данные для создания пользователя. Пришла пора написать первый REST-контроллер, который будет обрабатывать запросы на создание новых пользователей.

Создайте класс WebController в java-пакете web:

package org.example.web; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * Обработчик web-запросов */ @RestController public class WebController < /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@RequestBody CreateUserDto createUserDto) < /** * Получили запрос на создание пользователя, * пока можем только залогировать этот факт */ LOGGER.info("Create user request received: <>", createUserDto); > > 

Создайте в проекте папку http-test, а в ней создайте файл test.http:

### Запрос на создание пользователя POST http://localhost:8080/users Content-type: application/json

Запустите приложение, а затем тестовый POST-запрос в файле test.http

В результате запуска запроса вы должны увидеть ответ Response code: 200 – это значит, что запрос выполнился успешно. В консоли приложения будет выведено сообщение: Create user request received: – это значит, что запрос «дошел» до приложения. Но на данном этапе мы пока не можем ничего сделать – нам негде хранить пользователей.

Валидация данных

Наше приложение уже умеет получать запросы на создание пользователей. Но, прежде чем приступить к обработке запроса, было бы неплохо проверить пришедшие данные на корректность. Предположим, мы хотим, чтобы в имени пользователя было от 5 до 25 символов и присутствовали только буквы латинского алфавита.

Добавьте стартер валидации в файл build.gradle:

plugins < id 'java' // Плагины для Spring Boot проектов id "org.springframework.boot" version "2.6.7" id "io.spring.dependency-management" version "1.0.11.RELEASE" >group 'org.example' version '1.0-SNAPSHOT' repositories < mavenCentral() >dependencies < // Стартер для web-сервиса implementation 'org.springframework.boot:spring-boot-starter-web' // Стартер для валидации implementation 'org.springframework.boot:spring-boot-starter-validation' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' >test

Добавьте правила валидации на поле name в классе CreateUserDto:

package org.example.web.dto; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; /* Запрос на создание пользователя */ /** Чтобы воспользоваться DTO-классом необходим механизм десериализации - превращения JSON-строки вида в экземпляр класса CreateUserDto. Класс Builder реализует шаблон Строитель, который принято использовать в классах моделей и DTO */ @JsonDeserialize(builder = CreateUserDto.Builder.class) public class CreateUserDto < /** * Имя пользователя * Ключ "name" - обязательный * Длина - от 5 до 25 символов * Может содержать только символы латинского алфавита */ @NotNull(message = "Key 'name' is mandatory") @Length(min = 5, max = 25, message = "Name length must be from 5 to 25") @Pattern(regexp = "^[a-zA-Z]+$", message = "Name must contain only letters a-z and A-Z") private final String name; public static Builder builder() < return new Builder(); >/** * Конструктор сделан закрытым, потому что объекты этого класса * надо порождать таким образом: * dto = CreateUserDto.builder().setName("John Doe").build() */ private CreateUserDto(Builder builder) < this.name = builder.name; >public String getName() < return name; >/** * Используется при выводе сообщений на экран */ @Override public String toString() < return "'; > /** * Подсказываем механизму десериализации, * что методы установки полей начинаются с set */ @JsonPOJOBuilder(withPrefix = "set") public static class Builder < private String name; public Builder setName(String name) < this.name = name; return this; >public CreateUserDto build() < return new CreateUserDto(this); >> > 

Добавьте проверку входного параметра метода createUser в классе WebController:

package org.example.web; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; /** * Обработчик web-запросов */ @RestController public class WebController < /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@Valid @RequestBody CreateUserDto createUserDto) < /* Получили запрос на создание пользователя, пока можем только залогировать этот факт */ LOGGER.info("Create user request received: <>", createUserDto); > > 

Запустите приложение и попробуйте послать тестовые запросы в файле http.test с разными вариантами значения поля name. Убедитесь, что при допустимых значениях код ответа равен 200 (успех), иначе – 400 (некорректный запрос).

Модель данных

Итак, наше приложение умеет получать запросы на создание пользователя и проверяет полученные данные на корректность. Пора каким-то образом сохранить нового пользователя в базе данных. Но прежде создадим класс модели пользователя.

Создайте java-пакет model, а в нем java-класс UserInfo, для операций над сущностью пользователя:

package org.example.model; /* Информация о пользователе */ public class UserInfo < /** * Имя пользователя */ private final String name; public static Builder builder() < return new Builder(); >/** * Конструктор сделан закрытым, потому что объекты этого класса * надо порождать таким образом: * dto = User.builder().setName("John Doe").build() */ private UserInfo(Builder builder) < this.name = builder.name; >public String getName() < return name; >/** * Используется при выводе сообщений на экран */ @Override public String toString() < return "'; > public static class Builder < private String name; public Builder setName(String name) < this.name = name; return this; >public UserInfo build() < return new UserInfo(this); >> > 

Внимательный читатель может заметить, что класс UserInfo очень похож на класс CreateUserDto. Неудивительно – если честно, я создал его копированием, удалив аннотации и поправив комментарии. Зачем в приложении два почти одинаковых класса?

CreateUserDto – это класс транспортного слоя для передачи данных, а UserInfo – это класс для оперирования сущностью пользователя на уровне бизнес-логики. Их одинаковость – это временное состояние. В дальнейшем, по мере появления новых требований к транспорту, может меняться класс CreateUserDto, а по мере появления нового бизнес-функционала будет дополнятся класс UserInfo. Иногда эти изменения синхронны, иногда – нет, и классы начнут все больше отличаться.

Транспортные классы отделены в транспортный слой от остального приложения – это признак хорошей архитектуры. В данном случае транспортный слой находится в пакете web. Зачем это нужно?

Представьте, что мы написали отличный менеджер пользователей, но в какой-то момент весь проект включили в платформу, где уже есть соглашение о том, как передаются данные о пользователях, и вам надо работать по указанному протоколу. Например, в целевой платформе вместо HTTP используется обмен сообщениями при помощи Kafka, или вместо ключа name в их системах ходят запросы с ключом userName.

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

Если транспортные классы вашего приложения «проникли» куда-то за пределы транспортной логики, то придется переписывать все приложение. Придется иметь несколько версий приложения: «старую» и «новую», и дорабатывать их параллельно. А это уже не просто дублирование кода – это грозит дублированием всего процесса ведения доработок: постановка, разработка, тестирование.

Но если вы «выдержали» архитектуру, то в вашем приложении просто появится новый транспортный сервис, а бизнес-логику можно будет переиспользовать. Как это делается, будет продемонстрировано позже.

Liquibase — создание базы данных и подключение к ней

На этом этапе у нас уже есть модель сущности пользователя, которого мы хотим сохранить, но пока нет базы данных для этого. Воспользуемся библиотекой Liquibase для создания базы данных.

В промышленных приложениях используют СУБД PostgreSQL, Oracle или MS SQL Server, но мы воспользуемся H2, которая отлично подходит для учебных целей и может создать эфемерную базу данных в оперативной памяти при каждом запуске приложения.

Подключите стартер JDBC, библиотеку Liqubase и библиотеку H2 в файле build.gradle:

. dependencies < . // Стартер jdbc implementation 'org.springframework.boot:spring-boot-starter-jdbc' // Библиотеки Liquibase implementation 'org.liquibase:liquibase-core:4.9.1' // Библиотеки H2 implementation 'com.h2database:h2:2.1.212' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' >. 

Создайте файл application.yml в каталоге resources, указав параметры подключения к базе данных:

db: driverClassName: org.h2.Driver url: jdbc:h2:mem:user_db;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS user_db username: admin password: admin maxPoolSize: 10 

В каталоге resources создайте каталог db, а в нем файл changelog.xml, по которому liquibase создаст для нас таблицу user_info:

    create table user_info ( name varchar(25) primary key );   

Создайте java-пакет configuration, а в нем класс DataBaseConfiguration, который обеспечит для приложения возможность отправлять запросы к базе данных:

package org.example.configuration; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import liquibase.integration.spring.SpringLiquibase; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; import javax.sql.DataSource; /** * Конфигурация компонентов для работы с БД */ @Configuration @EnableTransactionManagement public class DatabaseConfiguration < @Value("$") private String driver = "org.postgresql.Driver"; @Value("$") private int poolLimit = 10; private final String dbUrl; private final String userName; private final String userPassword; @Autowired public DatabaseConfiguration(@Value("$") String userName, @Value("$") String userPassword, @Value("$") String dbUrl) < this.userName = userName; this.userPassword = userPassword; this.dbUrl = dbUrl; >@Bean(destroyMethod = "close") public HikariDataSource hikariDataSource() < HikariConfig config = new HikariConfig(); config.setDriverClassName(driver); config.setJdbcUrl(dbUrl); config.setUsername(userName); config.setPassword(userPassword); config.setMaximumPoolSize(poolLimit); return new HikariDataSource(config); >@Bean public TransactionAwareDataSourceProxy transactionAwareDataSource() < return new TransactionAwareDataSourceProxy(hikariDataSource()); >@Bean public DataSourceTransactionManager dataSourceTransactionManager() < return new DataSourceTransactionManager(transactionAwareDataSource()); >@Bean public TransactionTemplate transactionTemplate() < return new TransactionTemplate(dataSourceTransactionManager()); >@Bean public JdbcTemplate jdbcTemplate() < return new JdbcTemplate(hikariDataSource()); >@Bean public NamedParameterJdbcTemplate namedParameterJdbcTemplate() < return new NamedParameterJdbcTemplate(jdbcTemplate()); >@Bean @ConfigurationProperties(prefix = "spring.datasource.liquibase") public LiquibaseProperties mainLiquibaseProperties() < LiquibaseProperties liquibaseProperties=new LiquibaseProperties(); liquibaseProperties.setChangeLog("classpath:/db/changelog.xml"); return liquibaseProperties; >@Bean public SpringLiquibase springLiquibase() < LiquibaseProperties liquibaseProperties = mainLiquibaseProperties(); return createSpringLiquibase(hikariDataSource(), liquibaseProperties); >private SpringLiquibase createSpringLiquibase(DataSource source, LiquibaseProperties liquibaseProperties) < return new SpringLiquibase() < < setDataSource(source); setDropFirst(liquibaseProperties.isDropFirst()); setContexts(liquibaseProperties.getContexts()); setChangeLog(liquibaseProperties.getChangeLog()); setDefaultSchema(liquibaseProperties.getDefaultSchema()); setChangeLogParameters(liquibaseProperties.getParameters()); setShouldRun(liquibaseProperties.isEnabled()); setRollbackFile(liquibaseProperties.getRollbackFile()); setLabels(liquibaseProperties.getLabels()); >>; > > 

Если все сделано правильно, то в журнале сообщений при старте приложения будет запись:

ChangeSet db/changelog.xml::user::dev ran successfully

Это означает, что в памяти была создана база данных user_db, а в ней таблица user_info, в которой мы будем сохранять данные о пользователях.

DAO — отправка запросов к базе данных

Благодаря Liquibase наше приложение обеспечено базой данных с необходимой нам таблицей user_info. Пришла пора научиться записывать туда данные. Делается это при помощи Data Access Object (DAO) – специализированного класса, который принято выносить в отдельный DAO-слой приложения.

Создайте java-пакет dao и в нем класс UserInfoDao, который будет отвечать за отправку запросов к таблице user_info:

package org.example.dao; import org.example.dao.mapper.UserInfoRowMapper; import org.example.model.UserInfo; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; /** * Запросы к таблице user_info */ public class UserInfoDao < /** * Объект для отправки SQL-запросов к БД */ private final NamedParameterJdbcTemplate jdbcTemplate; public UserInfoDao(NamedParameterJdbcTemplate jdbcTemplate) < this.jdbcTemplate = jdbcTemplate; >/** * Создает запись о пользователе в БД * @param userInfo информация о пользователе */ public void createUser(UserInfo userInfo) < jdbcTemplate.update( "INSERT INTO user_info (name) VALUES (:name) ", new MapSqlParameterSource("name", userInfo.getName()) ); >/** * Возращает информацию о пользователе по имени * @param userName имя пользователя * @return информация о пользователе */ public UserInfo getUserByName(String userName) < return jdbcTemplate.queryForObject("SELECT * FROM user_info WHERE name = :name", new MapSqlParameterSource("name", userName), new UserInfoRowMapper() ); >/** * Удаляет пользователя из БД * @param userName имя пользователя */ public void deleteUser(String userName) < jdbcTemplate.update( "DELETE FROM user_info WHERE name = :name", new MapSqlParameterSource("name", userName) ); >> 

DAO-классу UserInfoDao необходим вспомогательный класс, отвечающий за преобразование записи из таблицы БД в java-класс UserInfo. В пакете dao создайте пакет mapper и в нем класс UserInfoRowMapper:

package org.example.dao.mapper; import org.example.model.UserInfo; import org.springframework.jdbc.core.RowMapper; import java.sql.ResultSet; import java.sql.SQLException; /** * Трансляция записи из таблицы user_info в java-класс UserInfo * * Используется в */ public class UserInfoRowMapper implements RowMapper  < /** * Возвращает информацию о пользователе * @param rs запись в таблице user_info * @param rowNum номер записи * @return информация о пользователе * @throws SQLException если в таблице нет колонки */ @Override public UserInfo mapRow(ResultSet rs, int rowNum) throws SQLException < return UserInfo.builder() .setName(rs.getString("name")) .build(); >> 

Мы описали DAO-класс, теперь надо добавить конфигурацию, по которой Spring Boot создаст при старте приложения “bean” – экземпляр этого класса. В java-пакете configuration создайте класс DaoConfiguration:

package org.example.configuration; import org.example.dao.UserInfoDao; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; /** * Создание "бинов" DAO-классов */ @Configuration public class DaoConfiguration < @Bean UserInfoDao userInfoDao(NamedParameterJdbcTemplate jdbcTemplate) < return new UserInfoDao(jdbcTemplate); >> 

Добавьте в класс WebController использование класса UserInfoDao для работы с БД:

package org.example.web; import org.example.dao.UserInfoDao; import org.example.model.UserInfo; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** * Обработчик web-запросов */ @RestController public class WebController < /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Объект для операциями с БД * TODO: Позже надо перейти на использование сервисного слоя */ private final UserInfoDao userInfoDao; /** * Инъекция одних объектов в другие происходит через конструктор * и обеспечивается библиотеками Spring */ public WebController(UserInfoDao userInfoDao) < this.userInfoDao = userInfoDao; >/** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@Valid @RequestBody CreateUserDto createUserDto) < LOGGER.info("Create user request received: <>", createUserDto); /** * Сохраняем пользователя, преобразуя DTO в модель */ userInfoDao.createUser( UserInfo.builder().setName(createUserDto.getName()).build() ); > /** * Обработчик запросов на получение информации о пользователе * @param userName имя пользователя * @return информация о пользователе */ @GetMapping("/users/") public UserInfo getUserInfo(@PathVariable String userName) < return userInfoDao.getUserByName(userName); >/** * Обработчик запросов на удаление пользователя * @param userName имя пользователя */ @DeleteMapping("/users/") public void deleteUser(@PathVariable String userName) < userInfoDao.deleteUser(userName); >> 

Дополните тестовый файл test.http новыми запросами:

### Запрос на создание пользователя POST http://localhost:8080/users Content-type: application/json < "name": "JohnDoe" >### Запрос информации о пользователе GET http://localhost:8080/users/JohnDoe ### Запрос на удаление пользователя DELETE http://localhost:8080/users/JohnDoe 

Выполните запросы последовательно. Если все сделано правильно, будут получены ответы с кодом 200. Наше приложение теперь умеет: сохранять информацию о пользователе, возвращать ее по запросу, удалять информацию о пользователе.

Сервисный слой и бизнес-логика

Приложение работает, но имеет пока скрытую архитектурную проблему – обращение к DAO-классу происходит напрямую из транспортного слоя.

Предположим, мы хотим избежать появления пользователей с именами типа «administrator», «root» или «system». Или, прежде чем посылать запросы на создание и удаление пользователя, неплохо было бы проверить его наличие в БД.

Писать эту логику в транспортном слое нельзя – при появлении нового транспорта, этот фрагмент кода придется дублировать.

Добавлять эту проверку в UserInfoDao тоже не стоит потому что:

  1. Нарушается принцип единой ответственности, класс начинает терять свою специализацию «работа с таблицей user_info»
  2. DAO-классы – это тоже, в известном смысле, «деталь» приложения, которую, возможно, придется заменить или дополнить при переходе на новую СУБД. Это будет сложнее сделать, если код утяжелен какой-то дополнительной логикой, кроме отсылки SQL-запроса.

Для написания подобной «бизнес-логики» правильно будет создать отдельный «сервисный» слой – это смысловое ядро приложения, вокруг которого крутятся сравнительно легко заменяемые «детали»: транспорт, база данных, клиенты других сервисов т.д.

Добавьте java-пакет service и создайте в нем класс UserInfoService, который будет отвечать за бизнес-операции над сущностью пользователя:

package org.example.service; import org.example.dao.UserInfoDao; import org.example.model.UserInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.EmptyResultDataAccessException; import java.util.Set; /** * Бизнес-логика работы с пользователями */ public class UserInfoService < private static final Logger LOGGER = LoggerFactory.getLogger(UserInfoService.class); /** * Объект для работы с таблице user_info */ private final UserInfoDao userInfoDao; /** * Иньекция испольуземых объектов через конструктор * @param userInfoDao объект для работы с таблице user_info */ public UserInfoService(UserInfoDao userInfoDao) < this.userInfoDao = userInfoDao; >/** * Создание пользователя * @param userInfo информация о пользователе */ public void createUser(UserInfo userInfo) < checkNameSuspicious(userInfo.getName()); if (!isUserExists(userInfo.getName())) < userInfoDao.createUser(userInfo); LOGGER.info("User created by user info: <>", userInfo); > else < // TODO Заменить на своё исключение RuntimeException exception = new RuntimeException("User already exists with name " + userInfo.getName()); LOGGER.error("Error creating user by user info <>", userInfo, exception); throw exception; > > /** * Возвращает информацию о пользователе по его имени * @param userName имя пользователя * @return информация о пользователе */ public UserInfo getUserInfoByName(String userName) < try < return userInfoDao.getUserByName(userName); >catch (EmptyResultDataAccessException e) < LOGGER.error("Error getting info by name <>", userName, e); // TODO Заменить на своё исключение throw new RuntimeException("User not found by name " + userName); > > /** * Удаление пользователя * @param userName имя пользователя */ public void deleteUser(String userName) < if (isUserExists(userName)) < userInfoDao.deleteUser(userName); LOGGER.info("User with name <>deleted", userName); > > /** * Проверка на сущестование пользователя с именем * @param userName имя пользователя * @return true - если пользователь сущестует, иначе - false */ private boolean isUserExists(String userName) < try < userInfoDao.getUserByName(userName); return true; >catch (EmptyResultDataAccessException e) < return false; >> /** * Проверка на то, что имя пользователя не содержится в стоп-листе * @param userName имя пользователя */ private void checkNameSuspicious(String userName) < if (Set.of("administrator", "root", "system").contains(userName)) < // TODO: Заменить на свое исключение RuntimeException exception = new RuntimeException(userName + " is unacceptable"); LOGGER.error("Check name failed", exception); throw exception; >> > 

Замените использование dao-объекта на использование сервисного класса в WebController:

package org.example.web; import org.example.model.UserInfo; import org.example.service.UserInfoService; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** * Обработчик web-запросов */ @RestController public class WebController < /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Объект для работы с информацией о пользователе */ private final UserInfoService userInfoService; /** * Иньекция одних объектов в другие происходит через конструктор * и обеспечивается библиотеками Spring */ public WebController(UserInfoService userInfoService) < this.userInfoService = userInfoService; >/** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@Valid @RequestBody CreateUserDto createUserDto) < LOGGER.info("Create user request received: <>", createUserDto); /** * Сохраняем пользователя, преобразуя DTO в модель */ userInfoService.createUser( UserInfo.builder().setName(createUserDto.getName()).build() ); > /** * Обработчик запросов на получение информации о пользователе * @param userName имя пользователя * @return информация о пользователе */ @GetMapping("/users/") public UserInfo getUserInfo(@PathVariable String userName) < LOGGER.info("Get user info request received userName=<>", userName); return userInfoService.getUserInfoByName(userName); > /** * Обработчик запросов на удаление пользователя * @param userName имя пользователя */ @DeleteMapping("/users/") public void deleteUser(@PathVariable String userName) < LOGGER.info("Delete user info request received userName=<>", userName); userInfoService.deleteUser(userName); > > 

Запустите последовательно три тестовых запроса. Если все сделано правильно, будут получены ответы с кодом 200. Обратите внимание на записи в консоли приложения:

Create user request received:
User created by user info:
Get user info request received userName=JohnDoe
Delete user info request received userName=JohnDoe
User with name JohnDoe deleted

Они могут быт очень полезными при диагностике неполадок. Общие рекомендации по логгированию такие:

  1. Информационное сообщение сразу при получении запроса с выводом содержимого запроса.
  2. Информационное сообщение об успешности операции перед выходом из метода в сервисном классе.
  3. Сообщение об ошибке сразу после catch. Не забудьте показать само исключение.
  4. Сообщение об ошибке перед throw, если не было catch
  5. Отладочные сообщения в сложных алгоритмах

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

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