Reaktivt med Spring

Reaktivt med Spring
november 15, 2015 Ola Petersson

I år har reaktivitet fullkomligt exploderat och med det har många ramverk som t.ex. Meteor JS fått stor popularitet. Med JavaEE7 så tillkom stöd för websockets vilket är en stor möjliggörare för att bygga just responsiva webapplikationer. Jag tänkte göra ett försök att återskapa en av de features vi byggde under Hack Out West, då i Meteor, med en stack lite mer lik de man kan se ute hos de större bolagen; Spring och ReactJS.

Vad ska vi bygga?
Vi ska göra ett skal till en spel-lobby. När man kommer till vår sida så listas de spel som finns och om ett nytt skapas så ska detta i realtid uppdateras. Ingen form av refreshmekanism eller liknande används.

via GIPHY

All källkod finns på https://github.com/olbpetersson/spring-boot-reactive och jag rekommenderar att man tar sig en titt på det eftersom jag inte kommer beskriva all kod som behövs för att få ett körande exempel.

Spring med MongoDB
I mitt testexempel har jag utnyttjat den förinställda konfiguration som kommer med spring-boot-starter-data-mongodb (se pom.xml). Vid installation av en MongoDB sätts en databas upp med namnet test, och det är denna spring försöker ansluta till på localhost. Genom att sedan skapa ett interface som ärver av MongoRepository kan man utnyttja Springs magi genom att AutoWire:a (inject) detta interface och börja operera med det, helt utan någon egenskriven implementation. Mitt interface ser ut som följer:


public interface LobbyRepository extends MongoRepository<Lobby, String> {

    Lobby findByName(String name);
    List<Lobby> findAll();

}

Och det nyttjar min entitet Lobby:

public class Lobby {
    private int players;
    @Id
    private String name;

    public Lobby(String name) {
        this.players = 1;
        this.name = name;
    }
    //...(getters and setters)
}

Websocketkonfiguration
Med en databas på plats så är det dags att öppna upp vår websocket så att vi kan läsa ut våra Lobbies från databasen. En stor fördel med Spring är att de har en implementation av STOMP-protokollet i sitt framework. Detta tillåter oss att skapa ett publish-subscribe-pattern där en klient ansluter och sedan får ta del av alla meddelanden som läggs på det ämne som klienten har valt att prenumerera på. Detta gör vi genom att sätta upp en egen MessageBrokerConfigurer:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        stompEndpointRegistry.addEndpoint("/test");
    }
}

Det vi gör ovan är att registrera en endpoint (”test”) som klienten kan ansluta sin websocket till. Mha @EnableWebSocketMessageBroker kan vi också skapa en in-memory message-broker som kan servea tillbaka meddelanden från servern till klienten på ”kön” ”topic”.

Operationslager med lite fusk
För att få igång ett fungerande exempel har jag fuskat lite och skrivit ihop en rest-endpoint som populerar db:n och också notifierar alla klienter som lyssnar. Det som bör nämnas kring denna är att vi återigen nyttjar lite Spring-magi genom SimpMessagingTemplate för att kunna notifiera våra klienter.

package se.olapetersson.rest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import se.olapetersson.entities.Lobby;
import se.olapetersson.repositories.LobbyRepository;

import java.util.List;

/**
 * Created by ola on 2015-11-11.
 */
@RestController
@RequestMapping(value = "/lobbies")
public class LobbyController {

    private SimpMessagingTemplate messagingTemplate;

    @Autowired
    LobbyRepository repository;

    @Autowired
    public LobbyController(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    @RequestMapping(method = RequestMethod.GET)
    public List<Lobby> getLobbies(){
        return repository.findAll();
    }

    @RequestMapping(value="/{name}", method=RequestMethod.GET)
    public void createLobby(@PathVariable String name){

        repository.save(new Lobby(name));
        messagingTemplate.convertAndSend("/topic/lobbies", repository.findAll());
    }

    @RequestMapping(value="/{id}/clear", method=RequestMethod.GET)
    public void deleteLobby(@PathVariable String id){
        repository.delete(id);
        messagingTemplate.convertAndSend("/topic/lobbies", repository.findAll());
    }
}

Här ser vi också att jag tar lite genvägar då jag meddelar klienterna när någon opererar genom vår REST-endpoint. Man kan tänka sig att man skulle kunna flytta ner detta till t.ex. save-metoden. Det Meteor gör för att propagera databasändringar är att taila oplogen för mongodb, detta är inget jag kommer gå vidare med i denna post 🙂

Klienten
React använder ett xml-likt syntax som heter JSX, och vi behöver därför använda oss av deras transformator, jag har valt att inkludera den i min index.html vilket inte är rekommenderat för större projekt. Då bör man istället kompilera sin JSX till ren JS innan man deployar. Min head-tag inkluderar i vilket fall

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.2/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.2/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>

I react bygger vi upp våra egna komponenter. I detta fallet har vi en klass, PlayField, som innehåller flertalet LobbyCards vilka bara visas upp med sin titel. Den intressanta koden händer dock här:

var PlayField = React.createClass({

    getInitialState: function(){
        return {data: []};
    },

    componentDidMount: function(){
        this.connectToLobbies();
    },

    connectToLobbies: function(){
        var stompClient = null;
        var component = this;
        function setComponentState(data){
            component.setState(data);
        }
        function connect() {
            var socket = new WebSocket('ws://localhost:8080/test');
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function(frame) {
                console.log('Connected: ' + frame);
                initLobbies();
                stompClient.subscribe('/topic/lobbies', function(data){
                    console.log(data.body);
                    setComponentState({data: JSON.parse(data.body)});
                });
            });
        }

        function initLobbies(){
            stompClient.send("/app/lobbies/init", {}, "init");
        }
        ...
        connect();
    },
    render: function(){
        var lobbies = [];
        var lobbyData = this.state.data;
        console.log(lobbyData.length);
        for(var i =0; i < lobbyData.length; i++){
            lobbies.push(<LobbyCard title={lobbyData[i].name} key={lobbyData[i].name} />);
        }
        return (
            <div className="jumbotron container-fluid">
                <h2>Lobbies</h2>
                {lobbies}
            </div>
        );
    }
});

Det första vi gör är att rendera vår PlayField-klass. När denna har monterats i DOM:en så ansluter vi till en websocket och använder sedan StompJs för att få dit protokollet över anslutningen. Vi börjar sedan prenumerera på vår topic ”lobbies” och skickar sedan en initial ping för att få svaret med de existerande lobby-entiterna. I vår callback för vår prenumeration så byter vi vid ny data state på vårt playfield för att på detta sätt direkt få uppdateringar ut till vårt GUI.

Showtime
Om du klonade ut repot och har installerat mongoDB ska det nu bara vara att köra mvn spring-boot:run . Om du går mot http://localhost:8080 och sedan startar en ny webläsare där du går mot http://localhost:8080/lobbies/Squeed bör du nu direkt se att ett spelkort med titeln Squeed dyker upp. Gå mot /lobbies/Squeed/clear så ser du att spelkortet direkt försvinner.

För den som är intresserad av den mer färdiga produkten kan man se meteor-implementation av Destination Unknownhttps://github.com/HackOutWest15/destinationunknown

via GIPHY

1 Kommentar

Pingbacks

  1. […] my earlier blogpost (warning, it’s in Swedish) I wanted to follow up on it with another technology that has […]

Lämna ett svar

E-postadressen publiceras inte. Obligatoriska fält är märkta *

*

Denna webbplats använder Akismet för att minska skräppost. Lär dig hur din kommentardata bearbetas.