From be767070f3f484a19ca115578cb00f0b4fc30cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Miller=20=28=E9=94=BA=E4=BF=8A=29?= Date: Fri, 16 Jul 2021 00:25:26 +0800 Subject: [PATCH] feat($STOMP): support STOMP over WebSocket --- auth-center/pom.xml | 7 +- .../websocket/WebSocketMessagePayload.java | 15 + .../websocket/WebSocketMessageResponse.java | 18 ++ spring-boot-admin/pom.xml | 10 +- spring-cloud-starter/pom.xml | 10 +- .../MafAutoConfiguration.java | 4 +- .../configuration/WebMvcConfiguration.java | 4 +- .../websocket/GreetingController.java | 40 +++ .../websocket/WebSocketConfiguration.java | 73 +++++ .../src/main/resources/static/home.html | 12 +- .../main/resources/static/script/websocket.js | 119 +++++++ .../static/{styles => style}/404.css | 0 .../static/{styles => style}/home.css | 0 .../static/{styles => style}/video.css | 0 .../main/resources/static/style/websocket.css | 298 ++++++++++++++++++ .../src/main/resources/static/video.html | 6 +- .../src/main/resources/static/websocket.html | 54 ++++ 17 files changed, 656 insertions(+), 14 deletions(-) create mode 100644 common/src/main/java/com/jmsoftware/maf/common/websocket/WebSocketMessagePayload.java create mode 100644 common/src/main/java/com/jmsoftware/maf/common/websocket/WebSocketMessageResponse.java create mode 100644 spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/websocket/GreetingController.java create mode 100644 spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/websocket/WebSocketConfiguration.java create mode 100644 universal-ui/src/main/resources/static/script/websocket.js rename universal-ui/src/main/resources/static/{styles => style}/404.css (100%) rename universal-ui/src/main/resources/static/{styles => style}/home.css (100%) rename universal-ui/src/main/resources/static/{styles => style}/video.css (100%) create mode 100644 universal-ui/src/main/resources/static/style/websocket.css create mode 100644 universal-ui/src/main/resources/static/websocket.html diff --git a/auth-center/pom.xml b/auth-center/pom.xml index 9da0c579..da980e1c 100644 --- a/auth-center/pom.xml +++ b/auth-center/pom.xml @@ -1,5 +1,5 @@ - 4.0.0 @@ -118,6 +118,11 @@ runtime + + com.jmsoftware.maf + universal-ui + + org.springframework.boot diff --git a/common/src/main/java/com/jmsoftware/maf/common/websocket/WebSocketMessagePayload.java b/common/src/main/java/com/jmsoftware/maf/common/websocket/WebSocketMessagePayload.java new file mode 100644 index 00000000..63c5f67f --- /dev/null +++ b/common/src/main/java/com/jmsoftware/maf/common/websocket/WebSocketMessagePayload.java @@ -0,0 +1,15 @@ +package com.jmsoftware.maf.common.websocket; + +import lombok.Data; + +/** + * Description: WebSocketMessagePayload, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 7/15/2021 5:35 PM + **/ +@Data +public class WebSocketMessagePayload { + private String sender; + private String receiver; + private Object content; +} diff --git a/common/src/main/java/com/jmsoftware/maf/common/websocket/WebSocketMessageResponse.java b/common/src/main/java/com/jmsoftware/maf/common/websocket/WebSocketMessageResponse.java new file mode 100644 index 00000000..ea6d3845 --- /dev/null +++ b/common/src/main/java/com/jmsoftware/maf/common/websocket/WebSocketMessageResponse.java @@ -0,0 +1,18 @@ +package com.jmsoftware.maf.common.websocket; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * Description: WebSocketMessagePayload, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 7/15/2021 5:35 PM + **/ +@Data +public class WebSocketMessageResponse { + private final LocalDateTime timestamp = LocalDateTime.now(); + private String sender; + private String receiver; + private Object content; +} diff --git a/spring-boot-admin/pom.xml b/spring-boot-admin/pom.xml index ace0190d..4c02cabc 100644 --- a/spring-boot-admin/pom.xml +++ b/spring-boot-admin/pom.xml @@ -1,5 +1,5 @@ - 4.0.0 @@ -98,6 +98,14 @@ org.springframework.boot spring-boot-starter-quartz + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.boot + spring-boot-starter-reactor-netty + org.springframework.integration spring-integration-sftp diff --git a/spring-cloud-starter/pom.xml b/spring-cloud-starter/pom.xml index 050e31f0..bd21d259 100644 --- a/spring-cloud-starter/pom.xml +++ b/spring-cloud-starter/pom.xml @@ -1,5 +1,5 @@ - 4.0.0 @@ -54,6 +54,14 @@ org.springframework.boot spring-boot-starter-quartz + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.boot + spring-boot-starter-reactor-netty + org.springframework.boot spring-boot-configuration-processor diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/MafAutoConfiguration.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/MafAutoConfiguration.java index c9966e82..75860611 100644 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/MafAutoConfiguration.java +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/MafAutoConfiguration.java @@ -19,6 +19,7 @@ import com.jmsoftware.maf.springcloudstarter.service.CommonService; import com.jmsoftware.maf.springcloudstarter.service.impl.CommonServiceImpl; import com.jmsoftware.maf.springcloudstarter.sftp.SftpConfiguration; +import com.jmsoftware.maf.springcloudstarter.websocket.WebSocketConfiguration; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.exceptions.PersistenceException; import org.mybatis.spring.MyBatisSystemException; @@ -66,7 +67,8 @@ MinioConfiguration.class, JacksonConfiguration.class, TypeConversionConfiguration.class, - QuartzConfiguration.class + QuartzConfiguration.class, + WebSocketConfiguration.class }) public class MafAutoConfiguration { @PostConstruct diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/configuration/WebMvcConfiguration.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/configuration/WebMvcConfiguration.java index 604d22a5..62e18d0e 100644 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/configuration/WebMvcConfiguration.java +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/configuration/WebMvcConfiguration.java @@ -29,11 +29,13 @@ public class WebMvcConfiguration implements WebMvcConfigurer { * * @param registry CORS registry */ + @SuppressWarnings("BroadCORSAllowOrigin") @Override public void addCorsMappings(CorsRegistry registry) { log.info("Configuring CORS allowedOrigins: {}, allowedMethods: {}, allowedHeaders: {}", ALL, ALL, ALL); registry.addMapping("/**") - .allowedOrigins(ALL) + .allowCredentials(true) + .allowedOriginPatterns(ALL) .allowedMethods(ALL) .allowedHeaders(ALL) .maxAge(MAX_AGE_SECS); diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/websocket/GreetingController.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/websocket/GreetingController.java new file mode 100644 index 00000000..8b1b3e13 --- /dev/null +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/websocket/GreetingController.java @@ -0,0 +1,40 @@ +package com.jmsoftware.maf.springcloudstarter.websocket; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jmsoftware.maf.common.websocket.WebSocketMessagePayload; +import com.jmsoftware.maf.common.websocket.WebSocketMessageResponse; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Controller; + +import java.util.Map; + +/** + * Description: GreetingController, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 7/15/2021 5:24 PM + **/ +@Slf4j +@Controller +@RequiredArgsConstructor +public class GreetingController { + private final SimpMessagingTemplate simpMessagingTemplate; + private final ObjectMapper objectMapper; + + @SneakyThrows + @MessageMapping("/send-message") + public void sendMessage(@Payload WebSocketMessagePayload payload, @Headers Map headers) { + val response = new WebSocketMessageResponse(); + response.setSender(payload.getSender()); + response.setReceiver(payload.getReceiver()); + response.setContent(headers.get("simpSessionId").toString() + "---" + payload.getContent()); + this.simpMessagingTemplate.convertAndSend("/topic/public/broadcast", + this.objectMapper.writeValueAsString(response)); + } +} diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/websocket/WebSocketConfiguration.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/websocket/WebSocketConfiguration.java new file mode 100644 index 00000000..82a03e84 --- /dev/null +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/websocket/WebSocketConfiguration.java @@ -0,0 +1,73 @@ +package com.jmsoftware.maf.springcloudstarter.websocket; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +import static org.springframework.web.cors.CorsConfiguration.ALL; + +/** + * Description: WebSocketConfiguration, change description here. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com, date: 7/15/2021 12:15 AM + * @see + * Using Spring Boot for WebSocket Implementation with STOMP + **/ +@Slf4j +@EnableWebSocketMessageBroker +@ConditionalOnClass({ServerEndpointExporter.class}) +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { + @Bean + public ServerEndpointExporter serverEndpointExporter() { + log.warn("Initial bean: '{}'", ServerEndpointExporter.class.getSimpleName()); + return new ServerEndpointExporter(); + } + + @Bean + public GreetingController greetingController(SimpMessagingTemplate simpMessagingTemplate, + ObjectMapper objectMapper) { + log.warn("Initial bean: '{}'", GreetingController.class.getSimpleName()); + return new GreetingController(simpMessagingTemplate, objectMapper); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/web-socket", "/ws") + .setAllowedOriginPatterns(ALL) + .withSockJS(); + } + + /** + * {@inheritDoc} + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // In-memory message broker with one or more destinations for sending and receiving messages + + // There are two destination prefixes defined: topic and queue. They follow the convention that destinations + // for messages to be carried on to all subscribed clients via the pub-sub model should be prefixed with + // "topic". + // On the other hand, destinations for private messages are typically prefixed by "queue". + registry.enableSimpleBroker("/topic/", "/queue/"); + // Defines the prefix app that is used to filter destinations handled by methods annotated with + // @MessageMapping which you will implement in a controller. The controller, after processing the message, + // will send it to the broker. + registry.setApplicationDestinationPrefixes("/app"); + + // Use this for enabling a Full featured broker like RabbitMQ + /* + registry.enableStompBrokerRelay("/topic") + .setRelayHost("localhost") + .setRelayPort(61613) + .setClientLogin("guest") + .setClientPasscode("guest"); + */ + } +} diff --git a/universal-ui/src/main/resources/static/home.html b/universal-ui/src/main/resources/static/home.html index e7599316..3ebba95f 100644 --- a/universal-ui/src/main/resources/static/home.html +++ b/universal-ui/src/main/resources/static/home.html @@ -4,8 +4,8 @@ UNSET - - + +
@@ -14,11 +14,11 @@ Welcome to {{ projectArtifactId }}@{{ version }}! - - + + {{ applicationName }} - +
Create New Person diff --git a/universal-ui/src/main/resources/static/script/websocket.js b/universal-ui/src/main/resources/static/script/websocket.js new file mode 100644 index 00000000..9e262941 --- /dev/null +++ b/universal-ui/src/main/resources/static/script/websocket.js @@ -0,0 +1,119 @@ +'use strict'; + +var usernamePage = document.querySelector('#username-page'); +var chatPage = document.querySelector('#chat-page'); +var usernameForm = document.querySelector('#usernameForm'); +var messageForm = document.querySelector('#messageForm'); +var messageInput = document.querySelector('#message'); +var messageArea = document.querySelector('#messageArea'); +var connectingElement = document.querySelector('.connecting'); + +var stompClient = null; +var username = null; + +var colors = [ + '#2196F3', '#32c787', '#00BCD4', '#ff5652', + '#ffc107', '#ff85af', '#FF9800', '#39bbb0' +]; + +function connect(event) { + username = document.querySelector('#name').value.trim(); + + if (username) { + usernamePage.classList.add('hidden'); + chatPage.classList.remove('hidden'); + + var socket = new SockJS('/web-socket'); + stompClient = Stomp.over(socket); + + stompClient.connect({}, onConnected, onError); + } + event.preventDefault(); +} + + +function onConnected() { + // Subscribe to the Public Topic + stompClient.subscribe('/topic/public/broadcast', onMessageReceived); + + // Tell your username to the server + // stompClient.send(`/app/join/${username}`, {},) + + connectingElement.classList.add('hidden'); +} + + +function onError(error) { + connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!'; + connectingElement.style.color = 'red'; +} + + +function sendMessage(event) { + var messageContent = messageInput.value.trim(); + + if (messageContent && stompClient) { + var chatMessage = { + sender: username, + receiver: null, + content: messageInput.value + }; + + stompClient.send("/app/send-message", {}, JSON.stringify(chatMessage)); + messageInput.value = ''; + } + event.preventDefault(); +} + + +function onMessageReceived(payload) { + var body = JSON.parse(payload.body); + console.info('body', body); + + var messageElement = document.createElement('li'); + + if (body.type === 'JOIN') { + messageElement.classList.add('event-message'); + body.content = body.sender + ' joined!'; + } else if (body.type === 'LEAVE') { + messageElement.classList.add('event-message'); + body.content = body.sender + ' left!'; + } else { + messageElement.classList.add('chat-message'); + + var avatarElement = document.createElement('i'); + var avatarText = document.createTextNode(body.sender[0]); + avatarElement.appendChild(avatarText); + avatarElement.style['background-color'] = getAvatarColor(body.sender); + + messageElement.appendChild(avatarElement); + + var usernameElement = document.createElement('span'); + var usernameText = document.createTextNode(body.sender); + usernameElement.appendChild(usernameText); + messageElement.appendChild(usernameElement); + } + + var textElement = document.createElement('p'); + var messageText = document.createTextNode(body.content); + textElement.appendChild(messageText); + + messageElement.appendChild(textElement); + + messageArea.appendChild(messageElement); + messageArea.scrollTop = messageArea.scrollHeight; +} + + +function getAvatarColor(messageSender) { + var hash = 0; + for (var i = 0; i < messageSender.length; i++) { + hash = 31 * hash + messageSender.charCodeAt(i); + } + + var index = Math.abs(hash % colors.length); + return colors[index]; +} + +usernameForm.addEventListener('submit', connect, true) +messageForm.addEventListener('submit', sendMessage, true) diff --git a/universal-ui/src/main/resources/static/styles/404.css b/universal-ui/src/main/resources/static/style/404.css similarity index 100% rename from universal-ui/src/main/resources/static/styles/404.css rename to universal-ui/src/main/resources/static/style/404.css diff --git a/universal-ui/src/main/resources/static/styles/home.css b/universal-ui/src/main/resources/static/style/home.css similarity index 100% rename from universal-ui/src/main/resources/static/styles/home.css rename to universal-ui/src/main/resources/static/style/home.css diff --git a/universal-ui/src/main/resources/static/styles/video.css b/universal-ui/src/main/resources/static/style/video.css similarity index 100% rename from universal-ui/src/main/resources/static/styles/video.css rename to universal-ui/src/main/resources/static/style/video.css diff --git a/universal-ui/src/main/resources/static/style/websocket.css b/universal-ui/src/main/resources/static/style/websocket.css new file mode 100644 index 00000000..384b3cd8 --- /dev/null +++ b/universal-ui/src/main/resources/static/style/websocket.css @@ -0,0 +1,298 @@ +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +html,body { + height: 100%; + overflow: hidden; +} + +body { + margin: 0; + padding: 0; + font-weight: 400; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 1rem; + line-height: 1.58; + color: #333; + background-color: #f4f4f4; + height: 100%; +} + +body:before { + height: 50%; + width: 100%; + position: absolute; + top: 0; + left: 0; + background: #128ff2; + content: ""; + z-index: 0; +} + +.clearfix:after { + display: block; + content: ""; + clear: both; +} + +.hidden { + display: none; +} + +.form-control { + width: 100%; + min-height: 38px; + font-size: 15px; + border: 1px solid #c8c8c8; +} + +.form-group { + margin-bottom: 15px; +} + +input { + padding-left: 10px; + outline: none; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 20px; + margin-bottom: 20px; +} + +h1 { + font-size: 1.7em; +} + +a { + color: #128ff2; +} + +button { + box-shadow: none; + border: 1px solid transparent; + font-size: 14px; + outline: none; + line-height: 100%; + white-space: nowrap; + vertical-align: middle; + padding: 0.6rem 1rem; + border-radius: 2px; + transition: all 0.2s ease-in-out; + cursor: pointer; + min-height: 38px; +} + +button.default { + background-color: #e8e8e8; + color: #333; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12); +} + +button.primary { + background-color: #128ff2; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12); + color: #fff; +} + +button.accent { + background-color: #ff4743; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12); + color: #fff; +} + +#username-page { + text-align: center; +} + +.username-page-container { + background: #fff; + box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); + border-radius: 2px; + width: 100%; + max-width: 500px; + display: inline-block; + margin-top: 42px; + vertical-align: middle; + position: relative; + padding: 35px 55px 35px; + min-height: 250px; + position: absolute; + top: 50%; + left: 0; + right: 0; + margin: 0 auto; + margin-top: -160px; +} + +.username-page-container .username-submit { + margin-top: 10px; +} + + +#chat-page { + position: relative; + height: 100%; +} + +.chat-container { + max-width: 700px; + margin-left: auto; + margin-right: auto; + background-color: #fff; + box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); + margin-top: 30px; + height: calc(100% - 60px); + max-height: 600px; + position: relative; +} + +#chat-page ul { + list-style-type: none; + background-color: #FFF; + margin: 0; + overflow: auto; + overflow-y: scroll; + padding: 0 20px 0px 20px; + height: calc(100% - 150px); +} + +#chat-page #messageForm { + padding: 20px; +} + +#chat-page ul li { + line-height: 1.5rem; + padding: 10px 20px; + margin: 0; + border-bottom: 1px solid #f4f4f4; +} + +#chat-page ul li p { + margin: 0; +} + +#chat-page .event-message { + width: 100%; + text-align: center; + clear: both; +} + +#chat-page .event-message p { + color: #777; + font-size: 14px; + word-wrap: break-word; +} + +#chat-page .chat-message { + padding-left: 68px; + position: relative; +} + +#chat-page .chat-message i { + position: absolute; + width: 42px; + height: 42px; + overflow: hidden; + left: 10px; + display: inline-block; + vertical-align: middle; + font-size: 18px; + line-height: 42px; + color: #fff; + text-align: center; + border-radius: 50%; + font-style: normal; + text-transform: uppercase; +} + +#chat-page .chat-message span { + color: #333; + font-weight: 600; +} + +#chat-page .chat-message p { + color: #43464b; +} + +#messageForm .input-group input { + float: left; + width: calc(100% - 85px); +} + +#messageForm .input-group button { + float: left; + width: 80px; + height: 38px; + margin-left: 5px; +} + +.chat-header { + text-align: center; + padding: 15px; + border-bottom: 1px solid #ececec; +} + +.chat-header h2 { + margin: 0; + font-weight: 500; +} + +.connecting { + padding-top: 5px; + text-align: center; + color: #777; + position: absolute; + top: 65px; + width: 100%; +} + + +@media screen and (max-width: 730px) { + + .chat-container { + margin-left: 10px; + margin-right: 10px; + margin-top: 10px; + } +} + +@media screen and (max-width: 480px) { + .chat-container { + height: calc(100% - 30px); + } + + .username-page-container { + width: auto; + margin-left: 15px; + margin-right: 15px; + padding: 25px; + } + + #chat-page ul { + height: calc(100% - 120px); + } + + #messageForm .input-group button { + width: 65px; + } + + #messageForm .input-group input { + width: calc(100% - 70px); + } + + .chat-header { + padding: 10px; + } + + .connecting { + top: 60px; + } + + .chat-header h2 { + font-size: 1.1em; + } +} \ No newline at end of file diff --git a/universal-ui/src/main/resources/static/video.html b/universal-ui/src/main/resources/static/video.html index 9a186caa..8377d095 100644 --- a/universal-ui/src/main/resources/static/video.html +++ b/universal-ui/src/main/resources/static/video.html @@ -4,8 +4,8 @@ ExRx.net Crawler Server - - + +
@@ -14,7 +14,7 @@ Hello, World! Welcome to {{ appInfo.projectArtifactId }}@{{ appInfo.version }}! -
diff --git a/universal-ui/src/main/resources/static/websocket.html b/universal-ui/src/main/resources/static/websocket.html new file mode 100644 index 00000000..a9a476e0 --- /dev/null +++ b/universal-ui/src/main/resources/static/websocket.html @@ -0,0 +1,54 @@ + + + + + Spring Boot WebSocket Chat Application | CalliCoder + + + + + +
+
+

Type your username

+
+
+ +
+
+ +
+
+
+
+ + + + + + + +