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.
-
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
-
-
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.
-
Create a directory to house your code, and into that directory unzip starter.zip.
-
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.
-
Add a runtime dependency on the HyperSQL in-memory database to build.gradle:
dependencies { // ... runtime("org.hsqldb:hsqldb") }
-
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.
-
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> { }
-
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); } }
-
Build the application:
$ gradle assemble
-
Run the application:
$ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar
-
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.
-
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.
-
Build the application:
$ gradle assemble
-
Run the application:
$ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar
-
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 } }
-
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.
-
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);
-
Build the application:
$ gradle assemble
-
Run the application:
$ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar
-
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...)
-
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.
-
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.
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.
-
Make sure you have used the cf CLI to log into your Cloud Foundry service.
-
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
-
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
-
Access the application at the route provided by CF:
$ curl -i cities-undeliverable-iatrochemistry.cf.mycloud.com/cities
-
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
-
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
-
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.
-
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).
-
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
-
Build the application:
$ gradle assemble
-
Re-push the application:
$ cf push
-
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.
-
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); }
-
Build the application:
$ gradle assemble
-
Re-push the application:
$ cf push
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.