Skip to content

Latest commit

 

History

History
831 lines (739 loc) · 24.5 KB

demo-script.adoc

File metadata and controls

831 lines (739 loc) · 24.5 KB

Building a Cloud-ready Microservice

This document is intended to be used as a script for demonstrating the construction of a database-backed REST microservice based on Spring Boot, Spring Data, and Spring Cloud, and deployment of the service to Cloud Foundry. It can also be used as a self-guided tutorial.

Prerequisites

  • Gradle

    • can also be installed via Homebrew (brew install gradle) or GVM (gvm install gradle)

  • An IDE or text editor

  • An account on a Pivotal CF system with a MySQL service available

    • the script can be modified for other Cloud Foundry systems or other database services as required

Steps

Initializing the Application

  1. Browse to Spring Initializr and complete the form as follows:

    Group

    org.example

    Artifact

    cities

    Name

    cities

    Description

    Demo project

    Package name

    org.example.cities

    Styles

    Actuator, JPA, Rest Repositories, Web

    Type

    Gradle project

    Then click Generate. This will result in a file called starter.zip being downloaded to your workstation.

  2. Create a directory to house your code, and into that directory unzip starter.zip.

  3. Optionally generate project files for your favorite IDE by running gradle idea or gradle eclipse. Then open the project in your editor or IDE of choice.

  4. Add a runtime dependency on the HyperSQL in-memory database to build.gradle:

    dependencies {
        // ...
        runtime("org.hsqldb:hsqldb")
    }
  5. Create the package org.example.cities.domain and in that package create the class City. Into that file you can paste the following source code, which represents cities based on postal codes, global coordinates, etc:

    @Entity
    @Table(name="city")
    public class City implements Serializable {
        private static final long serialVersionUID = 1L;
    
        @Id
        @GeneratedValue
        private long id;
    
        @Column(nullable = false)
        private String name;
    
        @Column(nullable = false)
        private String county;
    
        @Column(nullable = false)
        private String stateCode;
    
        @Column(nullable = false)
        private String postalCode;
    
        @Column
        private String latitude;
    
        @Column
        private String longitude;
    
        public String getName() { return name; }
    
        public void setName(String name) { this.name = name; }
    
        public String getPostalCode() { return postalCode; }
    
        public void setPostalCode(String postalCode) { this.postalCode = postalCode; }
    
        public long getId() { return id; }
    
        public void setId(long id) { this.id = id; }
    
        public String getStateCode() { return stateCode; }
    
        public void setStateCode(String stateCode) { this.stateCode = stateCode; }
    
        public String getCounty() { return county; }
    
        public void setCounty(String county) { this.county = county; }
    
        public String getLatitude() { return latitude; }
    
        public void setLatitude(String latitude) { this.latitude = latitude; }
    
        public String getLongitude() { return longitude; }
    
        public void setLongitude(String longitude) { this.longitude = longitude; }
    }

    Notice that we’re using JPA annotations on the class and its fields. You’ll need to use your IDE’s features to add the appropriate import statements.

  6. Create the package org.example.cities.repositories and in that package create the interface CityRepository. Paste the following code and add appropriate imports:

    @RepositoryRestResource(collectionResourceRel = "cities", path = "cities")
    public interface CityRepository extends PagingAndSortingRepository<City, Long> {
    }
  7. Add JPA and REST Repository support to the org.example.cities.Application class that was generated by Spring Initializr.

    @Configuration
    @ComponentScan
    @EnableAutoConfiguration
    @EnableJpaRepositories // <---- Add this
    @Import(RepositoryRestMvcConfiguration.class) // <---- And this
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
  8. Build the application:

    $ gradle assemble
  9. Run the application:

    $ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar
  10. Access the application using curl. You’ll see that the primary endpoint automatically exposes the ability to page, size, and sort the response JSON.

    So what have you done? Created four small classes and one build file, resulting in a fully-functional REST microservice. The application’s DataSource is created automatically by Spring Boot using the in-memory database because no other DataSource was detected in the project.

    $ curl -i localhost:8080/cities
    HTTP/1.1 200 OK
    Server: Apache-Coyote/1.1
    X-Application-Context: application
    Content-Type: application/hal+json
    Transfer-Encoding: chunked
    Date: Tue, 27 May 2014 19:34:45 GMT
    
    {
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/cities{?page,size,sort}",
          "templated" : true
        }
      },
      "page" : {
        "size" : 20,
        "totalElements" : 0,
        "totalPages" : 0,
        "number" : 0
      }
    }

    Next we’ll import some data.

