Another kind of optimization we can think about is sending the data to the client as it becomes available to the server. Since we fetch results of the search in multiple threads, the data will come in multiple chunks. We could send them bit by bit instead of waiting for all the results.
Spring has excellent support for WebSockets, which is a protocol that allows clients to maintain a long-running connection to the server. Data can be pushed in web sockets on both ends of the connection and consumers will get the data in real-time.
We will use a JavaScript library called SockJS to ensure compatibility with all browsers. Sockjs will transparently fall back on another strategy if our users have an outdated browser.
We will also use StompJS to connect to our message broker.
Add the following library to your build:
compile 'org.springframework.boot:spring-boot-starter-websocket' compile 'org.springframework:spring-messaging' compile 'org.webjars:sockjs-client:1.0.0' compile 'org.webjars:stomp-websocket:2.3.3'
Add the WebJars to our default Thymeleaf template:
<script src="/webjars/sockjs-client/1.0.0/sockjs.js"></script> <script src="/webjars/stomp-websocket/2.3.3/stomp.js"></script>
To configure WebSockets in our application, we need to add a bit of configuration as well:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/ws"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/twitterSearch").withSockJS(); } }
This will configure the different channels available in our application. SockJS clients will connect to the twitterSearch
endpoint and will push data to the server on /ws/ channel
and be able to listen to /topic/
for changes.
This will allow us to inject a SimpMessagingTemplate
in a new controller to push data to the client in the /topic/searchResult
channel, as follows:
@Controller public class SearchSocketController { private CachedSearchService searchService; private SimpMessagingTemplate webSocket; @Autowired public SearchSocketController(CachedSearchService searchService, SimpMessagingTemplate webSocket) { this.searchService = searchService; this.webSocket = webSocket; } @MessageMapping("/search") public void search(@RequestParam List<String> keywords) throws Exception { Consumer<List<LightTweet>> callback = tweet -> webSocket.convertAndSend("/topic/searchResults", tweet); twitterSearch(SearchParameters.ResultType.POPULAR, keywords, callback); } public void twitterSearch(SearchParameters.ResultType resultType, List<String> keywords, Consumer<List<LightTweet>> callback) { keywords.stream() .forEach(keyword -> { searchService.search(resultType, keyword) .addCallback(callback::accept, Throwable::printStackTrace); }); } }
In our resultPage
, the JavaScript code is really simple:
var currentLocation = window.location.href; var search = currentLocation.substr(currentLocation.lastIndexOf('=') + 1); function connect() { var socket = new SockJS('/hello'); stompClient = Stomp.over(socket); // stompClient.debug = null; stompClient.connect({}, function (frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/searchResults', function (result) { displayTweets(JSON.parse(result.body)); }); stompClient.send("/app/search", {}, JSON.stringify(search.split(','))); }); }
The displayTweets
function remains essentially the same as before:
function displayTweets(tweets) { $.each(tweets, function (index, tweet) { addTweet(tweet); }) } function addTweet(tweet) { var template = _.template('<li class="collection-item avatar">' + '<img class="circle" src="${tweet.profileImageUrl}" />' + '<span class="title">${tweet.userName}</span>' + '<p>${tweet.text}</p>' + '</li>'); $('#tweets').append(template({tweet: tweet})); }
Here you go! The client will now receive the results of all the searches in the application-- live!
Before pushing this to production, it will require a little bit more work. Here are some ideas:
There is much more to WebSocket than just this simple example. Don't forget to have a look at the documentation at http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html for more information.
18.221.117.214