What is a Gateway API? It's a one-stop facade where we can make all our various web requests. The facade then dispatches the requests to the proper backend service based on the configuration settings.
In our case, we don't want the browser talking to two different ports. Instead, we'd rather serve up a single, unified service with different URL paths.
In Chapter 7, Microservices with Spring Boot, we used Spring Cloud for several microservice tasks, including service discovery, circuit breaker, and load balancing. Another microservice-based tool we will make use of is Spring Cloud Gateway, a tool for building just such a proxy service.
Let's start by adding this to our chat microservice:
compile('org.springframework.cloud:spring-cloud-starter-gateway')
With Spring Cloud Gateway on the classpath, we don't have to do a single thing to activate it in our chat microservice. Out of the box, Spring Cloud Gateway makes the chat microservice our front door for all client calls. What does that mean?
Spring Cloud Gateway forwards various web calls based on patterns to its respective backend service. This allows us to split up the backend into various services with some simple settings, yet offer a seamless API to any client.
To configure which URL patterns are forwarded where, we need to add this to our chat.yml stored in the Config Server:
spring: cloud: gateway: routes: # ======================================================== - id: imagesService uri: lb://IMAGES predicates: - Path=/imagesService/** filters: - RewritePath=/imagesService/(?<segment>.*), /${segment} - RewritePath=/imagesService, / - SaveSession - id: images uri: lb://IMAGES predicates: - Path=/images/** filters: - SaveSession - id: mainCss uri: lb://IMAGES predicates: - Path=/main.css filters: - SaveSession - id: commentsService uri: lb://IMAGES predicates: - Path=/comments/** filters: - SaveSession
Looking at the preceding code, we can discern the following:
- Each entry has an id, a uri, an optional collection of predicates, and an optional list of filters.
- Looking at the first entry, we can see that requests to /imagesService are routed to the load-balanced (lb: prefix), Eureka-registered IMAGES service. There are filters to strip the imagesService prefix.
- All requests to /images will also be sent to the images microservice. However, compared to /imagesServices, the full path of the request will be sent. For example, a request to /images/abc123 will be forwarded to the images service as /images/abc123, and not as /abc123. We'll soon see why this is important.
- Asking for /main.css will get routed to images as well.
- All requests to /comments will get sent to images, full path intact. (Remember that images uses Ribbon to remotely invoke comments, and we don't want to change that right now).
- All of these rules include the SaveSession filter, a custom Spring Cloud Gateway filter we'll write shortly to ensure our session data is saved before making any remote call.
What's going on?
First and foremost, we create a Gateway API, because we want to keep image management and chatting as separate, nicely defined services. At one point in time, there was only HTTP support. WebSocket support is newly added to Spring Cloud Gateway, so we don't use it yet, but keep all of our WebSocket handling code in the gateway instead. In essence, the chat microservice moves to the front, and the images microservice moves to the back.
This suggests that we should have chat serve up the main Thymeleaf template, but have it fetch image-specific bits of HTML from the images service.
To go along with this adjustment to our social media platform, let's create a Thymeleaf template at src/main/resources/templates/index.html in chat like this:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>Learning Spring Boot: Spring-a-Gram</title> <link rel="stylesheet" href="/main.css" /> </head> <body> <div> <span th:text="${authentication.name}" /> <span th:text="${authentication.authorities}" /> </div> <hr /> <h1>Learning Spring Boot - 2nd Edition</h1> <div id="images"></div> <div id="chatBox"> Greetings! <br/> <textarea id="chatDisplay" rows="10" cols="80" disabled="true" ></textarea> <br/> <input id="chatInput" type="text" style="width: 500px" value="" /> <br/> <button id="chatButton">Send</button> <br/> </div> </body> </html>
This preceding template can be described as follows:
- It's the same header as we saw in the previous chapter, including the main.css stylesheet.
- The <h1> header has been pulled in from the image service.
- For images, we have a tiny <div> identified as images. We need to write a little code to populate that from our images microservice.
- Finally, we have the same chat box shown in the earlier chapter.
- By the way, we remove the connect/disconnect buttons, since we will soon leverage Spring Security's user information for WebSocket messaging!
To populate the images <div>, we need to write a tiny piece of JavaScript and stick it at the bottom of the page:
<script th:inline="javascript"> /*<![CDATA[*/ (function() { var xhr = new XMLHttpRequest(); xhr.open('GET', /*[[@{'/imagesService'}]]*/'', true); xhr.onload = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { document.getElementById('images').innerHTML = xhr.responseText; // Register a handler for each button document.querySelectorAll('button.comment') .forEach(function(button) { button.addEventListener('click', function() { e.preventDefault(); var comment = document.getElementById( 'comment-' + button.id); var xhr = new XMLHttpRequest(); xhr.open('POST', /*[[@{'/comments'}]]*/'', true); var formData = new FormData(); formData.append('comment', comment.value); formData.append('imageId', button.id); xhr.send(formData); comment.value = ''; }); }); document.querySelectorAll('button.delete') .forEach(function(button) { button.addEventListener('click', function() { e.preventDefault(); var xhr = new XMLHttpRequest(); xhr.open('DELETE', button.id, true); xhr.withCredentials = true; xhr.send(null); }); }); document.getElementById('upload') .addEventListener('click', function() { e.preventDefault(); var xhr = new XMLHttpRequest(); xhr.open('POST', /*[[@{'/images'}]]*/'', true); var files = document .getElementById('file').files; var formData = new FormData(); formData.append('file', files[0], files[0].name); xhr.send(formData); }) } } } xhr.send(null); })(); /*]]>*/ </script>
This code can be explained as follows:
- The whole thing is an immediately invoked function expression (IIFE), meaning no risk of global variable collisions.
- It creates an XMLHttpRequest named xhr to do the legwork, opening an asynchronous GET request to /imagesService.
- A callback is defined with the onload function. When it completes with a successful response status, the images <div> will have its innerHTML replaced by the response, ensuring that the DOM content is updated using document.getElementById('images').innerHTML = xhr.responseText.
- After that, it will register handlers for each of the image's comment buttons (something we've already seen). The delete buttons and one upload button will also be wired up.
- With the callback defined, the request is sent.
Since we only need the image-specific bits of HTML from the images microservice, we should tweak that template to serve up a subset of what it did in the previous chapter, like this:
<!DOCTYPE html> <div xmlns:th="http://www.thymeleaf.org"> <table> <!-- ...the rest of the image stuff we've already seen... -->
This last fragment of HTML can be explained as follows:
- This is no longer a complete page of HTML, hence, no <html>, <head>, and <body> tags. Instead, it's just a <div>.
- Despite being just a <div>, we need the Thymeleaf namespace th to give the IDE the right information to help us with code completion.
- From there, it goes into the table structure used to display images. The rest is commented out, since it hasn't changed.
With these changes to chat and images, along with the Spring Cloud Gateway settings, we have been able to merge what appeared as two different services into one. Now that these requests will be forwarded by Spring Cloud Gateway, there is no longer any need for CORS settings. Yeah!
This means we can slim down our WebSocket configuration as follows:
@Bean HandlerMapping webSocketMapping(CommentService commentService, InboundChatService inboundChatService, OutboundChatService outboundChatService) { Map<String, WebSocketHandler> urlMap = new HashMap<>(); urlMap.put("/topic/comments.new", commentService); urlMap.put("/app/chatMessage.new", inboundChatService); urlMap.put("/topic/chatMessage.new", outboundChatService); SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); mapping.setOrder(10); mapping.setUrlMap(urlMap); return mapping; }
The preceding code is the same as shown earlier in this chapter, but with the CORS settings, which we briefly saw earlier, removed.
As a reminder, we are focusing on writing Java code. However, in this day and age, writing JavaScript is unavoidable when we talk about dynamic updates over WebSockets. For a full-blown social media platform with a frontend team, something like webpack (https://webpack.github.io/) and babel.js (https://babeljs.io/) would be more suitable than embedding all this JavaScript at the bottom of the page. Nevertheless, this book isn't about writing JavaScript-based apps. Let's leave it as an exercise to pull out all this JavaScript from the Thymeleaf template and move it into a suitable module-loading solution.