Importing Data

  1. Add this import.sql file to src/main/resources. This is a rather large dataset containing all of the postal codes in the United States and its territories. This file will automatically be picked up by Hibernate and imported into the in-memory database.

  2. Build the application:

    $ gradle assemble
  3. Run the application:

    $ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar
  4. Access the application again using curl. Notice the appropriate hypermedia is included for next, previous, and self. You can also select pages and page size by utilizing ?size=n&page=n on the URL string. Finally, you can sort the data utilizing ?sort=fieldName.

    $ curl -i localhost:8080/cities
    HTTP/1.1 200 OK
    Server: Apache-Coyote/1.1
    X-Application-Context: application
    Content-Type: application/hal+json
    Transfer-Encoding: chunked
    Date: Tue, 27 May 2014 19:59:58 GMT
    
    {
      "_links" : {
        "next" : {
          "href" : "http://localhost:8080/cities?page=1&size=20"
        },
        "self" : {
          "href" : "http://localhost:8080/cities{?page,size,sort}",
          "templated" : true
        }
      },
      "_embedded" : {
        "cities" : [ {
          "name" : "HOLTSVILLE",
          "county" : "SUFFOLK",
          "stateCode" : "NY",
          "postalCode" : "00501",
          "latitude" : "+40.922326",
          "longitude" : "-072.637078",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/1"
            }
          }
        }, {
          "name" : "HOLTSVILLE",
          "county" : "SUFFOLK",
          "stateCode" : "NY",
          "postalCode" : "00544",
          "latitude" : "+40.922326",
          "longitude" : "-072.637078",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/2"
            }
          }
        }, {
          "name" : "ADJUNTAS",
          "county" : "ADJUNTAS",
          "stateCode" : "PR",
          "postalCode" : "00601",
          "latitude" : "+18.165273",
          "longitude" : "-066.722583",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/3"
            }
          }
        }, {
          "name" : "AGUADA",
          "county" : "AGUADA",
          "stateCode" : "PR",
          "postalCode" : "00602",
          "latitude" : "+18.393103",
          "longitude" : "-067.180953",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/4"
            }
          }
        }, {
          "name" : "AGUADILLA",
          "county" : "AGUADILLA",
          "stateCode" : "PR",
          "postalCode" : "00603",
          "latitude" : "+18.455913",
          "longitude" : "-067.145780",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/5"
            }
          }
        }, {
          "name" : "AGUADILLA",
          "county" : "AGUADILLA",
          "stateCode" : "PR",
          "postalCode" : "00604",
          "latitude" : "+18.493520",
          "longitude" : "-067.135883",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/6"
            }
          }
        }, {
          "name" : "AGUADILLA",
          "county" : "AGUADILLA",
          "stateCode" : "PR",
          "postalCode" : "00605",
          "latitude" : "+18.465162",
          "longitude" : "-067.141486",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/7"
            }
          }
        }, {
          "name" : "MARICAO",
          "county" : "MARICAO",
          "stateCode" : "PR",
          "postalCode" : "00606",
          "latitude" : "+18.172947",
          "longitude" : "-066.944111",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/8"
            }
          }
        }, {
          "name" : "ANASCO",
          "county" : "ANASCO",
          "stateCode" : "PR",
          "postalCode" : "00610",
          "latitude" : "+18.288685",
          "longitude" : "-067.139696",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/9"
            }
          }
        }, {
          "name" : "ANGELES",
          "county" : "UTUADO",
          "stateCode" : "PR",
          "postalCode" : "00611",
          "latitude" : "+18.279531",
          "longitude" : "-066.802170",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/10"
            }
          }
        }, {
          "name" : "ARECIBO",
          "county" : "ARECIBO",
          "stateCode" : "PR",
          "postalCode" : "00612",
          "latitude" : "+18.450674",
          "longitude" : "-066.698262",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/11"
            }
          }
        }, {
          "name" : "ARECIBO",
          "county" : "ARECIBO",
          "stateCode" : "PR",
          "postalCode" : "00613",
          "latitude" : "+18.458093",
          "longitude" : "-066.732732",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/12"
            }
          }
        }, {
          "name" : "ARECIBO",
          "county" : "ARECIBO",
          "stateCode" : "PR",
          "postalCode" : "00614",
          "latitude" : "+18.429675",
          "longitude" : "-066.674506",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/13"
            }
          }
        }, {
          "name" : "BAJADERO",
          "county" : "ARECIBO",
          "stateCode" : "PR",
          "postalCode" : "00616",
          "latitude" : "+18.444792",
          "longitude" : "-066.640678",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/14"
            }
          }
        }, {
          "name" : "BARCELONETA",
          "county" : "BARCELONETA",
          "stateCode" : "PR",
          "postalCode" : "00617",
          "latitude" : "+18.447092",
          "longitude" : "-066.544255",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/15"
            }
          }
        }, {
          "name" : "BOQUERON",
          "county" : "CABO ROJO",
          "stateCode" : "PR",
          "postalCode" : "00622",
          "latitude" : "+17.998531",
          "longitude" : "-067.187318",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/16"
            }
          }
        }, {
          "name" : "CABO ROJO",
          "county" : "CABO ROJO",
          "stateCode" : "PR",
          "postalCode" : "00623",
          "latitude" : "+18.062201",
          "longitude" : "-067.149541",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/17"
            }
          }
        }, {
          "name" : "PENUELAS",
          "county" : "PENUELAS",
          "stateCode" : "PR",
          "postalCode" : "00624",
          "latitude" : "+18.023535",
          "longitude" : "-066.726156",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/18"
            }
          }
        }, {
          "name" : "CAMUY",
          "county" : "CAMUY",
          "stateCode" : "PR",
          "postalCode" : "00627",
          "latitude" : "+18.477891",
          "longitude" : "-066.854770",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/19"
            }
          }
        }, {
          "name" : "CASTANER",
          "county" : "LARES",
          "stateCode" : "PR",
          "postalCode" : "00631",
          "latitude" : "+18.269187",
          "longitude" : "-066.864993",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/cities/20"
            }
          }
        } ]
      },
      "page" : {
        "size" : 20,
        "totalElements" : 42741,
        "totalPages" : 2138,
        "number" : 0
      }
    }
  5. Try the following curl statements to see how the application behaves:

    $ curl -i "localhost:8080/cities?size=5"
    $ curl -i "localhost:8080/cities?size=5&page=3"
    $ curl -i "localhost:8080/cities?sort=postalCode,desc"

    Next we’ll add searching capabilities.

  1. Let’s add some additional finder methods to CityRepository:

    @RestResource(path = "name", rel = "name")
    Page<City> findByNameIgnoreCase(@Param("q") String name, Pageable pageable);
    
    @RestResource(path = "nameContains", rel = "nameContains")
    Page<City> findByNameContainsIgnoreCase(@Param("q") String name, Pageable pageable);
    
    @RestResource(path = "state", rel = "state")
    Page<City> findByStateCodeIgnoreCase(@Param("q") String stateCode, Pageable pageable);
    
    @RestResource(path = "postalCode", rel = "postalCode")
    Page<City> findByPostalCode(@Param("q") String postalCode, Pageable pageable);
  2. Build the application:

    $ gradle assemble
  3. Run the application:

    $ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar
  4. Access the application again using curl. Notice that hypermedia for a new search endpoint has appeared.

    $ curl -i "localhost:8080/cities"
    HTTP/1.1 200 OK
    Server: Apache-Coyote/1.1
    X-Application-Context: application
    Content-Type: application/hal+json
    Transfer-Encoding: chunked
    Date: Tue, 27 May 2014 20:33:52 GMT
    
    {
      "_links" : {
        "next" : {
          "href" : "http://localhost:8080/cities?page=1&size=20"
        },
        "self" : {
          "href" : "http://localhost:8080/cities{?page,size,sort}",
          "templated" : true
        },
        "search" : {
          "href" : "http://localhost:8080/cities/search"
        }
    },
    (Remainder omitted...)
  5. Access the new search endpoint using curl:

    $ curl -i "localhost:8080/cities/search"
    HTTP/1.1 200 OK
    Server: Apache-Coyote/1.1
    X-Application-Context: application
    Content-Type: application/hal+json
    Transfer-Encoding: chunked
    Date: Tue, 27 May 2014 20:38:32 GMT
    
    {
      "_links" : {
        "postalCode" : {
          "href" : "http://localhost:8080/cities/search/postalCode{?q,page,size,sort}",
          "templated" : true
        },
        "state" : {
          "href" : "http://localhost:8080/cities/search/state{?q,page,size,sort}",
          "templated" : true
        },
        "name" : {
          "href" : "http://localhost:8080/cities/search/name{?q,page,size,sort}",
          "templated" : true
        },
        "nameContains" : {
          "href" : "http://localhost:8080/cities/search/nameContains{?q,page,size,sort}",
          "templated" : true
        }
      }
    }

    Note that we now have new search endpoints for each of the finders that we added.

  6. Try a few of these endpoints. Feel free to substitute your own values for the parameters.

    $ curl -i "http://localhost:8080/cities/search/postalCode?q=75202"
    $ curl -i "http://localhost:8080/cities/search/name?q=Boston"
    $ curl -i "http://localhost:8080/cities/search/nameContains?q=Fort&size=1"

    Next let’s take a look at a few of the ``production ready'' endpoints added by Spring Boot Actuator.

Using Spring Boot Actuator

Try out the following endpoints. The output is omitted here because it can be quite large:

http://localhost:8080/beans

Dumps all of the beans in the Spring context.

http://localhost:8080/autoconfig

Dumps all of the auto-configuration performed as part of application bootstrapping.

Searching for DataSource will show the @Conditionals causing the embedded DB to be created:

"DataSourceAutoConfiguration" : [ {
      "condition" : "OnClassCondition",
      "message" : "@ConditionalOnClass classes found: org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType"
    } ],
    "DataSourceAutoConfiguration.JdbcTemplateConfiguration" : [ {
      "condition" : "DataSourceAutoConfiguration.DatabaseCondition",
      "message" : "existing auto database detected"
    } ],
    "DataSourceAutoConfiguration.JdbcTemplateConfiguration#jdbcTemplate" : [ {
      "condition" : "OnBeanCondition",
      "message" : "@ConditionalOnMissingBean (types: org.springframework.jdbc.core.JdbcOperations; SearchStrategy: all) found no beans"
    } ],
    "DataSourceAutoConfiguration.JdbcTemplateConfiguration#namedParameterJdbcTemplate" : [ {
      "condition" : "OnBeanCondition",
      "message" : "@ConditionalOnMissingBean (types: org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; SearchStrategy: all) found no beans"
    } ],
    "DataSourceAutoConfiguration.TomcatConfiguration" : [ {
      "condition" : "DataSourceAutoConfiguration.TomcatDatabaseCondition",
      "message" : "found database driver org.hsqldb.jdbcDriver"
    }, {
      "condition" : "OnBeanCondition",
      "message" : "@ConditionalOnMissingBean (types: javax.sql.DataSource; SearchStrategy: all) found no beans"
    } ],
    "DataSourceTransactionManagerAutoConfiguration" : [ {
      "condition" : "OnClassCondition",
      "message" : "@ConditionalOnClass classes found: org.springframework.jdbc.core.JdbcTemplate,org.springframework.transaction.PlatformTransactionManager"
    } ],
    "DataSourceTransactionManagerAutoConfiguration.TransactionManagementConfiguration" : [ {
      "condition" : "OnBeanCondition",
      "message" : "@ConditionalOnMissingBean (types: org.springframework.transaction.annotation.AbstractTransactionManagementConfiguration; SearchStrategy: all) found no beans"
    } ],
http://localhost:8080/env

Dumps the application’s shell environment as well as all Java system properties.

http://localhost:8080/metrics

Dumps all metrics currently being collected by Actuator, primarily response time and access counts for endpoints.

http://localhost:8080/mappings

Dumps all URI request mappings and the controller methods to which they are mapped.

Pushing to Cloud Foundry

  1. Make sure you have used the cf CLI to log into your Cloud Foundry service.

  2. Create an application manifest in manifest.yml:

    ---
    applications:
    - name: cities
      memory: 512M
      instances: 1
      path: build/libs/cities-0.0.1-SNAPSHOT.jar
      timeout: 180 # to give time for the data to import
  3. Push to Cloud Foundry with a random route to prevent collisions:

    $ cf push --random-route
    
    ...
    
    1 of 1 instances running
    
    App started
    
    Showing health and status for app cities...
    OK
    
    requested state: started
    instances: 1/1
    usage: 512M x 1 instances
    urls: cities-undeliverable-iatrochemistry.cf.mycloud.com
    
         state     since                    cpu    memory         disk
    #0   running   2014-05-27 04:15:05 PM   0.0%   433M of 512M   128.9M of 1G
  4. Access the application at the route provided by CF:

    $ curl -i cities-undeliverable-iatrochemistry.cf.mycloud.com/cities

Using Spring Cloud

  1. At present we’re still using the in-memory database. Let’s connect to a MySQL database service provided by Cloud Foundry. First we’ll create the service instance:

    $ cf create-service p-mysql 100mb-dev cities-db
    Creating service cities-db...
    OK
  2. Next add the service to your application manifest, which will bind the service to our application on the next push. We’ll also add an environment variable to switch on the ``cloud'' profile,

    ---
    applications:
    - name: cities
      memory: 512M
      instances: 1
      path: build/libs/cities-0.0.1-SNAPSHOT.jar
      timeout: 180
      services:                        # Add
      - cities-db                      # these
      env:                             # four
        SPRING_PROFILES_ACTIVE: cloud  # lines

    You can also accomplish the service binding by explicitly binding the service at the command-line:

    $ cf bind-service cities cities-db
    Binding service cities-db to app cities...
    OK
  3. Next we’ll add Spring Cloud and MySQL dependencies to our Gradle build. Comment or remove the hsqldb line add add the following in the dependencies section:

    dependencies {
        // ....
        compile("org.springframework.cloud:spring-cloud-spring-service-connector:1.0.0.RELEASE")
        compile("org.springframework.cloud:spring-cloud-cloudfoundry-connector:1.0.0.RELEASE")
        runtime("mysql:mysql-connector-java:5.1.25")
    }

    Since we’ve added new dependencies, re-run gradle idea or gradle eclipse to have them added to the IDE classpath.

  4. Next, let’s create the package org.example.cities.config and create in that package the class CloudDataSourceConfig. Add the following code:

    @Profile("cloud")
    @Configuration
    public class CloudDataSourceConfig extends AbstractCloudConfig {
        @Bean
        public DataSource dataSource() {
            return connectionFactory().dataSource();
        }
    }

    As before, have the IDE import the appropriate dependencies.

    The @Profile annotation will cause this class (which becomes Spring configuration when annotated as @Configuration) to be added to the configuration set because of the SPRING_PROFILES_ACTIVE environment variable we added earlier. You can still run the application locally (with the default profile) using the embedded database.

    With this code, Spring Cloud will detect a bound service that is compatible with DataSource, read the credentials, and then create a DataSource as appropriate (it will throw an exception otherwise).

  5. Add the following to src/main/resources/application.properties to cause Hibernate to create the database schema and import data at startup. This is done automatically for embedded databases, not for custom DataSources. Other Hibernate native properties can be set in a similar fashion:

    spring.jpa.hibernate.ddl-auto=create
  6. Build the application:

    $ gradle assemble
  7. Re-push the application:

    $ cf push
  8. Take a look at the env endpoint again to see the service bound in VCAP_SERVICES:

    $ curl cities-undeliverable-iatrochemistry.cf.mycloud.com/env
    ...
    "VCAP_SERVICES" : "{\"p-mysql\":[{\"name\":\"cities-db\",\"label\":\"p-mysql\",\"tags\":[\"mysql\",\"relational\"],\"plan\":\"100mb-dev\",\"credentials\":{\"hostname\":\"192.
    168.0.61\",\"port\":3306,\"name\":\"cf_84d72bc0_1fb9_427a_b8cc_a6cd7526f3c4\",\"username\":\"qRouPyXXexyXRRxo\",\"password\":\"JsF1GdLT1mN5WMDS\",\"uri\":\"mysql://qRouPyXXexyXRR
    xo:[email protected]:3306/cf_84d72bc0_1fb9_427a_b8cc_a6cd7526f3c4?reconnect=true\",\"jdbcUrl\":\"jdbc:mysql://qRouPyXXexyXRRxo:[email protected]:3306/cf_8
    4d72bc0_1fb9_427a_b8cc_a6cd7526f3c4\"}}]}",
    ...

    The application is now running against a MySQL database.

Customizing the DataSource

  1. You can customize the database connection that Spring Cloud creates with a few lines of code. Change the dataSource method in CloudDataSourceConfig to add some pooling and connection configuration:

    @Bean
    public DataSource dataSource() {
        PooledServiceConnectorConfig.PoolConfig poolConfig =
                new PooledServiceConnectorConfig.PoolConfig(20, 200);
    
        DataSourceConfig.ConnectionConfig connectionConfig =
                new DataSourceConfig.ConnectionConfig("characterEncoding=UTF-8");
        DataSourceConfig serviceConfig = new DataSourceConfig(poolConfig, connectionConfig);
    
        return connectionFactory().dataSource("cities-db", serviceConfig);
    }
  2. Build the application:

    $ gradle assemble
  3. Re-push the application:

    $ cf push

Wrapping It Up

You now have a fully functional REST microservice backed by a MySQL database running on Cloud Foundry, consisting of four Java classes with no boilerplate code or configuration.

Spring Boot, Spring Data JPA, Spring Data REST, and Spring Cloud provide the framework and scaffolding, allowing you to write only domain-specific code. Production-ready endpoints are provided by Spring Boot to help manage and introspect into the application.

Cloud Foundry provides a fast, easy, and efficient platform for deploying and scaling the microservice and connecting to a managed database.