Creating a Gateway API

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.

Spring Cloud Gateway also allows us to pull together legacy services into one unified service. Older clients can continue talking to the old system, while newer clients adopt the new gateway. This is known as API strangling (http://www.kennybastani.com/2016/08/strangling-legacy-microservices-spring-cloud.html).

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.
Don't forget to restart the Config Server after committing changes!

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.

Additionally, with WebSocket handling kept in the gateway, we can eliminate the latency of forwarding WebSocket messages to another service. It's left as an exercise for you to move WebSocket messaging into another service, configure Spring Cloud Gateway to forward them and measure the effects.

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.
Don't get confused by the fact that there are four xhr objects. One is used to fetch the image-based HTML content, the other three are used to handle new comments, delete images, and upload new images, when the corresponding button is clicked. They are in separate scopes and have no chance of bumping into each other.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.163.250