Projekt
Kombinacja Spring Boot i AngularJS (angular w wersji 1) tak mi się spodobała, iż utworzyłem sobie własny archetyp mavena, aby gwałtownie móc tworzyć kolejne proste aplikacje. Jest on dostępny na moim GitHubie. Instrukcje tworzenia z niego projektu są w Readme, nie będę się powtarzał.Eventy
Taka aktualizacja danych musi być wywoływana przez jakieś zdarzenie. Na potrzeby przykładu wykorzystałem springowe eventy. Eventem w springu może być zwykłe POJO. W moim przypadku mam klasę MyEvent z atrybutami timestamp i message.public class MyEvent {
private final long timestamp = System.currentTimeMillis();
private final String message;
public MyEvent(String message) {
this.message = message;
}
public long getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
@Override
public String toString() {
return "MyEvent{" +
"timestamp=" + timestamp +
", message='" + message + '\'' +
'}';
}
}Warto by jeszcze te eventy jakoś produkować. Do tego wykorzystałem springowy sheduler. Pozwala on wykonywać pewną operację regularnie z określoną częstotliwością. W moim przypadku jest to publikowanie nowego eventu co 5 sekund. Publikować eventy pozwala bean klasy ApplicationEventPublisher.
@Component
@EnableScheduling
public class MyEventProducer {
private final ApplicationEventPublisher publisher;
@Autowired
public MyEventProducer(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Scheduled(fixedRate = 5000)
public void publishMyEvent() {
publisher.publishEvent(new MyEvent("hello"));
}
}
WebSockets - Spring
Uruchomienie obsługi websocketów w boocie wymaga dodania odpowiedniej zależności:<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
Następnie należy skonfigurować wbudowany w kontener broker komunikatów.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
}
}
Endpoint /chat jest endpointem, dzięki którego należy nawiązywać połączenie z brokerem. /topic określa prefix, którym należy poprzedzać endpoint komunikatów.
Potrzebne jeszcze jest coś co będzie nasłuchiwać na generowane eventy i przysyłać komunikaty przez websocket.
@Component
public class WebSocketSender {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketSender.class);
@Autowired
private SimpMessagingTemplate template;
@EventListener
public void send(MyEvent myEvent) {
LOGGER.info(myEvent.toString());
template.convertAndSend("/topic/myevents", myEvent);
}
}
Wysyłkę komunikatów realizuję przez template. Wiadomości wysyłane są jako org.springframework.messaging.Message więc można skonstruować ten obiekt samemu, a można wykorzystać metodę konwertującą.
UWAGA. Poszukując informacji w internecie często napotykałem na tutoriale wykorzystujące adnotację @SendTo jednak zawsze była ona wykorzystana razem z adnotacją @MessageMapping. Z informacji, które udało mi się uzyskać (dokumentacja tego nie opisuje), @SendTo publikuje komunikaty tylko gdy metoda jest wywoływana przez obsługę komunikatu (tzn. jest wywołana przez komunikat z websocketu).
WebSockets - AngularJS
Potrzebujemy klienta do websocketa. Jak to w angularze, oczywiście jest odpowiedni plugin.<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>ng-stomp</artifactId>
<version>0.4.0</version>
</dependency>
Linkujemy odpowiedni skrypt:
<script src="webjars/ng-stomp/0.4.0/dist/ng-stomp.standalone.min.js"></script>Rejestrujemy plugin:
angular.module('app', ['ngStomp'])Za pomocą zależności $stomp łączymy się do endpointa '/chat' aby ustanowić połączenie i subskrybujemy na odpowiednie komunikaty. Opcjonalnie można włączyć sobie tryb debugowania aby w konsoli przeglądarki mieć podgląd przepływu danych.
$stomp.setDebug(function (args) {
$log.debug(args);
});
$stomp
.connect('/chat')
.then(function (frame) {
var subscription = $stomp.subscribe('/topic/myevents', function (payload, headers, res) {
handler(payload);
});
});
Odświeżanie widoku wykonuję dzięki metody $apply obiektu $scope, która aktualizuje zbindowaną zmienną w kontrolerze. Obsługę tę należy przekazać jako handler do subskrypcji. $scope.$apply(function () {
vm.message = message;
});