This sponsored post features a product relevant to our readers while meeting our editorial guidelines for being objective and educational.
Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)
В этом уроке мы будем использовать Spring Boot для среды веб-разработки, Websockets для обмена данными в реальном времени, Tomcat для контейнера приложений Java, Gradle для создания и управления зависимостями, Thymeleaf для рендеринга шаблонов, MongoDB для хранения данных и, наконец, там не будет XML для конфигураций bean-компонентов. Чтобы вдохновить вас, в конце этой статьи вы увидите полностью работающее приложение, подобное показанному ниже.



1. Сценарий
- Доу открывает страницу чата, чтобы общаться со своими друзьями.
- Ему предлагается выбрать прозвище.
- Он входит на страницу чата и отправляет сообщение. Сообщение отправляется на ендпоинт Spring MVC для сохранения в базу данных и трансляции.
- Указанный ендпоинт обрабатывает сообщение и передает это сообщение всем клиентам, подключенным к системе чата.
2. Зависимости и конфигурация Gradle
Прежде чем приступить к внутренней структуре проекта, позвольте мне объяснить, какие библиотеки мы будем использовать для перечисленных выше функций проекта, и управлять ими, используя Gradle. Когда вы клонируете проект из GitHub, вы увидите файл с именем build.gradle
в корневом каталоге проекта, как показано ниже.
buildscript { repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.4.RELEASE") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'spring-boot' apply plugin: 'war' jar { baseName = 'realtime-chat' version = '0.1.0' } war { baseName = 'ROOT' } sourceCompatibility = 1.7 targetCompatibility = 1.7 repositories { mavenCentral() } sourceCompatibility = 1.7 targetCompatibility = 1.7 dependencies { providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("org.springframework.boot:spring-boot-starter-data-mongodb") compile("org.springframework.boot:spring-boot-starter-websocket") compile("org.springframework:spring-messaging") testCompile("junit:junit") } task wrapper(type: Wrapper) { gradleVersion = '2.3' }
Я не буду погружаться в внутренности Gradle, но позвольте мне объяснить те части, которые нам нужны для нашего проекта. Spring Boot создан в основном для разработки автономных приложений в формате jar
. В нашем проекте мы создадим проект war
, а не jar
. Это потому, что для Modulus нужен war файл для автоматического развертывания проекта в облаке.
Чтобы создать war файл, мы применили apply plugin: 'war'
. Modulus также ожидает, что название будет ROOT.war
по умолчанию, и именно поэтому мы использовали:
war { baseName: 'ROOT.war' }
Когда вы запускаете задачу build
Gradle, она будет генерировать war файл для развертывания в контейнере Tomcat. И, наконец, как вы можете догадаться, раздел зависимостей предназначен для сторонних библиотек для конкретных действий.
Это все для раздела зависимостей проекта, и вы можете обратиться к руководству Gradle за дополнительной информацией о Gradle.
3. Дизайн
Если вы хотите разработать хорошее приложение, лучше всего определить структуру проекта небольшими частями. Вы можете увидеть фрагменты всей архитектуры нашего приложения.
3.1. Модель
Мы разрабатываем чат-приложение, поэтому можем сказать, что у нас есть модель ChatMessageModel
(т. е. Объект домена). Хотя мы сохраняем и просматриваем детали сообщения чата, мы можем отбросить объект чата от или к этой ChatMessageModel
. Кроме того, мы можем использовать модель User
для пользователей чата, но чтобы упростить приложение, мы будем использовать только nickname
как текст. Модель ChatMessageModel
имеет следующие поля: text
, author
и createDate
. Представление класса этой модели выглядит следующим образом:
package realtime.domain; import org.springframework.data.annotation.Id; import java.util.Date; /** * @author huseyinbabal */ public class ChatMessageModel { @Id private String id; private String text; private String author; private Date createDate; public ChatMessageModel() { } public ChatMessageModel(String text, String author, Date createDate) { this.text = text; this.author = author; this.createDate = createDate; } public String getText() { return text; } public void setText(String text) { this.text = text; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } @Override public String toString() { return "{" + "\"id\":\"" + id + '\"' + ",\"text\":\"" + text + '\"' + ",\"author\":\"" + author + '\"' + ",\"createDate\":\"" + createDate + "\"" + '}'; } }
Этот объект домена помогает нам представлять сообщение чата как JSON при необходимости. Наша модель в порядке, поэтому давайте продолжим работу с контроллерами.
3.2. контроллер
Контроллер - это поведение вашего приложения. Это означает, что вам необходимо просто и легко контролировать ваш контроллер с моделями доменов и другими службами. Мы ожидаем, что наши контроллеры будут обрабатывать:
- Запросы на сохранение сообщений чата
- Список последних сообщений чата
- Обслуживание страницы приложения чата
- Обслуживание страницы входа в систему
- Передача сообщений чата клиентам
Здесь вы можете увидеть общие ендпоинты:
package realtime.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import realtime.domain.ChatMessageModel; import realtime.message.ChatMessage; import realtime.repository.ChatMessageRepository; import java.util.Date; import java.util.List; /** * @author huseyinbabal */ @Controller public class ChatMessageController { @Autowired private ChatMessageRepository chatMessageRepository; @RequestMapping("/login") public String login() { return "login"; } @RequestMapping("/chat") public String chat() { return "chat"; } @RequestMapping(value = "/messages", method = RequestMethod.POST) @MessageMapping("/newMessage") @SendTo("/topic/newMessage") public ChatMessage save(ChatMessageModel chatMessageModel) { ChatMessageModel chatMessage = new ChatMessageModel(chatMessageModel.getText(), chatMessageModel.getAuthor(), new Date()); ChatMessageModel message = chatMessageRepository.save(chatMessage); List<ChatMessageModel> chatMessageModelList = chatMessageRepository.findAll(new PageRequest(0, 5, Sort.Direction.DESC, "createDate")).getContent(); return new ChatMessage(chatMessageModelList.toString()); } @RequestMapping(value = "/messages", method = RequestMethod.GET) public HttpEntity list() { List<ChatMessageModel> chatMessageModelList = chatMessageRepository.findAll(new PageRequest(0, 5, Sort.Direction.DESC, "createDate")).getContent(); return new ResponseEntity(chatMessageModelList, HttpStatus.OK); } }
Первая и вторая ендпоинты предназначены только для того, чтобы обслуживать страницу входа в систему и главную страницу чата. Третье действие - обработка нового хранилища сообщений и трансляции чата. После того, как сообщение будет сохранено, оно будет отправлено клиентам через канал /topic/message
. Чтобы хранить данные сообщений в MongoDB, мы будем использовать репозиторий MongoDB.
Как вы можете видеть, существуют два типа ендпоинтов /messages
: GET и POST. Когда вы отправляете POST-запрос на ендпоинт /messages
с надлежащим пейлоадом к сообщению, он автоматически переходит в класс ChatMessageModel, и сообщение будет сохранено в MongoDB. После успешного сохранения он автоматически будет перенаправлен клиентам. Но как? В этом экшене есть аннотация @SendTo("/topic/newMessage")
Это отправит содержимое, возвращаемое функцией, клиентам. И возвращаемый контент выглядит следующим образом:
... return new ChatMessage(chatMessageModelList.toString()); ...
Это последнее сообщение из базы данных:



Вышеупомянутое сообщение будет преобразовано в формат для обмена через WebSocket. Это сообщение канала будет обрабатываться на стороне клиента со сторонней библиотекой JavaScript, и оно будет обрабатываться в следующих разделах.
Для операций db сообщений используется spring-boot-starter-data-mongodb
. Эта библиотека помогает нам в работе с репозиториями, а создание объекта репозитория для MongoDB очень просто. Ниже приведен пример ChatMessageRepository
:
package realtime.repository; import org.springframework.data.mongodb.repository.MongoRepository; import realtime.domain.ChatMessageModel; import java.util.List; /** * @author huseyinbabal */ public interface ChatMessageRepository extends MongoRepository<ChatMessageModel, String> { List<ChatMessageModel> findAllByOrderByCreateDateAsc(); }
Если вы создаете интерфейс и расширяете MongoRepository <?, String>
, вы сможете автоматически использовать операции CRUD, такие как find()
, findAll()
, save()
и т.д.
Как вы можете видеть, MongoRepository
ожидает объект домена. Мы уже определили эту модель в разделе «Модель» учебника. В этом репозитории мы определили пользовательскую функцию findAllByOrderByCreateDateAsc()
.
Если вы когда-либо использовали JPA прежде, то сможете это легко понять, но позвольте мне кратко объяснить. Если вы определяете имя функции в интерфейсе, который расширяет MongoRepository
, это имя функции будет автоматически парсится на запрос в Spring. Это будет что-то вроде:
SELECT * FROM ChatMessageModel WHERE 1 ORDER BY createDate ASC
В ChatMessageController
мы использовали эту функцию, а также использовали функции по умолчанию MongoRepository
:
chatMessageRepository.findAll(new PageRequest(0, 5, Sort.Direction.DESC, "createDate")).getContent()
findAll
используется параметр для сортировки и разбивки на страницы. Вы можете посмотреть руководство на веб-сайте Spring для получения более подробной информации о Spring JPA.
3.3. View
В части View у нас есть только две страницы. Одна из них - это страница входа в систему, чтобы получить псевдоним пользователя, а вторая - главная страница чата для отправки сообщений пользователям чата.
Как вы можете видеть в разделе контроллера выше, они отображаются с использованием двух ендпоинтов, /login
и /chat
. Для создания интерактивных страниц мы будем использовать некоторые сторонние библиотеки JavaScript. Мы будем использовать их с CDN-страниц. Вы можете увидеть страницу входа ниже:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"/> <title></title> <script src="//code.jquery.com/jquery-1.11.1.js"></script> <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script> <script> $(function(){ if ($.cookie("realtime-chat-nickname")) { window.location = "/chat" } else { $("#frm-login").submit(function(event) { event.preventDefault(); if ($("#nickname").val() !== '') { $.cookie("realtime-chat-nickname", $("#nickname").val()); window.location = "/chat"; } }) } }) </script> <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"/> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet"/> </head> <body> <div class="container" style="padding-top: 50px"> <div class="row"> <div class="col-md-4 col-md-offset-4"> <div class="login-panel panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Choose a nickname to enter chat</h3> </div> <div class="panel-body"> <form role="form" id="frm-login"> <fieldset> <div class="form-group"> <input class="form-control" placeholder="Enter Nickname" name="nickname" id="nickname" type="text" autofocus="" required=""/> </div> <button type="submit" class="btn btn-lg btn-success btn-block">Enter Chat</button> </fieldset> </form> </div> </div> </div> </div> </div> </body> </html>
На странице входа у нас есть примерное текстовое поле псевдонима. Когда вы нажмете Enter Chat, ваш ник будет сохранен в файл cookie. Этот псевдоним будет использоваться для установки поля автора сообщений чата. Когда вы нажмете Enter Chat, откроется страница чата. Если вы уже вошли в систему и перешли на страницу входа в систему, вы будете перенаправлены на страницу чата.
Вот страница чата:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"/> <title></title> <script src="//code.jquery.com/jquery-1.11.1.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script> <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.4.0/jquery.timeago.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.0.0/sockjs.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> <script> var stompClient = null; function connect() { var socket = new SockJS('/newMessage'); stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { stompClient.subscribe('/topic/newMessage', function(message){ refreshMessages(JSON.parse(JSON.parse(message.body).content)); }); }); } function disconnect() { if (stompClient != null) { stompClient.disconnect(); } } function refreshMessages(messages) { $(".media-list").html(""); $.each(messages.reverse(), function(i, message) { $(".media-list").append('<li class="media"><div class="media-body"><div class="media"><div class="media-body">' + message.text + '<br/><small class="text-muted">' + message.author + ' | ' + new Date(message.createDate) + '</small><hr/></div></div></div></li>'); }); } $(function(){ if (typeof $.cookie("realtime-chat-nickname") === 'undefined') { window.location = "/login" } else { connect(); $.get("/messages", function (messages) { refreshMessages(messages) }); $("#sendMessage").on("click", function() { sendMessage() }); $('#messageText').keyup(function(e){ if(e.keyCode == 13) { sendMessage(); } }); } function sendMessage() { $container = $('.media-list'); $container[0].scrollTop = $container[0].scrollHeight; var message = $("#messageText").val(); var author = $.cookie("realtime-chat-nickname"); stompClient.send("/app/newMessage", {}, JSON.stringify({ 'text': message, 'author': author})); $("#messageText").val("") $container.animate({ scrollTop: $container[0].scrollHeight }, "slow"); } }) </script> <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"/> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet"/> <style type="text/css"> .fixed-panel { min-height: 500px; max-height: 500px; } .media-list { overflow: auto; } </style> </head> <body> <div class="container"> <div class="row " style="padding-top:40px;"> <h3 class="text-center">Realtime Chat Application with Spring Boot, Websockets, and MongoDB </h3> <br/><br/> <div class="col-md-12"> <div class="panel panel-info"> <div class="panel-heading"> <strong><span class="glyphicon glyphicon-list"></span> Chat History</strong> </div> <div class="panel-body fixed-panel"> <ul class="media-list"> </ul> </div> <div class="panel-footer"> <div class="input-group"> <input type="text" class="form-control" placeholder="Enter Message" id="messageText" autofocus=""/> <span class="input-group-btn"> <button class="btn btn-info" type="button" id="sendMessage">SEND <span class="glyphicon glyphicon-send"></span></button> </span> </div> </div> </div> </div> </div> </div> </body> </html>
Эта страница предназначена для простого просмотра и отправки сообщений. Сообщения доставляются на эту страницу через WebSockets. На этой странице вы можете увидеть sockjs
и stompjs
. Это касается обработки уведомлений. Всякий раз, когда приходит новое сообщение, последняя область сообщений повторно заполняется.
Кстати, когда вы впервые открываете страницу чата, последние сообщения будут извлекаться в области сообщений. Как видно на стороне JavaScript, наш канал сообщений - newMessage
. Итак, мы слушаем этот канал, и когда вы нажимаете кнопку Send, сообщение в текстовом поле будет отправлено на ендпоинт, и это сообщение будет передано подключенным клиентам после успешного хранения.
Как вы можете видеть, архитектура программного обеспечения здесь очень проста и легка в разработке. У нас есть готовый к продакшену код, и давайте зальем его в Modulus.
Модуль является одним из лучших PaaS для развертывания, масштабирования и мониторинга вашего приложения на выбранном вами языке.
4. Развертывание
4.1. Предпосылки
Перед развертыванием приложения создадим базу данных с помощью панели администрирования Modulus. Вам нужна учетная запись модуля для создания и развертывания dba, поэтому, пожалуйста, создайте учетную запись, если у вас ее нет.
Перейдите на панель инструментов Modulus и создайте базу данных:



На экране создания базы данных укажите имя базы данных, выберите версию MongoDB (я использовал 2.6.3, так что будет лучше, если вы выберете также 2.6.3) и, наконец, определите пользователя для применения операций чтения / записи базы данных.



После успешного создания базы данных вы можете получить URL MongoDB. Мы будем использовать URL MongoDB в переменных среды, которые будут использоваться приложением Spring Boot.
Чтобы установить переменные среды для MongoDB, вам необходимо иметь приложение. Перейдите на Dashboard и нажмите на Projects. На этой странице нажмите Create New Project.
Чтобы продолжить настройку переменных среды, перейдите в Dashboard и нажмите Projects. Выберите свой проект и нажмите Administration. Прокрутите страницу вниз и установите переменные окружения с помощью ключа SPRING_DATA_MONGODB_URI
и значения вашего URI базы данных:



При развертывании приложения Spring будет использовать значение переменной среды. Мы выполнили требования и продолжим работу с частью развертывания.
4.2. Развертывание с помощью CLI
Чтобы развернуть проект, выполните build задачу gradle:
gradle build
Эта задача создаст файл war ROOT.war
. Скопируйте этот файл в новую папку и установите CLI modulus, если вы этого не сделали.
npm install -g modulus
Войдите в систему;
modulus login
Теперь выполните следующую команду для развертывания ROOT.war
для Modulus.
modulus deploy
Это приведет к развертыванию war файла, и вы можете отслеживать логи проектов, чтобы просмотреть состояние своего развертывания, выполнив следующую команду:
modulus project logs tail
Вот и все с развертыванием!
5. Заключение
Основная цель этого учебника - показать вам, как создать чат-приложение в режиме реального времени с Spring Boot, WebSockets и MongoDB.
Для запуска проекта в продакшене Modulus используется как поставщик PaaS. Modulus содержит очень простые шаги для развертывания, а также имеет внутреннюю базу данных (MongoDB) для наших проектов. Кроме того, вы можете использовать очень полезные инструменты на панели инструментов Modulus, такие как журналы, уведомления, автомасштабирование, администрирование Db и т.д.