├── .gitignore ├── README.md ├── build.gradle └── src └── main ├── java └── messaging │ ├── Application.java │ ├── config │ ├── CamelConfig.java │ ├── SecurityConfig.java │ └── WebSocketConfig.java │ ├── controllers │ ├── APIController.java │ └── WebSocketController.java │ ├── dto │ └── MessageDTO.java │ └── handlers │ └── QueueHandler.java └── resources ├── application.properties └── static └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on: https://github.com/spring-guides/gs-messaging-stomp-websocket/blob/master/.gitignore 2 | 3 | # Operating System Files 4 | 5 | *.DS_Store 6 | Thumbs.db 7 | 8 | # Build Files # 9 | 10 | bin 11 | target 12 | build/ 13 | .gradle 14 | 15 | # Eclipse Project Files # 16 | 17 | .classpath 18 | .project 19 | .settings 20 | 21 | # IntelliJ IDEA Files # 22 | 23 | *.iml 24 | *.ipr 25 | *.iws 26 | *.idea 27 | 28 | # Spring Bootstrap artifacts 29 | 30 | dependency-reduced-pom.xml 31 | out 32 | README.html 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Real-Time Private Messaging Demo Using STOMP Over WebSockets on Spring Boot 2 | 3 | This project demonstrates the use of [STOMP](https://stomp.github.io/) over WebSockets on Spring Boot in conjunction with [Apache ActiveMQ](http://activemq.apache.org) and [Apache Camel](http://camel.apache.org) to send data to clients in real-time. For a more detailed explanation, visit the blog page at [http://yalingunayer.com/blog/realtime-data-delivery-on-spring-boot-using-activemq-and-stomp-over-websockets-part-2](http://yalingunayer.com/blog/realtime-data-delivery-on-spring-boot-using-activemq-and-stomp-over-websockets-part-2) 4 | 5 | ## Prerequisites 6 | 7 | - [Gradle](https://gradle.org/) 8 | - [Apache ActiveMQ](http://activemq.apache.org) 9 | 10 | ## Running 11 | 12 | Install and run your ActiveMQ instance. If the address your ActiveMQ is running on is not ```tcp://localhost:61616``` change the configuration at ```src/main/java/messaging/config/CamelConfig.java``` 13 | 14 | Once ActiveMQ starts running, run the application with the command ```gradle bootRun``` 15 | 16 | Visit ```http://localhost:8080``` to access the UI. 17 | 18 | Login with one of the following users: 19 | 20 | | Username | Password | 21 | | ------ | ------ | 22 | | user1 | pass1 | 23 | | user2 | pass2 | 24 | | user3 | pass3 | 25 | 26 | Make sure to login with different users on different browser sessions (multiple browsers or browser tabs) to demonstrate both user-specific and broadcast message delivery in real-time. 27 | 28 | ## License 29 | 30 | [Public Domain](http://choosealicense.com/licenses/unlicense/), or in other words, do whatever you want with it, but I provide no warranties of any kind so I can't be held responsible for any damages it may cause. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.2.RELEASE") 7 | } 8 | } 9 | 10 | apply plugin: 'java' 11 | apply plugin: 'eclipse' 12 | apply plugin: 'idea' 13 | apply plugin: 'spring-boot' 14 | 15 | jar { 16 | baseName = 'realtime-messaging' 17 | version = '0.1.0' 18 | } 19 | 20 | repositories { 21 | mavenLocal() 22 | mavenCentral() 23 | maven { url "https://repo.spring.io/libs-release" } 24 | } 25 | 26 | dependencies { 27 | compile("org.springframework.boot:spring-boot-starter-web") 28 | compile("org.springframework.boot:spring-boot-starter-websocket") 29 | compile("org.springframework:spring-messaging") 30 | compile("org.springframework:spring-jms:4.1.5.RELEASE") 31 | compile("org.springframework.security:spring-security-web:3.2.6.RELEASE") 32 | compile("org.springframework.security:spring-security-config:3.2.6.RELEASE") 33 | compile("org.apache.camel:camel-spring-boot:2.15.0") 34 | compile("org.apache.activemq:activemq-camel:5.10.0") 35 | testCompile("junit:junit") 36 | } 37 | 38 | task wrapper(type: Wrapper) { 39 | gradleVersion = '2.3' 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/messaging/Application.java: -------------------------------------------------------------------------------- 1 | package messaging; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.ConfigurableApplicationContext; 6 | 7 | @SpringBootApplication 8 | public class Application { 9 | 10 | public static void main(String[] args) { 11 | ConfigurableApplicationContext context = SpringApplication.run(Application.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/messaging/config/CamelConfig.java: -------------------------------------------------------------------------------- 1 | package messaging.config; 2 | 3 | import javax.jms.ConnectionFactory; 4 | 5 | import org.apache.activemq.ActiveMQConnectionFactory; 6 | import org.apache.activemq.pool.PooledConnectionFactory; 7 | import org.apache.camel.builder.RouteBuilder; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class CamelConfig { 13 | 14 | @Bean 15 | ConnectionFactory jmsConnectionFactory() { 16 | // use a pool for ActiveMQ connections 17 | PooledConnectionFactory pool = new PooledConnectionFactory(); 18 | pool.setConnectionFactory(new ActiveMQConnectionFactory("tcp://localhost:61616")); 19 | return pool; 20 | } 21 | 22 | @Bean 23 | RouteBuilder myRouter() { 24 | return new RouteBuilder() { 25 | 26 | @Override 27 | public void configure() throws Exception { 28 | // listen the queue named rt_messages and upon receiving a new entry 29 | // simply redirect it to a bean named queueHandler which will then send it to users over STOMP 30 | from("activemq:rt_messages").to("bean:queueHandler"); 31 | } 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/messaging/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package messaging.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.context.embedded.ServletListenerRegistrationBean; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 | import org.springframework.security.core.session.SessionRegistry; 12 | import org.springframework.security.core.session.SessionRegistryImpl; 13 | import org.springframework.security.web.session.HttpSessionEventPublisher; 14 | 15 | 16 | @Configuration 17 | @EnableWebSecurity 18 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 19 | 20 | @Autowired 21 | public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { 22 | // configure a simple in-memory authentication with three users 23 | auth.inMemoryAuthentication() 24 | .withUser("user1").password("pass1").roles("USER") 25 | .and() 26 | .withUser("user2").password("pass2").roles("USER") 27 | .and() 28 | .withUser("user3").password("pass3").roles("USER"); 29 | } 30 | 31 | @Bean 32 | public SessionRegistry sessionRegistry() { 33 | return new SessionRegistryImpl(); 34 | } 35 | 36 | @Bean 37 | public ServletListenerRegistrationBean httpSessionEventPublisher() { 38 | return new ServletListenerRegistrationBean(new HttpSessionEventPublisher()); 39 | } 40 | 41 | @Override 42 | protected void configure(HttpSecurity http) throws Exception { 43 | super.configure(http); 44 | // disable CSRF for simplicity and configure a session registry which will allow us to fetch a list of users 45 | http.csrf().disable().sessionManagement().maximumSessions(-1).sessionRegistry(sessionRegistry()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/messaging/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package messaging.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; 6 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 7 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void configureMessageBroker(MessageBrokerRegistry config) { 15 | // use the /topic prefix for outgoing WebSocket communication 16 | config.enableSimpleBroker("/topic"); 17 | 18 | // use the /app prefix for others 19 | config.setApplicationDestinationPrefixes("/app"); 20 | } 21 | 22 | @Override 23 | public void registerStompEndpoints(StompEndpointRegistry registry) { 24 | // use the /messaging endpoint (prefixed with /app as configured above) for incoming requests 25 | registry.addEndpoint("/messaging").withSockJS(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/messaging/controllers/APIController.java: -------------------------------------------------------------------------------- 1 | package messaging.controllers; 2 | 3 | 4 | import java.security.Principal; 5 | import java.util.Set; 6 | import java.util.stream.Collectors; 7 | 8 | import messaging.dto.MessageDTO; 9 | 10 | import org.apache.camel.CamelContext; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Qualifier; 13 | import org.springframework.security.core.session.SessionRegistry; 14 | import org.springframework.security.core.userdetails.User; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RequestMethod; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | @RestController 21 | public class APIController { 22 | 23 | @Autowired 24 | private CamelContext camelContext; 25 | 26 | @Autowired 27 | @Qualifier("sessionRegistry") 28 | private SessionRegistry sessionRegistry; 29 | 30 | /** 31 | * Receives the messages from clients and sends them to ActiveMQ. 32 | * 33 | * @param message the message to send, encapsulated in a wrapper 34 | */ 35 | @RequestMapping(value = "/send", method = RequestMethod.POST, consumes = "application/json") 36 | public void sendMessage(@RequestBody MessageDTO message, Principal currentUser) { 37 | // send any message sent by clients to a queue called rt_messages 38 | message.from = currentUser.getName(); 39 | camelContext.createProducerTemplate().sendBody("activemq:rt_messages", message); 40 | } 41 | 42 | /** 43 | * Returns the names of the currently logged-in users. 44 | * 45 | * @return set of user names 46 | */ 47 | @RequestMapping(value = "/users", method = RequestMethod.GET, produces = "application/json") 48 | public Set getUsers() { 49 | // get the list of users from Spring Security's session registry 50 | return sessionRegistry.getAllPrincipals().stream().map(u -> ((User) u).getUsername()).collect(Collectors.toSet()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/messaging/controllers/WebSocketController.java: -------------------------------------------------------------------------------- 1 | package messaging.controllers; 2 | 3 | 4 | import java.security.Principal; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.messaging.Message; 8 | import org.springframework.messaging.handler.annotation.MessageMapping; 9 | import org.springframework.messaging.simp.SimpMessageHeaderAccessor; 10 | import org.springframework.messaging.simp.SimpMessagingTemplate; 11 | import org.springframework.stereotype.Controller; 12 | 13 | @Controller 14 | public class WebSocketController { 15 | 16 | @Autowired 17 | private SimpMessagingTemplate messageTemplate; 18 | 19 | /** 20 | * Listens the /app/messaging endpoint and when a message is received, gets the user information encapsulated within it, and informs all clients 21 | * listening at the /topic/users endpoint that the user has joined the topic. 22 | * 23 | * @param message the encapsulated STOMP message 24 | */ 25 | @MessageMapping("/messaging") 26 | public void messaging(Message message) { 27 | // get the user associated with the message 28 | Principal user = message.getHeaders().get(SimpMessageHeaderAccessor.USER_HEADER, Principal.class); 29 | // notify all users that a user has joined the topic 30 | messageTemplate.convertAndSend("/topic/users", user.getName()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/messaging/dto/MessageDTO.java: -------------------------------------------------------------------------------- 1 | package messaging.dto; 2 | 3 | import java.io.Serializable; 4 | import java.util.Calendar; 5 | import java.util.Date; 6 | 7 | /** 8 | * A simple DTO class to encapsulate messages along with their destinations and timestamps. 9 | */ 10 | public class MessageDTO implements Serializable { 11 | 12 | private static final long serialVersionUID = 1L; 13 | 14 | public Date date; 15 | public String content; 16 | public String to; 17 | public String from; 18 | 19 | public MessageDTO() { 20 | this.date = Calendar.getInstance().getTime(); 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/messaging/handlers/QueueHandler.java: -------------------------------------------------------------------------------- 1 | package messaging.handlers; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import messaging.dto.MessageDTO; 7 | 8 | import org.apache.camel.Exchange; 9 | import org.apache.camel.Message; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.messaging.MessageHeaders; 12 | import org.springframework.messaging.simp.SimpMessageSendingOperations; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.util.MimeTypeUtils; 15 | 16 | /** 17 | * Receives messages from ActiveMQ and relays them to appropriate users. 18 | */ 19 | @Component(value = "queueHandler") 20 | public class QueueHandler { 21 | 22 | @Autowired 23 | private SimpMessageSendingOperations msgTemplate; 24 | 25 | private static Map defaultHeaders; 26 | 27 | static { 28 | defaultHeaders = new HashMap(); 29 | // add the Content-Type: application/json header by default 30 | defaultHeaders.put(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON); 31 | } 32 | 33 | public void handle(Exchange exchange) { 34 | Message camelMessage = exchange.getIn(); 35 | MessageDTO message = camelMessage.getBody(MessageDTO.class); 36 | // send the message specifically to the destination user by using STOMP's user-directed messaging 37 | msgTemplate.convertAndSendToUser(message.to, "/topic/messages", message, defaultHeaders); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Configuration goes here 2 | -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Realtime Messaging 5 | 10 | 11 | 12 |
13 |

Send a Message

14 |

To:

15 |

Message:

16 | 17 | 18 |
19 |
20 |

Messages Received

21 |
    22 |
    23 | 24 | 25 | 26 | 27 | 28 | 154 | 155 | --------------------------------------------------------------------------------