Skip to content

Latest commit

 

History

History
468 lines (411 loc) · 18.5 KB

lab05.adoc

File metadata and controls

468 lines (411 loc) · 18.5 KB

Adding Service Registration and Discovery with Spring Cloud

In this lab we’ll utilize Spring Boot and Spring Cloud to configure our application to register itself with a service registry. To do this we’ll also need to provision an instance of a Eureka service registry using Pivotal Cloud Foundry Spring Cloud Services. We’ll also add a simple client application that looks up our application from the service registry and makes requests to our Cities service.

Update Cloud-Native-Spring Boot Application to Register with Eureka

  1. These features are added by adding spring-cloud-services-starter-service-registry to the classpath. Open your Maven POM found here: /cloud-native-spring/pom.xml. Add the following spring cloud services dependency:

    <dependency>
        <groupId>io.pivotal.spring.cloud</groupId>
        <artifactId>spring-cloud-services-starter-service-registry</artifactId>
    </dependency>
  2. Thanks to Spring Cloud, instructing your application to register with Eureka is as simple as adding a single annotation to your app! Add an @EnableDiscoveryClient annotation to the class io.pivotal.CloudNativeSpringApplication (/cloud-native-spring/src/main/java/io/pivotal/CloudNativeApplication.java):

    @SpringBootApplication
    @EnableDiscoveryClient
    public class CloudNativeSpringApplication {

    Completed:

    package io.pivotal.cloudnativespring;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    public class CloudNativeSpringApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(CloudNativeSpringApplication.class, args);
        }
    
    }

