В этом уроке мы будем использовать Spring Boot для среды веб-разработки, Websockets для связи в реальном времени, Tomcat для контейнера приложений Java, Gradle для построения и управления зависимостями, Thymeleaf для рендеринга шаблонов, MongoDB для хранения данных, и, наконец, не будет XML для конфигураций бинов. Просто чтобы вдохновить вас, в конце этой статьи вы увидите полностью работающее приложение, подобное показанному ниже.
1. Сценарий
- Доу открывает страницу чата, чтобы общаться со своими друзьями.
- Ему предлагается выбрать никнейм.
- Он заходит на страницу чата и отправляет сообщение. Сообщение отправляется конечной точке Spring MVC для сохранения в базе данных и трансляции.
- Указанная конечная точка обрабатывает сообщение и передает его всем клиентам, подключенным к системе чата.
2. Построение зависимостей и настройка Gradle
Прежде чем перейти к внутренней структуре проекта, позвольте мне объяснить, какие библиотеки мы будем использовать для перечисленных выше функций проекта, и управлять ими с помощью Gradle. Когда вы клонируете проект из GitHub, вы увидите файл build.gradle
в корневом каталоге проекта, как build.gradle
ниже.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
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 нужен файл войны, чтобы автоматически развернуть проект в своем облаке.
Чтобы создать файл войны, мы использовали apply plugin: 'war'
. Модуль также ожидает, что именем войны будет ROOT.war
по умолчанию, и поэтому мы использовали:
1
2
3
|
war {
baseName: ‘ROOT.war’
}
|
Когда вы запускаете задачу build
Gradle, она генерирует файл war для развертывания в контейнере Tomcat. И, наконец, как вы можете догадаться, раздел зависимостей предназначен для сторонних библиотек для конкретных действий.
Это все для раздела зависимостей проекта, и вы можете обратиться к руководству пользователя Gradle для получения дополнительной информации о Gradle.
3. Разработка программного обеспечения
Если вы хотите разработать хорошее приложение, рекомендуется определить структуру вашего проекта небольшими кусочками. Вы можете увидеть кусочки всей архитектуры нашего приложения.
3.1. модель
Мы разрабатываем приложение чата, поэтому мы можем сказать, что у нас есть модель ChatMessageModel
(т.е. объект домена). Пока мы сохраняем или просматриваем детали сообщения чата, мы можем привести объект чата к этой модели ChatMessageModel
или к ChatMessageModel
. Также мы можем использовать модель User
для пользователей чата, но для упрощения приложения мы будем использовать просто nickname
качестве текста. Модель ChatMessageModel
имеет следующие поля: text
, author
и createDate
. Классовое представление этой модели выглядит следующим образом:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
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. контроллер
Контроллер — это поведение вашего приложения. Это означает, что вам нужно, чтобы ваш контроллер был простым и мог легко взаимодействовать с моделями доменов и другими сервисами. Мы ожидаем, что наши контроллеры будут обрабатывать:
- Запрос на сохранение сообщения чата
- Список последних сообщений чата
- Обслуживание страницы приложения чата
- Обслуживание страницы входа
- Трансляция сообщений чата клиентам
Здесь вы можете увидеть общие конечные точки:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
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")
. Это отправит содержимое, возвращенное функцией, клиентам. И возвращенный контент, как показано ниже:
1
2
3
|
…
return new ChatMessage(chatMessageModelList.toString());
…
|
Это последнее сообщение из базы данных:
Приведенное выше сообщение будет преобразовано в формат для связи через WebSocket. Это канальное сообщение будет обрабатываться на стороне клиента сторонней библиотекой JavaScript и обрабатываться в следующих разделах.
Для операций с сообщениями db используется spring-boot-starter-data-mongodb
. Эта библиотека помогает нам в операциях с репозиторием, и создать объект репозитория для MongoDB очень просто. Вы можете увидеть пример ChatMessageRepository
ниже:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
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
, это имя функции будет автоматически проанализировано в запросе на стороне MongoRepository
Spring. Это будет что-то вроде:
1
|
SELECT * FROM ChatMessageModel WHERE 1 ORDER BY createDate ASC
|
В ChatMessageController
мы использовали эту функцию, а также функции по умолчанию в MongoRepository
:
1
|
chatMessageRepository.findAll(new PageRequest(0, 5, Sort.Direction.DESC, «createDate»)).getContent()
|
findAll
используется параметр для сортировки и разбиения на страницы. Вы можете ознакомиться с руководством на веб-сайте Spring для получения более подробной информации о Spring JPA.
3.3. Посмотреть
В части просмотра у нас есть только две страницы. Одна из них — это страница входа в систему, чтобы получить ник пользователя, а вторая — главная страница чата для отправки сообщений пользователям чата.
Как вы можете видеть в разделе контроллера выше, они отображаются с использованием двух конечных точек, /login
и /chat
. Для создания интерактивных страниц мы будем использовать сторонние библиотеки JavaScript. Мы будем использовать их со страниц CDN. Вы можете увидеть страницу входа ниже:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
<!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 , откроется страница чата. Если вы уже вошли в систему и перейдете на страницу входа, вы будете перенаправлены на страницу чата.
Вот страница чата:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
<!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 + ‘ |
});
}
$(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»>
</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»>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
|
Эта страница предназначена для простого просмотра и отправки сообщений. Сообщения доставляются на эту страницу через WebSockets. На этой странице вы можете увидеть sockjs
и stompjs
. Это для обработки уведомлений. Всякий раз, когда приходит новое сообщение, область последних сообщений заполняется заново.
Кстати, когда вы впервые открываете страницу чата, самые последние сообщения будут извлекаться в области сообщений. Как вы можете видеть на стороне JavaScript, наш канал сообщений — newMessage
. Итак, мы прослушиваем этот канал, и когда вы нажмете кнопку « Отправить» , сообщение в текстовом поле будет отправлено конечной точке, и это сообщение будет передано подключенным клиентам после успешного хранения.
Как видите, архитектура программного обеспечения здесь очень проста и легка в разработке. У нас есть готовый к использованию код, и давайте развернем его в модуле.
Modulus — это один из лучших PaaS для развертывания, масштабирования и мониторинга вашего приложения на выбранном вами языке.
4. Развертывание
4.1. Предпосылки
Перед развертыванием приложения создадим базу данных с помощью админ-панели Modulus. Вам нужна учетная запись Modulus для создания базы данных и развертывания приложений, поэтому, пожалуйста, создайте учетную запись, если у вас ее нет.
Перейдите на панель инструментов Modulus и создайте базу данных:
На экране создания базы данных укажите имя базы данных, выберите версию MongoDB (я использовал 2.6.3, так что будет лучше, если вы выберете и 2.6.3), и, наконец, определите пользователя, который будет применять операции чтения / записи базы данных.
Вы можете получить URL MongoDB после успешного создания базы данных. Мы будем использовать URL-адрес MongoDB в переменных среды, которые будут использоваться приложением Spring Boot.
Чтобы установить переменные окружения для MongoDB, вам необходимо иметь приложение. Перейдите в Dashboard и нажмите Projects . На этой странице нажмите Создать новый проект.
Чтобы продолжить настройку переменных среды, перейдите на панель инструментов и нажмите « Проекты». Выберите свой проект и нажмите Администрирование. Прокрутите страницу вниз и установите переменные среды с помощью ключа SPRING_DATA_MONGODB_URI
и значения URI вашей базы данных:
При развертывании приложения Spring будет использовать это значение переменной среды. Мы сделали с требованиями, и давайте продолжим с частью развертывания.
4.2. Развертывание с помощью CLI
Чтобы развернуть проект, запустите задачу сборки gradle:
1
|
gradle build
|
Эта задача создаст файл войны с именем ROOT.war
. Скопируйте этот файл в новую папку и установите модуль CLI, если у вас его нет.
1
|
npm install -g modulus
|
Войдите в систему;
1
|
modulus login
|
Теперь выполните следующую команду, чтобы развернуть ROOT.war
в модуле.
1
|
modulus deploy
|
Это развернет файл war, и вы сможете настроить журналы проекта, чтобы увидеть состояние вашего развертывания, выполнив следующую команду:
1
|
modulus project logs tail
|
Это все с развертыванием!
5. Заключение
Основная цель этого руководства — показать, как создать приложение для чата в реальном времени с использованием Spring Boot, WebSockets и MongoDB.
Для запуска проекта в производстве в качестве поставщика PaaS используется модуль Modulus. Модуль имеет очень простые шаги для развертывания, а также имеет внутреннюю базу данных (MongoDB) для наших проектов. Кроме того, вы можете использовать очень полезные инструменты на панели инструментов Modulus, такие как Журналы, Уведомления, Автоматическое масштабирование, Администрирование БД и другие.