Przejdź do głównej zawartości

Aplikacja czasu rzeczywistego w Spring i AngularJS

Naszła mnie potrzeba aby utworzyć pewien dashboard, który reagowałby na zmiany przychodzące z serwera. Jako, że tworzę swoje aplikacje z wykorzystaniem Spring Boot (backend) i AngularJS (frontend) to o komunikacje serwer -> klient należy zadbać samemu. Po pospiesznej analizie tematu okazało się, że taką komunikację zapewnia protokół WebSockets (co dla niektórych może jest oczywiste, lecz dla mnie nie było). Co trzeba zrobić? Zestawić połączenie pomiędzy serwerem, a klientem i przesyłać wiadomości. Proste? Proste. No to jazda.

Projekt

Kombinacja Spring Boot i AngularJS (angular w wersji 1) tak mi się spodobała, że utworzyłem sobie własny archetyp mavena, aby szybko 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, za pomocą 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ę za pomocą 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;
});

Komentarze

Prześlij komentarz

Popularne posty z tego bloga

Sneaky throws, czyli checked i unchecked exception w jednym!

Często podczas pisania kodu Java korzystam z wyrażeń lambda wprowadzonych wraz z Java 8. Często prowadzi to też do pewnych nowych problemów i zmusza do szukania rozwiązań. Przyjrzyjmy się jednemu z nich. Checked exception w wyrażeniu lambda Często zdarza się, że metoda wywoływana wewnątrz lambdy rzuca wyjątek. Nie ma sprawy gdy wyjątek jest obiektem klasy będącej podklasą RuntimeException . Wtedy po prostu się nim nie przejmujemy. Wyjątek jest przekazywany w górę stosu wywołań. Problem zaczyna się gdy kompilator zmusza nas do obsługi wyjątku. Jak zwykle mamy 2 wyjścia: obsłużyć wyjątek za pomocą bloku try-catch , bądź zadeklarować przekazanie wyjątku dalej za pomocą słowa kluczowego throws . O ile wiemy co zrobić po złapaniu wyjątku to wszystko gra. Co natomiast gdy chcemy wybrać drugą opcję? Większość interfejsów stosowanych jako typy parametrów, często przekazywanych jako lambdy jak np: Function , Consumer czy Supplier nie deklarują, że mogą rzucić wyjątek. W takim wypadku druga

Wzorzec Open Session in View w Spring Boot

Dzisiejszy post będzie z cyklu: "Wtf? Dlaczego to działa?". A dotyczy on pewnego wzorca, którego implementacja jak się okazuje jest we frameworku Spring Boot domyślnie włączona, czego nie wszyscy programiści mogą się spodziewać. Geneza Zaczęło się od tego, że pisałem kolejny test integracyjny do kolejnego kontrolera. Test napisany, uruchamiam, zielono. No i przeglądam sobie jeszcze raz kod zanim wypchnę zmiany do review. I rzuciło mi się w oczy, że zapomniałem dodać adnotacji @Transactional (do tej pory uważałem, że to jest zawsze wymagane). I wtedy zadałem sobie przytoczone na wstępie pytanie. Dlaczego test przeszedł skoro mój serwis dociąga sobie obiekt oznaczony jako LAZY ? Dużo się nagimnastykowałem by wpisać odpowiednie query do Google'a i znaleźć odpowiedź: Open Session in View. Ale o tym za chwilę. Jak działa Hibernate Żeby lepiej zrozumieć opisywane zagadnienie warto przypomnieć jakie kroki w uproszczeniu musi wykonać Hibernate podczas komunikacji z bazą d

Dlaczego default jest blee... Wielodziedziczenie

Piszę ten post gdyż złapałem się na tym, że nie potrafiłem uargumentować swojego przekonania dotyczącego pewnego aspektu programowania gdy padło pytanie: "dlaczego?". Dopiero później wszystko sobie przypomniałem i mam nadzieję, że ten post nie pozwoli mi już o tym zapomnieć ;). Dawno, dawno temu, gdy ukazała się Java w wersji 8, wielu z nas zachwycało się nowym mechanizmem: metody default! Bo to takie fajne, nowe, można ograniczyć powtarzanie się kodu, no generalnie bajer. Dopiero po chwili niestety, dotarło, że wprowadzenie takiego rozwiązania to cofanie się w czasie.  Wielodziedziczenie Podczas lat rozwoju języków programowania pojawił się w pewnym momencie trend aby unikać stosowania rozwiązania zwanego wielodziedziczeniem. Czym jest ten mechanizm? Pozwala on na dziedziczenie z wielu klas bazowych jednocześnie. Unikanie jego stosowania, a nawet unikanie implementacji takiego mechanizmu w nowoczesnych językach jest poparte sensownymi argumentami. Po pierwsze, może wy