Create Spring Cloud Service Registry instance and deploy application

  1. Now that our application is ready to read and register with a Eureka instance, we need to deploy a Eureka Registry! This can be done through Pivotal Cloud Foundry using the Services Marketplace. Previously we did this through the Marketplace UI, but this time we will use the Cloudfoundry CLI (though we could also do this through the UI:

    $ cf create-service p-service-registry standard service-registry

    Or you can use Services Marketplace to create an instance of Service Registry:

    registry create
  2. After you create the service registry instance, navigate to your cloudfoundry space in the Apps Manager UI and refresh the page. You should now see the newly create service registry intance. Select the manage link to view the registry dashboard. Note that there are not any registered applications at the moment:

    registry1
  3. We will now bind our application to our service-registry within our Cloudfoundry deployment manifest. Add the additional reference to the service to the bottom of /cloud-native-spring/manifest.yml in the services list:

      services:
      - config-server
      - service-registry

    Complete:

    ---
    applications:
      - name: cloud-native-spring
        random-route: true
        memory: 1G
        instances: 1
        timeout: 180 # to give time for the data to import
        path: ./target/cloud-native-spring-0.0.1-SNAPSHOT-exec.jar
        buildpack: java_buildpack_offline
        env:
          TRUST_CERTS: api.cnd-workshop.pcfdot.com
        services:
          - config-server
          - service-registry

Deploy and test application

  1. Build the application

    $ mvn clean package
  2. For the 2nd half of this lab we’ll need to have this maven artifact in our local repository, so install it with the following command:

    $ mvn install
  3. Push application into Cloud Foundry

    $ cf push
  4. If we now test our application URLs we will see no change. However, if we view the Service Registry dashboard (accessible from the manage link in Apps Manager) you will see that a service named cloud-native-spring has been registered:

    registry2
  5. Next we’ll create a simple UI application that will read the service registry to discover the location of our cities REST service and connect.

Create another Spring Boot Project as a Client UI

  1. Browse to https://start.spring.io

  2. Generate a Maven Project with Spring Boot 2.1.3.

  3. Fill out the Project metadata fields as follows:

    Group

    io.pivotal

    Artifact

    cloud-native-spring-ui

  4. In the dependencies section, add the following:

    Thymeleaf, Actuator, Feign, HATEOS

  5. Click the Generate Project button. Your browser will download a zip file.

    start spring io
  6. Copy then unpack the downloaded zip file to Cloud-Native-Java-Workshop/labs/lab05/cloud-native-spring-ui

    Your directory structure should now look like:

    Cloud-Native-Java-Workshop:
    ├── labs
    │   ├── lab01
    │   │   ├── cloud-native-spring
    │   ├── lab05
    │   │   ├── cloud-native-spring-ui
  7. Import the project’s pom.xml into your editor/IDE of choice.

  8. We will need to add a the general entry for Spring Cloud Services dependency management as we added to our other project. Open your Maven POM found here: /cloud-native-spring-ui/pom.xml:

    <dependency>
        <groupId>io.pivotal.spring.cloud</groupId>
        <artifactId>spring-cloud-services-dependencies</artifactId>
        <version>2.0.2.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>

    Completed POM import dependencies section:

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>io.pivotal.spring.cloud</groupId>
                <artifactId>spring-cloud-services-dependencies</artifactId>
                <version>2.0.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
  9. As before, we need to add spring-cloud-services-starter-service-registry and reference to io.pivotal.cloudnativespring.domain.City Entity class to the classpath. Add these to your POM:

    <dependency>
        <groupId>io.pivotal.spring.cloud</groupId>
        <artifactId>spring-cloud-services-starter-service-registry</artifactId>
    </dependency>
    <dependency>
        <groupId>io.pivotal</groupId>
        <artifactId>cloud-native-spring</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>

    Completed dependencies:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>io.pivotal.spring.cloud</groupId>
            <artifactId>spring-cloud-services-starter-service-registry</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-hateoas</artifactId>
        </dependency>
        <dependency>
            <groupId>io.pivotal</groupId>
            <artifactId>cloud-native-spring</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
  10. Since this UI is going to consume REST services, it’s an awesome opportunity to use Feign. Feign will handle ALL the work of invoking our services and marshalling/unmarshalling JSON into domain objects. We’ll add a Feign Client interface into our app. Take note of how Feign references the downstream service; it’s only the name of the service it will lookup from Eureka service registry. Add the following interface declaration to the CloudNativeSpringUiApplication:

    @FeignClient(value = "cloud-native-spring")
    interface CityClient {
        @RequestMapping(method = RequestMethod.GET, value = "/cities", produces = "application/hal+json")
        PagedResources<City> getCities();
    }

    We’ll also need to add a few annotations to our boot application:

    @SpringBootApplication
    @EnableFeignClients
    @EnableDiscoveryClient
    public class CloudNativeSpringUiApplication {

    Completed:

    package io.pivotal.cloudnativespringui;
    
    import io.pivotal.cloudnativespring.domain.City;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.hateoas.PagedResources;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    @SpringBootApplication
    @EnableFeignClients
    @EnableDiscoveryClient
    public class CloudNativeSpringUiApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(CloudNativeSpringUiApplication.class, args);
        }
    
        @FeignClient(value = "cloud-native-spring")
        interface CityClient {
            @RequestMapping(method = RequestMethod.GET, value = "/cities", produces = "application/hal+json")
            PagedResources<City> getCities();
        }
    }
  11. Next we’ll create a simple @Controller for serving up the thymeleaf page. Create the class io.pivotal.cloudnativespringui.CitiesController (/cloud-native-spring-ui/src/main/java/io/pivotal/cloudnativespringui/CitiesController.java) and into it paste the following code:

    package io.pivotal.cloudnativespringui;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    
    @Controller
    public class CitiesController {
    
        private CloudNativeSpringUiApplication.CityClient feignCitiesClient;
    
        public CitiesController(CloudNativeSpringUiApplication.CityClient feignCitiesClient) {
            this.feignCitiesClient = feignCitiesClient;
        }
    
        @GetMapping("/")
        public String getCities(Model model) {
    
            model.addAttribute("cities", feignCitiesClient.getCities());
            return "cities"; //thymeleaf template
        }
    }
  12. Next we’ll create a Thymeleaf template for rendering our data. Our template will consume the content returned by Feign client we just created. Create template cities.html under /cloud-native-spring-ui/src/main/resources/templates

    Your directory structure should now look like:

    cloud-native-spring-ui:
    ├── src
    │   ├── main
    │   │   ├── resources
    │   │   │   ├── templates
    │   │   │   │   ├── cities.html

    And paste the following code into it:

    <!doctype html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
          xmlns:th="http://www.thymeleaf.org">
    
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
              integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    
        <title>Spring Boot Discovery Client Example - Cities</title>
    </head>
    <body>
    <h1>Spring Boot Discovery Client Example - Cities</h1>
    
    <div>
        <table id="cityTable" class="table table-dark">
            <thead>
            <tr>
                <th scope="col">Name</th>
                <th scope="col">County</th>
                <th scope="col">State</th>
                <th scope="col">PostalCode</th>
                <th scope="col">Latitude</th>
                <th scope="col">Longitude</th>
            </tr>
            </thead>
            <tr th:each="city : ${cities}">
                <th scope="row" th:text="${city.name}"></th>
                <td th:text="${city.county}"></td>
                <td th:text="${city.stateCode}"></td>
                <td th:text="${city.postalCode}"></td>
                <td th:text="${city.latitude}"></td>
                <td th:text="${city.longitude}"></td>
            </tr>
        </table>
    </div>
    
    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
            integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
            crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
            integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
            crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
            integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
            crossorigin="anonymous"></script>
    </body>
    </html>
  13. We’ll also want to give our UI App a name so that it can register properly with Eureka and potentially use cloud config in the future. Add the following configuration to /cloud-native-spring-ui/src/main/resources/application.properties:

    spring.jpa.generate-ddl=false
    spring.jpa.hibernate.ddl-auto=none
    spring.application.name=cloud-native-spring-ui
    spring.data.rest.base-path=/api

Deploy and test application

  1. Build the application. We have to skip the tests otherwise we may fail because of having 2 spring boot apps on the classpath

    $ mvn clean package -DskipTests
  2. Create an application manifest in the root folder /cloud-native-spring-ui

    $ touch manifest.yml

  3. Add application metadata

    ---
    applications:
        - name: cloud-native-spring-ui
          random-route: true
          memory: 1G
          instances: 1
          path: ./target/cloud-native-spring-ui-0.0.1-SNAPSHOT.jar
          buildpack: java_buildpack_offline
          env:
            TRUST_CERTS: api.cnd-workshop.pcfdot.com
          services:
            - service-registry
  4. Push application into Cloud Foundry

    $ cf push
  5. Test your application by navigating to the URL of the application, which should show the Thymeleaf template. You should now see a table listing the first set of rows returned from the cities microservice

    ui
  6. From a commandline stop the cloud-native-spring microservice (the original city service, not the new UI)

    $ cf stop cloud-native-spring
  7. Refresh the UI app. What happens? Now you get a nasty error that is not very user friendly!

  8. Next we’ll learn how to make our UI Application more resilient in the case that our downstream services are unavailable.